多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

多智能体Agent生产落地:规划、工具、记忆与评测的工程实践

多智能体Agent生产落地:规划、工具、记忆与评测的工程实践 做了一年多的Agent落地项目我最常被问的一句话是你那个agency-agents到底解决了什么问题大家直觉上觉得Agent不就是让大模型自己规划、自己调用工具吗为什么还需要专门做一套东西每次被问到我都得从头解释一遍“多智能体落地没有想象中那么性感”讲着讲着就变成了半个小时的案例分析。这里先解释一下这个名字。agency-agents里的agency不是“代理机构”而是指智能体在真实业务中拥有的自主执行权——能自己决定下一步做什么、能发现错误并修正、能对不确定的事情主动说“我不知道”。我参与的这个内部项目代号就叫agency-agents目标不是做一个玩具Demo而是跑在真实业务线上、能被人力部门放心使用的一套Agent基础框架。这篇文章就把这一年多里踩过的坑、沉淀下来的架构思路和评测方法完整写出来适合正在设计多智能体系统的后端开发、AI应用架构师以及那些想从“跑通Demo”往“生产可用”迈一步的团队参考。1. 从“能聊”到“能干”agent-agents要解决的老大难1.1 为什么不自接用现成编排框架最早规划项目时团队里有人提过直接拿现成的开源编排框架包一圈不就行了何必自己造轮子。我当时的想法也一样先接开源框架跑了一个星期Demo确实流畅大模型能调用两个内置工具、能简单回答业务问题。但一旦往真实业务上靠问题立刻暴露。第一个问题编排框架默认接管了完整对话循环但它把“工具调用失败后怎么办”处理得非常模糊。真实接口经常超时、返回参数非法、权限校验不过模型看到一堆错误文本后开始瞎猜然后重复调用同一个失败的工具形成死循环。第二个问题企业内部系统的身份认证、审计、操作留痕这些诉求开源编排框架基本不考虑而我们面对的业务场景要求每一步操作都能追溯到具体人和具体时间。第三个问题框架版本迭代太快社区文档和线上真实版本经常对不上排查一个问题要翻好几个渠道。与其追着框架跑不如基于一个稳定的最小核心自己封装。所以我们最后只写了一个极薄的运行壳核心代码量并不大但它把Agent的行为边界、工具执行协议、错误处理策略全部固定下来。我的核心体会是在Agent这条路上框架越薄越可控通用性和业务稳定性往往是两回事。1.2 Agent与传统对话程序的分界线状态、工具、记忆很多读者对Agent的认知还停留在“一个更聪明的聊天机器人”这个误解会直接导致架构设计跑偏。我在设计agency-agents时第一件事就是把传统对话程序和Agent在工程上的差异理清楚。维度传统对话程序Agent本框架的目标形态状态管理通常只有会话ID上下文是一次性拼接需要维护用户意图、任务进度、中间结果的多级状态工具调用基本不调用外部系统靠话术撑住需要主动选择工具、填参、执行、解读结果记忆只保留当前上下文窗口需要短期上下文与长期记忆分离记忆要写入、检索、刷新失败处理回复一句“请稍后再试”要鉴别失败原因、决定重试或换路径并把失败信息回传给模型可观测性记录聊天气泡即可每次规划、工具调用、参数变更都要有轨迹日志从这张表能看出来Agent真正复杂的地方不在“模型有多聪明”而在外围系统要替模型承担多少确定性工作。换句话说大模型负责“想”框架负责“做”两边要分工明确。1.3 最小闭环感知、决策、行动、反馈回填在agency-agents里我们给每个Agent任务定义了一个最小闭环所有场景都是这个闭环的变体。拿一个真实的内部场景举例一位运营人员让Agent查询某活动过去一周的转化数据如果数据异常再找出可能原因。这个看似简单的需求闭环是这样的。第一步感知Agent从对话中提取关键参数比如活动ID、时间范围、指标定义如果参数缺失就主动追问而不是猜一个默认值。第二步决策模型将任务拆成两步先查询转化数据再根据结果判断是否需要进一步归因。第三步行动调用数据平台的查询工具带着参数发起请求。第四步反馈回填工具返回一个结构化的结果可能是数据本身也可能是错误码模型根据这些信息继续推进或修正路径。这个闭环看起来平淡无奇但它的价值在于每一步之间都有明确的接口协议。模型不能跳过感知直接去调用工具也不能在工具返回错误后假装没看见。实现时我会用状态机约束闭环流转状态机外的行为一律拦截。这是整个框架的骨架。2. 规划层把模糊指令变成可执行脚本2.1 先拆后做还是边做边看规划是Agent最核心的能力也是最难在工程上约束的能力。我在这部分的取舍是不追求让模型随时随地自由规划而是在不同任务类型里限定它使用两种规划模式之一。第一种是计划前置模式Plan-then-Act。适合流程固定、步骤清晰的业务场景比如退款处理、工单分派、定期报表生成。这种模式下模型收到需求后先输出一个完整的计划列表每一条都对应一个工具调用或一个原子动作全部经过框架校验通过后才逐步执行。第二种是动态决策模式ReAct。适合探索性任务比如故障排查、资料搜集模型每执行一步都要观察结果再决定下一步。这种模式灵活但可预测性差需要更强的边界约束。我建了个简单的选择条件如果任务目标可以在一个请求里描述清楚优先用计划前置如果任务本身就是开放的、结果不可预知选用动态决策。千万不要把所有任务都扔给动态决策模式否则生产环境会变成一场不可控的即兴表演线上出了问题你根本没法解释它是怎么走到那一步的。2.2 用JSON Schema约束模型输出计划让模型“自由发挥”输出计划是最容易翻车的做法。一开始我们的模型输出五花八门有时候用自然语言、有时候用列表、有时候把计划藏在思考过程里让下游解析器非常痛苦。后来统一改用结构化输出约束效果立刻不一样。让模型“自由发挥”输出计划是最容易翻车的做法。一开始我们的模型输出五花八门有时候用自然语言、有时候用列表、有时候把计划藏在思考过程里让下游解析器非常痛苦。后来统一改用结构化输出约束效果立刻不一样。我们要求规划结果必须是严格的JSON并且用JSON Schema做校验。我不放全部Schema只保留最关键的骨架结构大概长这样。{ plan_id: uuid, steps: [ { step_id: 1, action: call_tool, tool_name: query_activity_metrics, arguments: { activity_id: ACT20240601, metrics: [conversion_rate], time_range: last_7_days }, success_criteria: 返回数据中包含conversion_rate字段 } ], estimated_steps: 2, requires_confirmation: false }这套约束有两个关键点。一是requires_confirmation字段高风险动作比如退款、删除、修改权限模型必须显式标记然后由框架在执锁前向用户确认。二是success_criteria模型写清楚“执行到什么程度就算成功”后面做轨迹评测时就靠这个字段判读模型是否达成了目标。2.3 计划执行前的两道参数防线光有结构化计划还不够真正的拦截发生在执行前。我们框架里设置了两道防线帮我挡掉了大量低级错误。第一道防线是计划完整性校验。检查每一个step的tool_name是否在已注册工具列表里、arguments是否满足必填、step之间的依赖关系是否指向了不存在的上一个结果。第二道防线是工具参数级校验。这一步更多是检查参数格式和取值范围比如时间范围不能跨年超过90天、状态码必须是枚举值之一。举一个我印象特别深的例子。某次客服Agent要做订单状态变更模型生成的计划里写了“查询订单号并更新状态”但查询结果的变量名写错了导致更新工具拿不到订单号。如果只有第一道防线这种错误根本发现不了因为参数格式都合法。但第二道防线里我们要求更新类工具必须传入订单号且必须是查询工具返回的原始字段值不能是模型编造的字符串。这一次拦截直接避免了一次线上脏数据写入。很多团队觉得模型输出计划挺准的没必要层层校验但我在真实环境里的经验是模型在简单用例上确实准一旦上下文长了、中间结果多了变量引用错误就会频繁出现校验层必不可少。3. 工具层Agent的“手脚”是怎么长出来的3.1 工具注册本质给模型写清楚说明书让Agent调用工具不是直接把函数列表甩给模型就行。工具层设计得好不好直接决定模型调用成功率和业务安全性。在agency-agents里每个工具都要有一份结构化说明书包含名称、描述、参数定义、触发场景和禁止事项。我列一个我们内部工具说明书的精简版当作参考。{ type: function, function: { name: query_activity_metrics, description: 查询活动维度的转化率、参与率、分享率等指标。仅当用户明确提到某个活动ID且指标属于活动导流场景时使用。数据延迟约10分钟。, parameters: { type: object, properties: { activity_id: { type: string, description: 活动唯一ID格式为ACT开头后面跟8位数字 }, metrics: { type: array, items: {type: string}, enum: [conversion_rate, participation_rate, share_rate] }, time_range: { type: string, enum: [last_24h, last_7_days, last_30_days] } }, required: [activity_id, metrics, time_range] } } }这个说明书的写法有讲究。描述部分不能只写“查询活动指标”这种废话要把触发条件、边界、数据延迟都写清楚。比如“数据延迟约10分钟”这句话就是为了防止用户问实时数据时模型误用这个工具并给出误导性结论。枚举值也严格限制宁可让模型多问一次也不要放任它自由填时间范围然后拿到的结果驴唇不对马嘴。3.2 执行结果回传错误码比报错文本更有用工具调用返回给模型的内容决定了模型能不能做出正确的下一步判断。很多项目直接把底层接口的错误信息原样拷给模型这是个大坑。真实业务的异常信息里经常夹杂着IP地址、内部函数名、数据库错误细节模型一旦看到这些容易被带偏还会在回复里复述内部敏感信息。所以我们制定了结果回传的规范。成功时返回一个包含摘要字段的结果对象把最核心的数据值放进去太长的明细只在用户要求时才展示。失败时则返回结构化错误码而不是原始报错文本。大致设计如下。错误码段含义模型应采取的默认动作ERR_PARAM_INVALID传入参数不合法根据提示修改参数后重试一次ERR_AUTH_FAILED当前身份无权限停止执行并告知用户无权限ERR_TIMEOUT工具执行超时不自动重试建议稍后再试ERR_DATA_NOT_FOUND未查询到数据澄清查询条件必要时更换工具ERR_SYSTEM_BUSY下游系统过载停止执行转人工处理模型看到“ERR_TIMEOUT”会知道不要立刻重试看到“ERR_AUTH_FAILED”也不会反复撞墙。这套协议跑了一个月后工具调用的无效重试次数下降了大约四成效果立竿见影。底层细节依然会完整记录在日志里但那是给人看的不是给模型看的。3.3 “查不到订单”实际是我传错时间格式——一次完整排查说一次让我印象深刻的排障过程。某天客服Agent开始集中报错用户问订单状态Agent总是回答“查不到订单信息”。表面上看是数据问题但打开轨迹日志一看问题根本不在数据。轨迹回放显示Agent调用查询订单工具时传入的时间格式是“2024-06-01 00:00:00”但工具期望的是“20240601”。两边的时区还差了一个小时。底层接口拿到不认识的格式后没有报参数错误而是当成无数据返回于是Agent就顺着这个结果告诉用户“查不到订单”。排查链路是这样的第一步把失败会话的轨迹日志导出来看模型每一步的输入输出第二步定位到查询工具这一步发现返回值是IRR_DATA_NOT_FOUND第三步查工具调用时的入参对比工具定义发现时间格式完全不符第四步翻底层接口文档确认工具层缺少一个参数清洗步骤。修复方案不是去改模型提示词而是在工具层加一个标准化参数转换器把所有外部传入的时间格式统一转换后再发给底层接口。从那以后我再也不觉得“Agent回错了”一定就是模型笨很多问题其实是工具层不够健壮。4. 记忆层上下文不等于记忆4.1 短期记忆与长期记忆必须分开管很多刚开始做Agent的人会把记忆等同于上下文窗口觉得只要把历史对话全部塞给模型它就能记住一切。这个想法在测试环境勉强能用到生产环境很快就会崩掉。一方面是token成本暴涨另一方面是上下文太长会导致模型注意力分散最近几轮对话的权重被早期旧信息冲淡反而更容易答非所问。在agency-agents里状态设计把记忆分成两层。短期记忆就是当前任务上下文包含用户本轮意图、已执行的步骤、收集到的关键结果任务结束后就可以压缩归档。长期记忆则独立存储包含用户偏好、业务实体关系、历史会话摘要、一些硬性的业务规则。每次新对话开始时框架会从长期记忆里检索出与当前问题相关的片段拼接到上下文中而不是把所有历史都倒进去。这样做的逻辑很简单模型只需要它此刻用得上的信息。用户上个月提过什么需求和当前查询可能相关但不需要把上个月的完整聊天记录原样搬出来给一个摘要就够了。4.2 记忆写入和检索的落地流程记忆层的落地我把它拆成了写入和检索两条链路。写入链路在每轮对话结束后异步执行先让模型生成一段不超过两百字的结构化摘要再从摘要里抽取关键实体比如用户ID、业务ID、时间节点、偏好标签最后同时写入摘要库和向量库。检索链路则在每次构建上下文前执行先用关键词把候选记忆捞出来再用向量相关性排序最后根据一个阈值判断哪些片段真的需要进入上下文。这里有一个容易被忽略的细节检索结果不是越多越好。我们内部的经验值是单轮对话最多带入三段记忆片段每段控制在三百字以内。低于相关性阈值的记忆宁可不用也不要硬塞。因为一段错误的记忆比没有记忆更可怕它会让模型编造一个“用户曾经说过”的假象。推理能力再强的模型只要喂进去的前提是错的结论也不可能对。4.3 记忆污染与覆盖刷新记忆层最大的风险不是“记不住”而是“记错了”。举一个我们真实遇到的情况。用户在一月份设置了一个收货地址三月份又改成另一个地址。Agent在四月的某次对话中从长期记忆里检索到了一月份的旧地址直接当作当前地址给了用户。这就是典型的记忆污染问题。解决思路分几层。第一层是写入时区分“事实型记忆”和“偏好型记忆”地址、手机号这类事实型记忆采用覆盖式写入同一个实体有新的合法值就直接替换旧值不允许两条同时存在。第二层是给每条记忆记录写入时间和更新时间检索时对时间敏感的信息加上新鲜度权重。第三层是定期做记忆合并与失效清理对话摘要超过一定数量就压缩合并超过有效期的记忆转入冷存储或直接删除。在设计这套机制之前我天真地以为向量数据库能解决一切记忆问题实际跑下来才发现业务规则比检索技术更重要。什么样的记忆能覆盖、什么样的记忆必须人工确认这些规则不写清楚Agent就会在一个不存在的旧信息上建起整座错误的推理大厦。5. 评测关没有评测机制Agent项目迟早失控5.1 分级用例集冒烟、回归、对抗做Agent项目最忌讳的就是“跑一次看看效果”因为大模型输出有随机性这次效果好不代表下次效果还好。我们建立了一套分级的用例集日常维护成本不算高但作用非常大。级别用例数量目的典型场景冒烟用例30条左右每次代码或提示词变更后快速验证框架是否还能跑通新用户咨询、简单订单查询、退出对话回归用例200条左右确保历史修复过的缺陷不复发上次修过的地址覆盖场景、权限校验场景对抗用例50条左右测试边界和极端输入空参数、超长文本、包含敏感词、故意诱导模型越权冒烟用例每天都要跑回归用例每逢变更就跑一遍对抗用例每周抽跑一次。跑完以后生成一个统一的成功率表格哪个用例挂了、挂在了哪一步、模型当时的决策轨迹是什么全都留档。这套东西的终极目的是为了让我们在做改动时敢说“没问题”而不是凭感觉拍拍胸脯。5.2 轨迹回放是定位根因的“笨办法”很多团队来找我交流时都会问“你们怎么调Prompt有什么秘诀”我的回答往往让他们失望大部分问题不是靠调Prompt解决的而是靠轨迹回放定位出来的。所谓轨迹回放就是把Agent一次完整运行中的所有输入输出记录下来然后像看一部摄影机一样一帧一帧看完它是怎么走到失败那一步的。每一轮运行我们至少记录这些字段任务ID、会话ID、用户ID模型输入的完整内容加了哪些记忆片段、System Prompt版本模型输出的规划结果JSON每一步工具的名称、入参、返回值、错误码每轮耗时的分析和token消耗量最终回复内容遇到失败用例第一件事不是改提示词而是打开轨迹逐条看规划是否合理、工具入参是否准确、失败后模型的处理是否恰当。八成以上问题的根因都会在这个过程中暴露。比如可能是某个工具描述有歧义也可能是某段记忆检索结果干扰了决策这些都是光看最终答案发现不了的。5.3 一次成功但必须回滚的Prompt改版说一个我自己踩过的真实决策案例。某次我优化了规划阶段的主提示词把一些示例写得“更聪明”了在冒烟用例上成功率从82%提升到91%非常亮眼。但那天晚上跑完回归用例数据却给我泼了一盆冷水涉及多步骤工具调用的用例成功率反而从75%掉到了68%。仔细看轨迹后发现提示词里那些“更聪明”的表达方式诱导模型在规划里加入了更多它想象中的判断逻辑。比如在一个应该直接查询数据的用例里模型额外添加了一步“先分析数据口径再查询”这步其实毫无必要反而制造了多一次失败的机会。被优化的单轮对话确实变好了但真正吃性能的多步调用场景被拖累了。最后我把这版提示词回滚了然后专门为单轮对话和多步工具调用场景写了两套不同的规划指令而不是试图用一套万能模板通吃所有场景。这个经历让我坚定了一个原则Agent项目的任何改动都必须回归用例集和数据说话不经过评测的“优化”都可能是在给另一个场景埋雷。6. 实测中的反直觉结论与个人体会6.1 限制比引导更管用刚开始做Agent时我和团队成员的直觉都是“提示词要写得更详细、引导模型想得更深”但实际跑下来的结论是Agent在生产环境里更需要的不是引导而是限制。什么时候必须停止、哪些工具不能碰、哪些参数绝对不能猜、哪些话不能对用户说这些在提示词里写得越硬性Agent反而越可靠。自由度这个东西在Demo里是亮点在生产里是风险源。后来我们每次新增能力时都会多问一句是不是已经给了模型足够的限制如果还没有就先不上线。这套逻辑大概也是为什么agency-agents能在内部逐渐被更多业务方接过去使用的原因——大家要的不是一个经常超常发挥但偶尔闯祸的实习生而是一个稳定交付、偶尔亮眼但从不越界的同事。6.2 重试要克制另一个人人都容易掉进去的坑叫“无脑重试”。模型发现工具调用失败时如果允许它自行重试最常见的结果是在同一个参数错误上反复撞墙。比如某个工具需要数字字符串模型传了带引号的字符串失败后它再传一次带引号的字符串依然失败。后来我们定了两条规矩。第一同一个工具在同一个任务里最多重试一次第二次失败立即转人工或结束。第二重试前必须改变至少一个参数不允许原封不动再调一次。这两条规矩看起来简单却让工具调用的平均失败重试次数大幅下降。让模型“再想想”不是靠无限重试而是靠带着修正方案重新尝试。6.3 给Agent留一条“不知道就直说”的路最后想分享一个容易被忽略的设计Agent必须被允许说“我不知道”。我们一开始把Agent当作万能助手尽力避免它示弱结果反而出现大量幻觉。模型编造订单号、编造用户历史行为这些一旦被业务方当真后果很严重。解决办法是在框架层面加了一个强制规则当模型无法从任何已调用工具中得到明确证据支撑时禁止给出肯定式回答必须输出“无法确认”类型的结果并推荐用户转人工客服。同时我们还在评测用例里专门加了对抗用例故意给模型一些它根本没有数据支撑的问题要求它正确拒答而不是硬编。这比在提示词里写一万遍“如果你不确定就说不知道”都有效因为这就是把“诚实”变成了一种可量化的行为指标。在真实业务里跑了一年多我最大的感受是Agent不会因为模型变强就自动变可靠它需要一套严肃的工程系统去承接模型的不确定性。agency-agents这个项目真正沉淀下来的不是某个算法或某个惊艳的Demo而是这些围绕规划、工具、记忆、评测建立的习惯和约束。如果让我给后来者一个具体的建议我会说不要沉迷于让模型想得更深先花时间让它每一步都走得更稳、更可观测。把轨迹日志和评测用例当成你最重要的资产来维护这个Agent项目才可能真正从实验里走出来。
返回列表