
上午十点运维群里又有人我“订单服务 QPS 现在多少帮我拉一下。”我打开监控面板复制了一段数据贴回去。五分钟后另一个同事问的是同一件事下午三点又来一次。这些提问本身不复杂但扛不住频率高——每个问题都要有人找到对应系统、拼接上下文、筛选数据、再整理成一段人能看懂的结论。后来我慢慢意识到这不只是“做个机器人”的问题而是要在团队内部搭一层 Agent 基础设施把“钉群提问”当作统一入口路由到不同能力再以任务的形式交付结果。这篇文章就从我的实践出发记录从钉群消息接入、意图路由、Agent 执行到任务交付的全链路设计思路和踩坑经验。这套东西目前服务我们内部十几个群覆盖运维查询、日志检索、发布辅助、知识问答四类场景。如果你正在做团队级的 AI Agent或者想把群机器人升级成真正能干活的任务系统这篇内容应该能给你一些可直接落地的参考。1. 从钉群到 Agent 运行时消息接入层是怎么设计的1.1 为什么选钉群机器人而不是自建 IM刚开始讨论入口时有人提议做独立的 Web 页面或者自研 IM 机器人理由是可以完全控制交互体验。但实际评估下来我们放弃了。团队原本就重度使用钉钉群、组织架构、权限体系、审批流都已经跑得很成熟让用户再去一个新平台提问学习成本不说光推广就是个无底洞。钉群机器人最大的价值是“零迁移成本”。用户在原来的工作场景里一下机器人和真人一样自然。群本身还自带上下文同一个群里的提问天然围绕同一类业务这给后续任务路由提供了很强的先验信息。比如“IT 运维群”里的问题大概率是查监控、查日志“发布群”里大概率是版本和变更问题这类信号在自建 IM 里反而不容易拿到。另外钉钉自带审批和通知能力Agent 执行写操作时可以直接发一条审批卡片到群里申请人在手机上一键确认完全不用额外做一套审批 UI。这是自建方案很难绕过的成本。1.2 Webhook 模式与 Stream 模式的取舍钉钉机器人接入方式有几种最开始我用的是自定义机器人 Webhook后面踩了坑才换成 Stream 模式。两者的本质差别在于Webhook 是单向的适合机器人主动推消息如果要接收群里的消息并处理官方推荐的方式是事件订阅而 Stream 模式通过长连接接收事件不需要在公网暴露回调地址。我整理过两者的对比接入方式是否需要公网回调支持的交互能力适用场景自定义机器人 Webhook不需要只能推送只能发消息不能接收告警通知、定时播报钉钉事件订阅HTTP需要公网 HTTPS 回调可接收消息需验签与重试有公网入口的团队钉钉 Stream 模式不需要公网可接收消息长连接内网服务、无公网入口我们最终选 Stream 模式原因很直接内部服务大多跑在私网环境没有对外暴露回调地址的条件。用 HTTP 订阅还得在网关层加一堆验签和重试逻辑而 Stream 模式维持一个长连接消息推送由钉钉主动发起部署省心得多。但 Stream 模式也有个容易被忽略的问题连接断开后的事件补推。长连接偶发重连时消息可能重复也可能短暂丢失。所以从第一行代码开始我就把消息幂等处理放在最前面。1.3 消息协议把“群聊”变成结构化事件收到钉钉推送后第一步不是直接调模型而是把非结构化的群聊消息统一转换成内部事件。我在入口层定义了一个标准消息结构{ event_id: event_8f3a2b1c, conversation_id: cid_xxxxx, sender_id: user_12345, sender_nick: 李明, msg_type: text, text: { content: agent 帮我查一下订单服务最近一小时的QPS }, at_agent: true, timestamp: 1690000000000 }这个结构有几个关键字段event_id用来做幂等conversation_id用来隔离会话上下文sender_id用来做权限判断at_agent用来判断这条消息是否需要机器人响应。转换之后还做了一条清洗流水线去掉消息里的前缀、去掉多余空格、保留必要的命令关键词。有些用户习惯说“你好”“谢谢”这类寒暄消息不需要进入 Agent 执行链路直接走“闲聊短路”回一句即可。这一步能省掉大量无意义的模型调用。2. 意图识别与任务路由避免“一问就崩”2.1 路由识别节点分类先行模型兜底消息接入后最关键的一步是路由识别。很多团队一上来就让大模型直接回答所有问题结果就是模型经常答非所问甚至在一句话里把查监控、查日志、发通知的能力混着用场面很容易失控。我参考了 Agent 框架里常见的“路由识别节点”思路先做规则分类再做模型分类兜底。规则分类的好处是稳定、便宜、可排查像“查日志”“扩容”“发布”“QPS”这类高频关键词直接命中对应的任务管道完全不需要走大模型。def route_message(message: str) - str: # 优先级从高到低先处理明确的指令动词 if re.search(r查日志|日志|log, message, re.I): return log_query if re.search(r扩容|伸缩|发布|上线, message, re.I): return ops_execute if re.search(rQPS|RT|错误率|监控, message, re.I): return monitor_query if re.search(r文档|wiki|怎么|如何, message, re.I): return doc_search # 都不匹配交给模型做意图分类 return llm_classify(message, candidates[ monitor_query, log_query, ops_execute, doc_search, knowledge_query, chitchat ])路由分类和人工客服的分诊是一个道理。如果所有问题都直接给专家大模型专家很快就会疲惫而且成本不可控。规则分类在前模型兜底在后既保证了高频场景的响应速度又保留了语义理解的覆盖能力。2.2 Skill 与 Agent 的边界别把所有能力都塞进一个 Agent很多刚接触 Agent 开发的人会把所有工具都塞到一个 Agent 里然后在提示词里写“你可以做监控、可以查日志、可以发布、可以写代码”。结果模型在推理时经常不知道该先调哪个工具上下文一长就开始混乱。这里必须搞清楚 Skill 和 Agent 的区别。我个人的理解Agent 是一个拥有记忆、上下文、推理循环和任务拆解能力的执行主体Skill 则是这个主体可以调用的标准能力单元可以是一段提示词模板、一个工具函数、或者一组固定执行流程。Agent 负责“决策用哪个 Skill、怎么组合”Skill 负责“把某件事做对”。我们早期尝试过一个大而全的 Agent后来拆成了多个轻量 Agent运维查询 Agent、日志分析 Agent、变更执行 Agent、知识问答 Agent。每个 Agent 内部挂载与自身职责相关的 Skill再通过一个 Supervisor Agent 统一接受任务并按意图分发给子 Agent。这样每个 Agent 的 prompt 短、职责单一、故障隔离测试起来也方便。2.3 上下文记忆从单轮问答到多轮任务钉群里的提问从来不是单轮的。典型的对话是这样的用户帮我查一下订单服务。Agent请问需要查什么指标用户QPS。Agent您好订单服务当前 QPS 是 xxx。用户再对比一下昨天同一时间。第二句“QPS”单独拿出来谁也看不懂。所以必须按会话维度维护上下文。我使用了 Redis 做短期记忆key 用conversation_id sender_idvalue 保存最近十轮的对话摘要和关键实体。但记忆不是越多越好。早期我把最近十轮完整消息全部塞给模型结果 token 消耗暴涨响应也变慢。后来做了两个改进一是对历史做摘要压缩只保留“用户诉求、Agent 已执行动作、关键数据结果”二是提取关键实体比如服务名、环境、时间范围在后续轮次中自动补全为完整查询条件。这套短期记忆对多轮任务很有帮助但它解决不了长期偏好问题。比如某位用户永远查生产环境另一位用户只关心预发布环境这类信息我们后来通过向量库做长期记忆按用户维度存储在路由阶段注入到提示词里。记忆不是越多越好而是“够用、可解释、方便清理”。3. Agent 执行层harness、工具注册与多 Agent 协作3.1 Harness 与 Agent 的关系执行框架不是模型在 Agent 项目里我经常被问到“用的什么模型”“为什么不用某某框架”。模型当然是核心但真正决定一个 Agent 能不能稳定落地的是 harness——也就是 Agent 的执行框架。我的理解是harness 负责工程层面的循环控制接收任务、规划步骤、调用工具、观察结果、判断是否继续还要处理超时、重试、错误恢复和日志埋点。如果只用裸的大模型 API不做 harness那么 Agent 就只是一次性的“文本生成器”。你让它“查完日志再发个统计”它可能只调了第一个工具就结束或者在一句话里幻觉出根本不存在的数据。所以我们在 Agent 之上套了一层轻量 harnessclass Harness: def run(self, agent, task): ctx {task: task, current_step: 0} while ctx[current_step] agent.max_steps: action agent.plan(ctx) # 决策下一步动作 if action.is_final(): return action.result observation self.call_tool(action, ctx) ctx[observations].append(observation) ctx[current_step] 1 raise MaxStepsExceeded()这个 harness 很小但把最关键的循环逻辑控制住了。我们调研过现成的开源 Agent 框架也参考过 Spring AI 2.0 的全链路设计思路但最终没有整个搬进来因为团队内部工具链比较特定自定义 harness 可以针对钉钉场景做大量优化比如直接支持内部任务系统、审批流和日志埋点。3.2 工具/Skill 注册与权限控制Agent 再强最终也要落到工具调用上。我们设计了一个简单的工具注册中心每个工具都是一个标准 schema{ name: query_service_metric, description: 查询指定服务在某个时间段的QPS/RT/错误率, parameters: { type: object, properties: { service: {type: string}, start: {type: string}, end: {type: string} }, required: [service] }, required_permission: monitor:read, operation_type: read }每个工具定义里必须包含operation_type和required_permission。这是 Agent 安全的重要防线。我们把所有工具按“读操作”和“写操作”分开管理读操作可以自动执行写操作必须走审批。权限控制不是只在 Agent 层做而是在工具调用层做。工具执行前会校验当前用户是否具备该工具的调用权限以及当前群是否属于允许使用该工具的范围内。比如监控查询默认所有成员可用而变更发布只允许列表中指定的发布人发起其他人即使提出“帮我发布一下”Agent 也会在生成计划后明确告知需要申请权限。3.3 多 Agent 协作场景拆解、分派、汇总当问题变成“订单服务最近一周出错率偏高帮我分析原因并出一份报告”时单个 Agent 就不够了。它既需要拉监控指标又需要查日志可能还需要查知识库里有没有对应的变更记录。我们用的是 Supervisor Agent 拆解 子 Agent 并行执行模式。具体流程是Supervisor 收到任务后结合路由结果把任务拆成三个子任务指标分析 Agent 拉取时间序列、日志分析 Agent 检索异常关键字、知识库 Agent 关联近期变更。子 Agent 各自独立执行结果以结构化 JSON 写入中间存储Supervisor 轮询到所有子任务结束后再基于这些结果生成最终结论。这里有个经验不要让子 Agent 把大量文本直接拼接到最终上下文中否则汇总 Agent 的上下文很快就会被撑爆。我们让子 Agent 把详细结果存到任务存储同时生成一份摘要返回给 SupervisorSupervisor 只依赖摘要做结论如果需要细节再按任务 ID 取原文。这样既保证信息不丢又控制了 Token 消耗。多 Agent 协作不是银弹。团队如果没有沉淀出足够的工具和接口拆成多 Agent 只会增加复杂度。我们的路线是先用单 Agent 把工具链路跑通当场景确实需要并行处理不同领域的数据时再逐步演进到多 Agent。4. 任务交付从异步审批到结果回执4.1 高危操作要留“人工刹车”Agent 能直接执行任务当然很爽但一位朋友提醒我真正危险的往往不是模型能力不够而是权限开得太快。我们上线没多久就遇到一次事故有人在群里问“能不能把订单表删了重来”Agent 已经把删除工具的调用参数拼好了还好当时所有写操作都强制走审批才没出大事。从那以后我对写操作和读操作做了明确区分读操作监控查询、日志检索、知识问答Agent 自动执行结果直接反馈。写操作发布、配置变更、数据订正Agent 可以生成执行计划但必须由发起人在钉钉审批卡片里确认后才执行。审批卡片里会展示操作的完整影响范围、预计耗时和回滚方案。比如发布 Agent 生成的审批卡长这样标题是“生产环境订单服务 v1.2.3 发布计划”内容包含变更文件、涉及实例、数据库变更、回滚命令。用户确认后后台才真正触发发布流程。这个“人工刹车”不是限制 Agent 的能力而是给 Agent 一个安全边界。模型会犯错工具调用也会出现预料之外的结果留一道人工闸门能极大降低事故影响。4.2 任务队列与幂等保障任务执行不是同步的尤其写操作可能耗时几分钟甚至更久。我们引入了一张任务表来管理任务状态核心字段包括字段说明task_id任务唯一IDevent_id触发任务的钉钉消息IDscene任务场景如 monitor_query、ops_executestatuspending / running / waiting_approval / succeeded / failed / canceledinput_data路由后生成的标准化参数output_data执行结果error_msg失败原因created_by发起人trace_id全链路追踪IDevent_id是幂等关键。钉钉推送和内部重试都有可能让同一条消息进入系统多次如果不做幂等一个“重启服务”的请求可能会被执行两遍。我们在消费端检查event_id是否已经存在存在就直接返回上一次的执行结果而不是重新执行。队列本身用 Redis Stream 实现。消费端做了退避重试任务失败后不无限重试默认三次重试后进入 failed 状态并把错误原因回传给群。对于长时间运行的任务我们通过定时轮询更新状态用户随时在群里发“任务执行到哪了”Agent 会读取任务表的最新状态并回复。4.3 结果回群如何把交付物说清楚任务执行完最后一步是把结果以人类可读的形式发回钉群。这个环节看似简单实际上很影响用户体感。早期我们直接让 Agent 用自然语言输出一段很长的结论用户在群里根本看不清关键数据。后来我们定了一套固定反馈模板先说任务摘要再说关键结论最后附上详细数据或链接。比如监控查询回复是这样的任务完成编号 T2025001查询时间14:30-15:30订单服务 QPS 峰值 3200均值 2100错误率 0.02%与昨日同期相比 QPS 下降 15%错误率无明显变化详细趋势图内部监控链接如果 Agent 生成了报告文件比如异常分析 Excel 或日志包我们会先上传到内部对象存储再返回短链接。钉钉的 Markdown 对复杂表格兼容性一般所以我的经验是长文本尽量分点重要指标用加粗能用链接的不要粘贴大段原文。5. 基础设施可观测性与测试没有这两样就不叫基础设施5.1 Agent 测试流程不是只测 PromptAgent 系统的测试和传统后端测试差别很大。传统接口只要输入输出稳定就行Agent 的输出却带有随机性。所以必须把测试分成多层来建。第一层是单元测试覆盖路由规则、工具函数、参数解析。比如“查日志”这类关键词路由输入不同变体断言路由结果是否准确。第二层是回归测试每次修改提示词或新增工具后用一组历史问答记录跑全量回归。我们会把过去三个月里比较典型的群聊提问收集成黄金数据集包含问题、期望路由结果、期望工具调用参数、期望最终回答。跑回归时对比输出是否和期望一致不一致就人工判断是改进还是回退。第三层是模拟群聊测试。本地启动一个假的钉钉事件源按照用户真实对话顺序推入一组消息断言整条链路的最终状态。比如输入“查一下订单服务”“QPS”“对比昨天”最终断言任务表里是否生成了正确的查询参数。上线早期我们还在测试群跑了一段时间的影子模式真实群消息会复制一份到测试环境但不影响真实用户。等观察了几天确认路由和工具调用稳定后才开放到正式群。5.2 全链路日志与审计Agent 系统最怕“黑盒”。用户问了一句系统内部到底走了哪些步骤、调了哪些工具、为什么给出这个答案如果没有任何日志出了问题根本没法排查。我给每条提问生成一个trace_id从消息接入、路由分类、工具调用、任务执行到最终回群全程透传。所有日志统一 JSON 格式输出包含操作者、群 ID、消息内容、路由结果、工具名称、调用耗时、Token 消耗、费用和最终状态。这样不仅能排查问题还能做成本分析。更重要的是审计。Agent 如果执行了写操作必须能追溯到是谁发起的、哪条消息触发的、审批人是谁、执行结果是什么。我们把审计记录单独存了一套表保留时间更长且不允许普通开发者直接修改。虽然内部用起来多了些约束但这套约束保证 Agent 基建不会演变成失控的“自动操作平台”。5.3 线上巡检与防劣化Agent 系统上线后不是一劳永逸。模型会更新工具接口会变用户的提问方式也在变所以需要每天做线上巡检。我设置了几个核心指标消息处理成功率成功完成任务的消息数 / 总消息数。平均响应时长从消息进入到最终回群的时间。人工介入率需要转人工处理的消息比例。工具调用失败率按工具维度统计。Token 成本按场景和模型维度统计。只要成功率连续几个时段低于阈值或者某个工具调用失败率明显上升系统就会通过告警通知到维护人。我们每周还会复盘一次失败 case把典型错误加入到测试集里让系统持续变好。6. 上线后的真实效果与持续迭代6.1 数据对比与收益上线三个月后我整理过一组内部数据指标上线前上线后周均监控/日志提问量约 240 条约 280 条自动解决率0%约 76%平均响应时长5-10 分钟30 秒以内写操作人工审批无100% 覆盖重复提问回复成本高大幅下降最明显的变化不是“少了一个值班的人”而是重复性问题不再打断研发节奏。以前群里的一个 QPS 查询至少要经历“看到消息、打开监控、截图、粘贴”四个动作现在十秒钟内就能得到结构化回复。钉群本身变成了团队知识流转的入口而不是一个消息黑洞。6.2 踩坑清单这套系统能跑通离不开下面这些坑。我挑几个印象最深的分享钉钉消息里的 信息格式不统一。有的客户端带空格有的不带直接解析很容易把发消息的人判断错。后来统一用钉钉事件里的 sender_id不再依赖文本解析。模型返回的 JSON 偶尔不合法。指定 JSON 输出格式后依然可能出现多余的逗号、截断内容。我们在工具调用层加了 JSON 二次解析和修复实在修不了就让 Agent 重新生成一次而不是直接失败。不设 token 上限钱会失控。最开始没有上下文裁剪一个复杂任务动辄消耗几十万 token。后来加入摘要压缩和上下文裁剪成本下降了约 40%。写操作权限一定要晚开不要早开。哪怕是团队内部只要权限能开就有人会用错误的方式调用。先跑通读操作再谨慎开放写操作。多 Agent 拼接长文本会导致汇总 Agent 上下文爆炸。子 Agent 的结果尽量落到存储只回摘要需要时再取详情。钉钉对单群消息发送频率有限制。Agent 一次性输出多个结果时要自己排队发送不能并发群发否则消息会被丢掉。6.3 下一步演进思路接下来我在关注两个方向一是把工具和能力描述标准化参考 A2A 协议里 Agent Card 的思路让不同团队维护的 Agent 可以互相发现和调用而不是每个人都自建一套工具注册中心。二是模型路由分级。目前所有场景默认走同一个大模型成本还是有优化空间。简单问题完全可以用小模型处理只有复杂拆解和多步推理才需要切到更大规模的模型。这个路由策略我们已经开始在部分场景灰度验证。钉群到任务交付这条链路本质上是在做一件事把人和系统之间的高频、低价值交互交给 Agent 基础设施去消化让团队把精力放在真正的创造性工作上。最后分享一点个人体会做这类基础设施最大的风险不是技术难度而是范围膨胀。如果一开始就想把所有场景都自动化大概率会陷入“什么都能做、什么都做不深”的泥潭。我建议从最高频、最读操作的场景切入比如监控查询和日志检索让用户先感受到“群里的机器人真的能干活”再逐步扩展发布、变更这类写操作。每扩展一个能力都意味着要多一层测试、审计和安全设计。慢一点反而更快。