多Agent系统在本地跑得再好,落不了地也是白搭。我最近把一套基于OpenClaw的多Agent协作系统接到了飞书机器人上,让规划、执行、知识检索几个角色在一个群聊里协同处理任务,效果比预想中好,但中间踩的坑也着实不少。这个方案解决的核心问题是:OpenClaw负责把不同能力的Agent编排起来,飞书机器人负责把消息触达和结果回传,二者一旦打通,办公场景里的人就能通过普通的群聊对话,直接驱动一套多Agent系统干活。适合正在做Agent应用落地、想找一个稳定IM入口的开发者参考,也适合那些已经跑通单体Agent、正准备往多Agent方向演进的技术团队。
1. 方案解读:从零散需求到可落地的总体架构
1.1 核心需求拆解:多Agent要解决的不只是聊天
很多人一听"多Agent",第一反应是"多开几个聊天机器人"。实际做下来完全不是这回事。多Agent真正解决的问题是角色分工和任务协同,比如用户问一句"帮我查一下上周的数据,按区域汇总,再生成一份简短结论",这里至少包含数据查询、分析、汇总报告三个环节。如果全部塞给一个Agent,它既要理解数据口径,又要掌握汇总逻辑,还要处理输出格式,任何一个环节出错都会导致整体回答不可用。拆成多个Agent之后,每个Agent只负责一件事,再通过编排层把它们的结果合并,整个流程的可控性和可解释性都会好很多。
这个项目里我把Agent拆成了三类:主调度Agent负责理解用户原始意图并拆分任务,专业执行Agent负责调用工具或查询数据,输出Agent负责把结果整理成适合在聊天里展示的形式。飞书机器人在这中间扮演的角色不是"某个Agent的替身",而是所有Agent的统一消息入口。一个群聊里只有一个机器人,但背后站着多个Agent,用户感知不到切换过程,这种统一入口的设计对办公场景来说非常重要。
核心需求其实可以浓缩成四点:统一消息接入、任务路由分发、会话上下文隔离、异步结果回传。只要把这四件事做好,多Agent系统在IM里就算真正落地了。最开始我踩过的坑是只顾着选模型和调提示词,忽略了消息语义设计,结果Agent在本地测试全对,一进飞书群就被各种@逻辑和消息格式搞得晕头转向。
1.2 整体架构分层:OpenClaw与飞书机器人之间的消息通道
整个系统的消息流转可以分成五层,每一层职责都很单一,这也是我调试时比较省心的原因。第一层是飞书客户端,用户在这里发消息、点卡片按钮。第二层是飞书开放平台,它负责把消息事件推送给我们的服务,或者在长连接模式下把事件直接投递到适配服务。第三层是适配服务,这一层做的事情是"协议转换":把飞书的事件结构解析出来,转成OpenClaw能理解的任务对象;等任务处理完,再把Agent的回复转成飞书消息API需要的内容格式。第四层是OpenClaw编排层,任务队列、调度器、Agent注册表都在这层,它不关心消息来自哪里,只关心拿到一个任务之后怎么拆、怎么派、怎么汇总。第五层是大模型和工具层,Agent实际调用的推理接口、检索接口、业务系统接口都在这里。
这个分层结构最大的优点是"适配层只翻译业务,不执行业务逻辑"。我见过一些项目把飞书消息判断直接写进Agent内部,比如在提示词里告诉模型"如果消息来自飞书群聊就怎么怎么样",结果模型经常被这种指令干扰。正确做法是适配层把"消息来源""发送者身份""是否@机器人"这些信息清洗好,作为结构化上下文传给OpenClaw,Agent只关心语义,不关心IM平台差异。
消息流向用文字描述就是:用户发消息 → 飞书开放平台推送事件 → 适配服务校验并入队 → OpenClaw调度器取出任务 → 主调度Agent拆解并派发给子Agent → 子Agent处理并回写结果 → 调度器汇总 → 适配服务调用飞书发送接口回消息。整条链路从接收到回复,基本都是异步的,这在第4部分会展开讲。
1.3 为什么把飞书机器人作为统一入口
市面上办公IM平台不少,我最终选择飞书机器人,不是因为它功能多,而是它的开放平台对机器人开发者来说确实友好。首先是接入模式灵活,既支持传统的Webhook回调,也支持长连接模式,长连接模式下不需要公网回调地址,本地开发体验好很多,这个特性在开发初期真的救了我一命。其次是权限模型清晰,机器人能收到哪些消息、能读哪些群聊、能发哪些内容,都可以在应用配置里按需勾选,不会出现"为了读一条@消息不得不给全部聊天记录权限"的情况。
还有一个很现实的原因:飞书的消息卡片交互能力比较成熟。Agent执行任务往往需要几十秒甚至几分钟,如果只是纯文本回复,用户会反复追问"好了没"。用消息卡片可以先回一张"任务处理中"的卡片,后续再把结果更新进同一张卡片,这种交互在办公场景里非常自然。
我补充一句:如果你所在团队用的是其他办公IM,这套方案的核心流程也不用推翻,适配层换一套消息解析和发送实现,编排层基本可以原样复用。我选飞书只是为了开发效率,并不是说OpenClaw和飞书做了深度绑定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:把OpenClaw和飞书开放平台先打底
2.1 OpenClaw运行环境:Docker部署与基础依赖
OpenClaw本身的安装不复杂,但因为它要管理多个Agent、共享上下文和任务队列,运行环境不建议太简陋。我采用的是Docker Compose方式,把OpenClaw编排服务、适配服务、Redis、数据库放在同一套网络里,方便联调。
yaml复制services:
openclaw:
image: openclaw/openclaw:latest
ports:
- "8000:8000"
volumes:
- ./agents:/app/agents
- ./data:/app/data
environment:
- OPENCLAW_LOG_LEVEL=debug
- REDIS_URL=redis://redis:6379/0
- DATABASE_URL=sqlite:////app/data/openclaw.db
depends_on:
- redis
adapter:
build: ./adapter
ports:
- "9000:9000"
environment:
- FEISHU_APP_ID=${FEISHU_APP_ID}
- FEISHU_APP_SECRET=${FEISHU_APP_SECRET}
- FEISHU_VERIFICATION_TOKEN=${FEISHU_VERIFICATION_TOKEN}
- FEISHU_ENCRYPT_KEY=${FEISHU_ENCRYPT_KEY}
- OPENCLAW_ENDPOINT=http://openclaw:8000
depends_on:
- openclaw
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
这里有几个细节值得注意。第一,适配服务和OpenClaw编排服务必须分开放,因为扩展节奏不一样:消息量大时先扩展适配服务,Agent任务重时再扩展编排节点。第二,Redis在这里承担的是消息去重、任务队列、分布式锁三个职责,不要图省事用SQLite代替,尤其是后面要处理并发的时候。第三,Agent定义文件放在挂载目录里,这样调整Agent行为不用重新构建镜像,改完配置文件重启进程即可。
如果不想用Docker,直接在服务器上装Python 3.11+环境也可以跑,但OpenClaw依赖的包数量不少,换环境时容易漏。用容器能省掉一大半环境问题,我建议一开始就上容器。
2.2 飞书自建应用:权限、事件订阅与机器人能力
飞书这边的配置流程,第一步是在开放平台创建一个企业自建应用,这一步需要管理员权限,最好提前跟团队里的管理员打声招呼。创建之后先别急着写代码,把应用信息记下来:App ID、App Secret、Verification Token、Encrypt Key,这四个参数后面全都用得到。
然后是开启机器人能力。在应用功能里找到"机器人",启用之后应用才有收发消息的入口。接着配置权限,我实际用到的权限不多,重点就几个:
| 权限用途 | 建议权限项 | 说明 |
|---|---|---|
| 接收私聊消息 | im:message | 读取用户发给机器人的单聊消息 |
| 接收群聊中@机器人的消息 | im:message.group_at_msg | 只读群聊里@机器人的消息 |
| 发送消息 | im:message:send_as_bot | 以机器人身份发送消息 |
| 读取群信息 | im:chat | 获取会话基本信息,用于群聊回复定位 |
需要提醒的是,不同企业版本的开放平台,权限名称可能略有差异,我这里写的是常规叫法,你配置的时候以开放平台界面实际展示为准。权限申请之后,有些需要发布应用版本并等待审核,内部测试环境通常审核很快,但也要留出时间。
事件订阅是重头戏。我订阅的是im.message.receive_v1,也就是"收到消息"事件。在事件订阅页面,飞书会要求你填写请求地址,如果选择长连接模式,就不用填公网地址,我会在下一小节细说。订阅完后一定要记得发布应用版本,不发布的话配置不会生效,这个坑我踩过一次,排查半天发现权限根本没加载。
2.3 本地联调的关键:回调地址验证与加密配置
飞书事件推送有两种模式,我强烈建议本地开发阶段直接用长连接模式,也就是WebSocket模式。这个模式下,飞书开放平台会主动维持一条长连接通道,把事件实时推给本地服务,不需要配置公网回调地址,也不需要内网穿透。只有在生产环境或者需要多实例负载均衡时,才切到Webhook模式,把回调地址指向公网可访问的网关。
如果你确实需要用Webhook模式,或者生产环境用Webhook接入,那就必须处理"URL验证"这个环节。飞书在保存回调地址时会向这个地址发送一个POST请求,包含challenge字段,服务端必须原样返回这个字段值才能验证通过。以下是适配服务里处理验证的简化代码:
python复制from flask import Flask, request, jsonify
app = Flask(__name__)
VERIFICATION_TOKEN = "your_verification_token"
@app.route("/webhook/callback", methods=["POST"])
def webhook_callback():
data = request.get_json()
# 第一次配置回调地址时,飞书会发送URL验证请求
if data.get("type") == "url_verification":
return jsonify({"challenge": data["challenge"]})
# 普通事件请求里,header中带token字段
event_header = data.get("header", {})
if event_header.get("token") != VERIFICATION_TOKEN:
return jsonify({"code": 1, "msg": "invalid token"}), 403
# 这里先返回成功,再异步处理事件
dispatch_event(data)
return jsonify({"code": 0, "msg": "success"})
加密模式是另一个容易翻车的地方。如果开了Encrypt Key,飞书推送的内容会包在encrypt字段里,需要先用AES解密才能拿到真实事件数据,challenge也在加密内容里。解密流程:先用Encrypt Key做AES-256-CBC解密,再解析JSON,然后才是token校验。不少人会在这一步搞反顺序,先去校验token,结果发现token是密文,永远匹配不上。
我的建议是:测试阶段关掉加密模式,把事件明文打开,优先把业务链路跑通;等确认没问题了再开加密,避免一开始就双重排查。
3. 核心对接:事件接收、消息解析与Agent分发
3.1 事件回调处理:签名校验、解密与消息去重
飞书推送的消息事件,核心结构大概是这样的:
json复制{
"schema": "2.0",
"header": {
"event_id": "a1b2c3...",
"event_type": "im.message.receive_v1",
"token": "verification_token",
"create_time": "1700000000000"
},
"event": {
"message": {
"message_id": "om_...",
"chat_id": "oc_...",
"message_type": "text",
"content": "{\"text\":\"你好\"}",
"mentions": []
},
"sender": {
"sender_id": {"open_id": "ou_..."},
"sender_type": "user"
}
}
}
这里有几个反直觉的点。第一个,event.message.content不是纯文本,而是一个字符串化的JSON,比如文本消息的content是{"text":"你好"},需要先json.loads再取text字段,否则你拿到的是一坨带引号的字符串。第二个,header.event_id是每条事件的唯一ID,但飞书在超时未确认或网络异常时可能会重试推送同一个event_id,所以必须做去重处理。
去重方案我直接用Redis的SETNX指令,以event_id作为key,设置600秒过期时间。如果SETNX返回0,说明这条事件已经处理过,直接丢弃;返回1才进入后续流程。代码大致是这样:
python复制import redis
r = redis.Redis.from_url("redis://redis:6379/0")
def is_duplicate(event_id: str) -> bool:
# 如果key不存在则设置并返回True,存在则返回False
return not r.set(f"event:{event_id}", "1", nx=True, ex=600)
除了去重,回调服务还必须尽快响应飞书。飞书对Webhook的响应时间要求是3秒内返回200,如果超时它会认为投递失败并触发重试。所以回调服务里绝对不能同步调用大模型,正确做法是把事件丢进任务队列,立即返回成功,真正的处理放到后台进程里做。整个链路做成异步之后,回调服务基本不会超时。
3.2 多Agent路由:按意图、群聊@和任务类型分发
事件解析完成之后,就轮到OpenClaw的调度器决定把任务交给谁。我先在Agent注册表里定义好每个Agent的基本信息:
python复制agents = {
"planner": {
"name": "主调度Agent",
"description": "理解用户需求,拆分任务,汇总结果",
"capabilities": ["任务拆分", "意图识别", "结果汇总"]
},
"executor": {
"name": "执行Agent",
"description": "调用工具,执行数据查询、文件处理等操作",
"capabilities": ["工具调用", "代码执行", "数据查询"]
},
"writer": {
"name": "输出Agent",
"description": "把结果整理成适合聊天展示的文本或卡片",
"capabilities": ["文本润色", "格式转换", "摘要生成"]
}
}
路由策略我分三层做,优先级从高到低。第一层是显式路由:如果用户在群聊里@了某个Agent对应的名字,比如"让执行Agent跑一下这个脚本",就直接把任务派给对应Agent。第二层是规则路由:有些高频请求可以用关键词精确匹配,比如消息以"查数据"开头,就归执行Agent。第三层是意图路由:前两层都没命中时,由主调度Agent通过大模型判断用户意图,再把任务拆给下游。
群聊里的@解析需要特别注意。飞书事件中的mentions字段是一个数组,里面每个元素包含被@对象的ID。我们要做两件事:一是看当前用户是否@了我们的机器人,如果完全没有@,说明这条消息跟我们无关,直接过滤;二是如果有@,再判断@的对象里是否包含子Agent身份标识,如果包含,就把任务定向派给子Agent。如果子Agent只是一个逻辑概念,没有独立飞书身份,那就在消息里通过文本匹配Agent名字来实现定向,比如用户说"让数据分析Agent看一下",调度器解析到"数据分析Agent"这个关键词,就路由过去。
这里有个通用做法推荐:让主机器人统一接收所有@消息,内部再做分发,而不是给每个Agent都创建一个飞书机器人。原因很简单,一个群聊里如果躺着三四个机器人,用户根本分不清该找谁,产品体验会很差。内部路由不改变用户侧感知,只是调度逻辑更清晰。
3.3 回复消息:单聊/群聊的发送策略与富文本卡片
任务处理完之后,适配服务要把结果发给用户。这一步看似简单,实际有不少细节。
单聊场景下,直接调用飞书消息发送接口,receive_id传用户的open_id,receive_id_type传open_id,msg_type根据内容类型指定。群聊场景下,问题就来了:如果群里有多个用户,回复消息不带任何@信息,别人不知道这条回复是发给谁的。所以在群聊里回消息时,要在content前面拼接上"@用户ID"的标记,飞书会在消息里自动显示成@某人,用户才知道这是给他的回复。
发送接口请求体大致是这样的:
json复制{
"receive_id": "oc_xxx",
"receive_id_type": "chat_id",
"msg_type": "text",
"content": "{\"text\":\"<at user_id=\\\"ou_xxx\\\"></at> 这是你的任务结果:...\"}"
}
除了纯文本,富文本卡片是更推荐的形式。Agent执行多步骤任务时,结果往往有状态、有耗时、有结构化数据,纯文本一长串既难读也不美观。我使用的卡片模板大致如下:
json复制{
"config": {"wide_screen_mode": true},
"header": {
"title": {"tag": "plain_text", "content": "任务执行结果"},
"template": "blue"
},
"elements": [
{"tag": "markdown", "content": "**状态**:成功\n**耗时**:12s\n**任务ID**:task_001"},
{"tag": "hr"},
{"tag": "markdown", "content": "本次查询共返回38条记录,详细报告见下方说明"}
]
}
使用卡片时要注意content字段必须是一个JSON字符串,不能直接塞对象。中文不需要手动转成Unicode,正常UTF-8字符串就行。消息长度超过限制时,把长内容拆成多条消息发送,或者用"分段加载"的方式,先发摘要再发详情。
4. 多Agent协作:从"多个机器人"到"一个团队"
4.1 Agent间通信:消息总线与共享上下文
多Agent协调工作,最忌讳的就是Agent之间互相直接调用。我在OpenClaw里给每个Agent规定的交互方式很简单:所有Agent不直接知道其他Agent的存在,只跟消息总线打交道。主调度Agent把子任务封装成一条消息发到总线,执行Agent订阅了对应主题,消费任务后把结果写回总线,主调度Agent再订阅结果主题拿到回执。这套模式本质上是发布订阅模型,好处是每个Agent都可以独立替换,不影响整体流程。
任务消息的结构需要约定统一,不然多Agent协作就是一团乱麻。我用的基础结构是:
python复制{
"task_id": "task_001",
"session_id": "session_xxx",
"parent_task_id": None,
"agent": "executor",
"action": "run_tool",
"payload": {"command": "query_sales_data", "params": {"start": "2025-01-01"}},
"status": "pending",
"created_at": "1700000000000"
}
task_id全局唯一,用来链路追踪;session_id是整个会话的上下文标识,后续所有Agent的中间产出都挂在这个ID下面;parent_task_id标记该任务由哪个父任务拆分而来。子Agent处理完后,把结果更新到共享上下文里,主Agent最终从共享上下文取所有子结果做汇总。
共享上下文的存储我用了Redis Hash,以session_id为key,字段名是agent_name:step,值就是中间结果。这样一个会话的所有Agent产出都能串起来,主Agent汇总时不需要向每个子Agent单独问"你刚才的结果是什么",直接读最新字段即可。
4.2 群聊场景下的@处理与话题隔离
群聊比单聊复杂很多,因为存在多个人、多个话题、甚至多个子讨论线程并存的情况。如果不做处理,很容易出现A用户问了一个问题,B用户又问了一个不相关的问题,两个任务的上下文在同一个会话里互相污染,Agent回答起来精神分裂。
我把消息隔离分成两级。第一级是会话隔离,用chat_id区分不同群聊或单聊,不同chat_id的会话绝不共享上下文。第二级是话题隔离,飞书群聊里存在话题线程,如果消息带有thread_id,就用chat_id + thread_id作为上下文key;如果消息没有thread_id,说明这是一个非话题群,只能用chat_id作为key。两种模式要兼容处理,我在代码里统一封装了一个get_session_key(event)函数,专门负责计算当前消息对应的会话键。
@处理这块,重点判断两点。第一,消息是否@了机器人本体,飞书事件里的mentions数组包含所有被@的对象,我们要找到自己的open_id是否在里面。第二,如果用户发了@all,mentions里面可能包含很多用户,但不一定包含机器人,这时候不能误判为"这条消息需要我处理"。正确做法是:只要mentions数组里没有机器人自身的id,无论其他人@得多热闹,都直接忽略。
还有一个小细节:当用户@机器人并且@了某个具体用户时,说明用户是在群里向那个人提问,顺带让机器人参与,这种消息不一定需要Agent完整执行,可能只需要一句简短回复。我现在的策略是,如果@机器人排在其他用户之前,按正常任务处理;如果@机器人夹在中间,倾向于只给提示不执行长任务,这个阈值可以按团队习惯调节。
4.3 超时、重试与任务队列:保证结果不丢
多Agent链路跑起来的第一个拦路虎就是超时。一次大模型调用可能就要几十秒,再加上工具调用、子Agent汇总,总耗时很容易超过飞书对Webhook的3秒响应限制。所以从设计上,整个流程必须是异步的:回调服务只负责接收和入队,后台Worker负责真正执行,执行完成后再通过飞书API主动把结果发回去。
我在Redis里维护了一个简单的任务队列,数据结构是List,用LPUSH入队、BRPOP消费。任务状态用字符串标识:pending、running、success、failed。Worker在处理前先把状态置为running,并把开始时间记录下来;处理完成后更新状态。如果Worker在处理过程中崩溃,任务会一直停在running状态,这就要靠一个定时巡检任务,把超过N分钟仍处于running状态的任务捞出来重新置为pending,再次入队。
发送结果这一步也有重试逻辑。飞书API偶尔会抖动,或者因为权限问题发送失败,不能假设一次就能成功。我实现了三层重试:第一次失败后等1秒重试,第二次等5秒,第三次等30秒,超过三次把任务标记为failed,并通知管理员。这里要注意,重试的是"发送消息"动作,不是重新执行Agent任务,否则可能产生重复的成本消耗。
python复制def safe_send_message(payload, max_retries=3):
for attempt in range(max_retries):
try:
resp = feishu_client.send_message(payload)
if resp.code == 0:
return True
except Exception:
pass
time.sleep(1 * (attempt + 1) ** 2)
return False
超时处理也要考虑"用户已经等得不耐烦"的情况。如果任务预计超过20秒,我建议先发送一条"任务处理中"的卡片,给用户一个预期,也避免他重复发送相同提问。用户看到"处理中"之后,就算Agent最终失败了,他也会知道系统在干活,而不是怀疑消息丢了。
5. 常见问题与排查技巧实录
5.1 事件回调验证一直失败怎么办
这个问题几乎每个第一次接入的人都会遇到。表现为:在飞书开放平台保存回调地址时提示"URL验证失败",或者保存成功但事件完全收不到。
排查顺序按优先级来。第一步,用命令行手动发一个POST请求测试你的回调地址,确认地址本身可访问,返回体确实是JSON格式的challenge。很多本地开发环境把地址写成了localhost,飞书服务器当然访问不到,这就是长连接模式存在的意义。第二步,检查token是否匹配,注意事件里的token在header字段里,不是顶层字段,有人写代码时取的是data["token"],实际应该是data["header"]["token"]。第三步,如果开了加密模式,确认是先解密再取challenge,不是直接返回原密文。
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
| URL验证提示失败 | 公网无法访问回调地址 | 用长连接模式,或检查组网配置 |
| 返回challenge但验证仍失败 | 返回值带引号或换行符 | 确保返回值是JSON对象,不是字符串 |
| 事件能收到但业务报403 | token校验字段位置不对 | 检查data.header.token |
| 事件里全是密文 | 加密模式未解密 | 先AES-CBC解密再解析 |
5.2 消息收到但回复乱码或格式错乱
飞书对消息格式要求比较严格,乱码问题多半出在content字段的处理上。我见过最典型的错误:直接从事件里取出content,发现它是个字符串,就直接当成纯文本展示,结果看到的是{"text":"你好"}这堆原始JSON。正确做法是先json.loads(content),再取text字段。
回复乱码的另一个来源是卡片JSON写错。发送卡片消息时,content的值是一个序列化成字符串的JSON对象,如果直接在请求体里放了一个裸JSON对象,或者JSON里多了尾逗号,飞书会报参数错误。格式错乱则多半是使用了富文本标签但标记不匹配,比如<at>标签没有闭合。建议每次拼接消息内容后用JSON解析函数校验一遍,能在发送前拦截大部分问题。
5.3 消息重复处理、丢失事件怎么办
重复处理和丢失事件看起来是相反的两个问题,根源其实都是异步机制。重复是因为飞书在超时后会重试同一个event_id,如果你没有做幂等,同一个用户消息就会被Agent执行两次,可能在群里出现两条相同的回答,也可能因为执行副作用造成数据重复写入。解决办法就是我前面说的SETNX去重,没有别的更省事的路径。
丢失事件一般发生在回调服务崩溃或者消息入队失败的时候。如果Worker进程在读取任务后崩溃,任务既不在队列里也没完成,就会无声无息丢掉。我的做法是做一层"任务落地确认":消息入队时把event_id对应的任务状态写入Redis,Worker开始处理前标记为running,处理完成标记为success。巡检脚本每5分钟扫一次,发现有超过30分钟仍是running的任务,强制重新入队。这样至少能保证任务不会因为进程崩溃而消失,最多是延迟处理。
5.4 多Agent并发时的状态互踩问题
并发问题在单Agent阶段不明显,一旦多个Agent同时处理同一个会话的任务,问题立刻暴露。最典型的是同一用户连续发了两条消息,两个任务并行执行,都试图往同一个session上下文里写入结果,后写入的覆盖了先写入的,最终汇总结果少了一段内容。
我先在代码层加了一把Redis分布式锁,锁的粒度是session_key,也就是同一会话同时只允许一个任务进入写上下文的临界区。锁的过期时间要设置合理,太短会导致长时间任务被误杀,太长会阻塞后续任务;我设的是120秒,每20秒续期一次。另一个做法是给上下文写入加版本号,每次写入检查版本号是否匹配,不匹配就说明有其他Agent改过,强制重读再更新。两种方式可以叠加。
日志链路追踪也是排查并发问题的关键。我给每个任务生成一个trace_id,从飞书事件开始,到主Agent拆解、子Agent执行、最终回复发送,所有日志都打印这个trace_id。定位问题时,按trace_id搜日志就能看到完整链路,而不用在多个服务日志里大海捞针。
我个人多Agent系统接IM最大的体会是:技术难点根本不在模型聪明不聪明,而在消息语义的一致性和任务状态的同步。先把单Agent从飞书收消息、回消息这条链路跑通,再加第二个Agent,比一上来就铺好几个Agent要稳得多。另外,事件去重、任务状态这些看似不起眼的基础设施,才是生产环境能不能长期跑下去的关键。最后分享一个小技巧:把适配服务的关键日志按event_id和trace_id双维度输出,排查问题时你会感谢自己当初多写了那么几行日志。
