Agent框架实战(下):记忆、工具调用与多Agent协作的稳定落地
上篇把Agent框架的主干聊完了任务拆解、模型调用封装还有那个最朴素的“想-做-察”循环。这篇“Agent框架下”要补的是主干之外真正决定生死的部分——记忆怎么管、上下文怎么不爆、工具调用怎么才安全、多Agent怎么协作、上线之后怎么看效果。我接下来写的每一节都来自几个模拟项目里的真实实践不保证银弹但方案都是踩过坑之后稳定下来的。如果你刚看完上篇正把单循环往真实业务场景里扩展这篇文章会对口如果是纯新手建议先把上篇里那个最小循环亲手实现一遍再往下读。框架理论的终极检验永远是它能不能在几十轮对话、十几个工具、若干个角色之间稳定不翻车。1. 从单轮到有状态任务运行时该怎么管记忆与上下文上篇里我刻意把Agent描述成了一个无状态循环每次输入都是独立到达模型决定动作、执行、再看结果。这个抽象在教学阶段是好的因为简单。可现实中几乎没有任务是无状态的。用户会说“刚才那个单子”“像上次那样”“括号里那几家都不要”这些指代全部依赖历史信息。所以框架里必须有一层记忆管理而不是把所有东西都塞进同一个上下文数组里。1.1 记忆分层的三种存储与各自边界我见过最粗暴的做法是把所有历史对话、工具结果、用户偏好全部塞进一个 JSON 数组每轮无脑拼接。前5轮还行第10轮就开始出现两个问题Token 成本飙升模型开始“忘事”——不是真的忘是窗口被无关信息挤爆了。我的做法是把记忆拆成三层各管各的记忆层存放内容更新时机读取方式典型容量短期缓冲最近N轮原始对话每轮追加每轮全量传入2k-8k token工作记忆当前目标、不可变约束、待办清单、已确认事实每轮结束前重写每轮全量传入数百到1k token长期存储用户偏好、历史结论、领域知识异步落库或定期摘要按需检索可无限增长为什么要分三层而不是只留一个短期缓冲因为三层解决的是三个不同的问题短期缓冲保证上下文连贯工作记忆保证目标不漂移长期存储保证跨会话能力。三层之间还要有明确的边界短期缓冲可以丢工作记忆必须精炼长期存储宁可少存也不要存一堆检索不回来的噪声。你可以把它想象成三个办公用品短期缓冲是聊天窗口里没关掉的即时消息工作记忆是手边便利贴长期存储是抽屉里的档案要用的时候才翻。便利贴和档案最大的区别是更新频率和读取成本。所以我的框架里工作记忆每轮都会由模型重写一次做三件事更新当前目标状态、提炼关键约束、标记已完成和未完成的事。这个重写动作不能省否则Agent会在第10轮突然把用户第2轮强调的优先级忘掉。1.2 上下文窗口挤压token预算与压缩策略框架跑起来之后第一个撞墙的地方几乎都是上下文窗口。对话历史每轮追加工具结果动不动就几百上千 token模型一次输入只能装那么多。像下面这种预算分配可以当起点系统提示 10% 工作记忆 15% 对话历史 50% 工具结果 20% 输出预留 5%比例按任务调整但分配顺序是固定的先保系统和不可变约束再保工作记忆最后才轮到对话历史和工具结果。一旦对话历史超过预算我会按代价从小到大做三件事工具结果不再全量保留只保留结构化摘要和关键字段。把早期轮次折叠成摘要塞进工作记忆。如果仍然超限让Agent主动向用户确认“我们先把范围收窄”而不是硬撑到爆窗。这个顺序背后的逻辑是工具结果的信息量密度最低先牺牲它对话记录里有线索价值折叠而不是删除用户目标和约束永远不能动。压缩策略还有一条很容易忽略的原则不要把不可变约束丢进摘要。比如用户说“只处理近30天的数据”这种约束一旦被压缩成“用户要求近期数据”模型就可能理解成“最近一个季度”。我的做法是把这类约束单独抽到工作记忆里作为独立字段存在。摘要只负责描述过程约束负责保留红线。这条在后面复盘生产事故时还会再遇到一次。压缩的骨架大致长这样def build_context(system, memory, recent_history, max_tokens): # 先把不可压缩的部分预留出来 reserved len(system) len(memory[constraints]) 512 budget max_tokens - reserved # 从最老的轮次开始折叠而不是直接删 history recent_history.copy() while len(history) 2 and token_count(history) budget: oldest history.pop(0) memory[summary] summarize_into(oldest, memory[summary]) return render(system, memory, history)这里的核心思想是“先折叠、后截断、约束永不动”。只要约束还在Agent就算忘了过程细节也不会跑偏到红线之外。2. 工具调用协议让Agent安全地“动手干活”框架第二件大事是工具调用。没有工具Agent只是会措辞的聊天机器人有了工具它才从一个“说话者”变成一个“行动者”这时候风险也随之而来。2.1 工具注册、参数校验与返回协议在Agent框架里模型不会直接执行你的 Python 函数它只是输出一个调用意图。框架负责把意图翻译成真实调用。这条边界要划清楚执行权永远在框架手里不在模型手里。我给工具层设计的基类很简单但每一行都有原因class BaseTool: name: str description: str parameters: dict # JSON Schema 形式 def validate(self, args: dict) - list[str]: # 返回错误列表空列表表示通过 # 模型可能生成 schema 里没有的字段也可能把枚举值写错 ... def run(self, args: dict) - dict: # 必须返回结构化结果而不是自由文本 return { ok: True, data: {...}, summary: 一句话给模型看的结论 }这个基类有四个要点description要站在模型的使用视角写说清楚“什么场景下用”而不是“内部怎么实现”。parameters用 JSON Schema 描述必须有required、enum、examples。validate不是可选项模型经常生成缺字段、类型错误的参数。run的返回必须结构化为ok / data / summary三个字段不能让模型去解析自由文本。很多人在工具描述上偷懒写“查询订单状态”模型就经常不知道该传order_id还是customer_id。我把描述改成“当用户询问订单配送、退款、签收状态时使用参数 order_id 为订单号或用户提供的外部单号”调用准确率立刻上了一个台阶。写工具描述是最典型的低投入高回报动作。返回协议里summary是给模型看的文本data是给下游程序用的结构化数据。两者分开的原因很简单模型读 summary 比读一整块 data 便宜程序读 data 比解析文本可靠。返回内容超过阈值时summary 必须由工具内部截断不能让框架拿原始回包去填上下文。2.2 权限边界与失败重试最容易翻车的地方工具一旦能对真实系统生效权限就是框架安全的地基。我把工具按风险分成三档风险级别工具示例默认处理只读查询订单、检索文档可直接调用并记录普通写保存草稿、发送通知可调用调用后写入 trace 并留痕高影响操作删除、批量修改、支付默认不注册需要人工确认再放行其中“默认不注册”是很多人做不到、但最重要的一条。高影响操作工具就不应该出现在模型的工具列表里。你靠 prompt 跟模型说“删除需谨慎”是拦不住的因为意图判断是概率性的。正确做法是在框架层就不给它选项删除按钮留给用户点模型只能生成一个“待删除清单”交给前端确认。失败重试同样要有协议。我见过太多框架在工具调用失败后原样重放最后既烧 token 又拖垮下游。下面这个重试骨架比较稳attempt 0 max_attempts 2 while attempt max_attempts: result tool.run(args) if result[ok]: break # 参数错误不重试系统超时才重试 if not result.get(retryable): break # 让模型读错误信息重新生成参数而不是重放旧参数 args model.suggest_correct_args(result[error]) attempt 1 time.sleep(1 attempt)失败以后不要无脑重跑。模型需要先读错误信息判断是参数错还是系统错。参数错就重新生成参数系统错就等一等再试。两类错误的处理方式完全不同。没有上限的重试循环不仅烧钱还可能把下游接口拖到雪崩这个我后面会专门复盘一次。3. 多Agent协作的编排模式不是所有任务都要一个Agent单干很多人一学框架就想上多Agent我劝你先冷静。多Agent在三个场景里确实有价值任务边界清晰、不同子任务需要不同模型或提示词、天然存在多个专业角色。除此之外单Agent反而更稳、更省、更好排查。多Agent不是身份象征是成本来源。3.1 三种协作拓扑链式、路由、共享黑板我在多个模拟项目里用得最多的有三种拓扑分别适合不同的题拓扑一句话描述适合场景主要成本链式A的输出作为B的输入顺序固定流水线固定中间结果必须严格定义路由编排Agent判断类型分发给子Agent任务类型多样但边界清楚路由判断会出错需要兜底共享黑板多个Agent读写同一份状态复杂目标需要多方协作写冲突与上下文污染链式最好理解。比如工单处理先有一个分类Agent输出类别字段再有一个要素提取Agent读取该字段生成结构化清单最后有一个回复Agent根据清单写答复。每一步输出都有 schema下一步只认 schema不读上一步的全部思考过程。这样每一步都可以单独替换、单独测试。路由模式适合客服这类场景总控Agent判断用户问题是退款、物流还是产品咨询然后分发给对应子Agent。路由节点必须返回一个置信度低于阈值就转人工不要硬分。共享黑板最灵活也最危险。比如一个规划Agent负责拆计划一个执行Agent负责写实现一个审查Agent负责挑问题三个Agent共用一份“项目状态”文档。它的优势是信息流动自由劣势是并发写冲突和上下文污染几乎无法完全避免。我建议只在单Agent确实装不下时再选它。3.2 避免“Agent套娃”会话隔离与消息路由的取舍一进入多Agent最常见的错误就是把所有上下文到处塞。有人把主对话全量复制给每个子Agent子Agent之间互相干扰最后回答里甚至混进了别的角色的内部思考。我的原则是每个子Agent是一个独立 session只看到与自己相关的上下文。框架不传全量对话而是按任务类型裁剪出parent_context。一份协作任务配置大概长这样task { task_id: T-003, parent_context: { intent: refund, constraints: [order_id..., user_verifiedTrue], work_memory: ... }, input: 用户申请退款..., output_schema: {status: enum, amount: number}, budget_tokens: 8000, max_steps: 4, callback: blackboard://T-003/result }parent_context只放与本任务相关的字段不是把主对话复制一份output_schema限制子Agent返回结构budget_tokens控制成本max_steps防止子Agent无限循环。会话隔离直接决定了排查成本。如果没有隔离出问题时你根本不知道是哪段上下文污染了哪个Agent。协作层级一旦超过三层基本不可维护所以我的硬性规定是出现 A 让 B、B 让 A、A 又让 B 这种循环调用框架必须立即熔断同一个 task_id 出现次数超过 N 就直接终止。4. 可观测性与评估跑起来之后怎么判断Agent是好是坏框架写出来只是开始。我见过太多项目死在一个问题上Agent看起来在认真工作但你不知道它干得好不好甚至不知道它干了什么。可观测性不是加分项是上线前提。4.1 给每次任务埋点trace、token、工具调用链我给每次任务设计了一份结构化日志核心字段大概长这样{ trace_id: t_8f3a..., task: query_refund_status, steps: [ {node: planner, model: model-a, tokens: 1234, latency_ms: 890}, {node: tool_query_order, tool: query_order, args: {order_id: ...}, ok: true, latency_ms: 120} ], total_cost: 0.021, result: success }trace_id要从入口生成穿过所有子Agent、工具调用和模型调用。没有它你在日志里连一次任务都拼不起来。Token 和成本必须记录。Agent 循环会在你眼皮底下翻倍消耗如果不埋点月底账单才会教做人。工具调用链更要记清楚调了哪个工具、参数是什么、成功没有、耗时多少。这四件事是排查一切问题的基础。日志别什么都记。完整 prompt 和 response 全量存几天就能把存储打爆。我的做法是默认只打印结构化的摘要事件完整内容按 trace_id 存到离线存储需要排查时再取出来。用 print 打调试日志这种习惯在Agent系统里会直接变成灾难。4.2 离线评测集与线上回归不靠肉眼判断效果改一个提示词或工具描述当时感觉变好了过两天又改回来这种反复太常见。原因是你在拿肉眼评估样本量又小。正确做法是建一个离线评测集。我的流程分三步从真实日志里挑 30-50 条代表性任务覆盖典型场景和边界场景。每条标注输入、期望的工具调用序列、期望参数约束、期望答案要点。每次改框架跑一遍通过率跌破阈值就不合入。评估维度可以参考这张表维度评估方式工具选择是否在正确时机调用了正确工具参数合法是否通过 validate且关键字段正确约束保持是否违反不可变约束结果质量答案是否覆盖要点、没有多余动作成本平均 token 是否超预算可以用模型当裁判但裁判模型不要和执行模型是同一个模型这等于让选手给自己打分。敏感任务最终还是要人审。我一般先让裁判模型打分把低分候选和边界 case 筛出来给人复核而不是全量交给机器。线上回归也很重要。灰度期间把同一条 case 同时喂给新老版本对比输出差异差异比例超过阈值就暂停灰度回去查是哪个改动引发的漂移。有了这套东西改框架才不会变成玄学。5. 踩坑实录三起生产事故复盘前面讲的是设计现在讲真实翻车。这三起事故我都完整跟过每次的根子都不是模型太笨而是框架边界没扎紧。复盘出来是为了让后来者少踩一遍。5.1 无限重试拖垮下游系统有一次做客服工单模拟项目Agent调用一个查询订单状态的工具那个工具背后是一个偶发超时的第三方接口。上线第二周订单服务的监控显示压力翻倍。翻日志才发现工具一旦超时框架层的循环没有上限模型每轮都认为“再试一次也许就好了”用同一组参数连续重试了7次。下游系统本来几秒钟就能恢复被持续重复请求压到雪崩。修复方案就是前面说的那三条max_attempts2、retryable标志区分参数错误和系统错误、重试间隔按 1 秒 3 秒退避。隐藏收益是 token 成本也降了排查日志反而更干净了。5.2 上下文漂移丢掉了用户约束某个报表生成场景用户在第1轮明确说“只要近30天的数据”。前两轮一切正常。到第5轮对话历史超过预算框架把早期轮次折叠成了摘要摘要里写的是“用户要求近期数据”。模型把“近期”理解成“最近一个季度”查询条件被悄悄改写生成了一份范围错误的报表。人审时才发现但业务时间已经浪费了。问题不全在摘要本身而在我把用户约束也丢进了可折叠的历史。约束属于工作记忆不属于对话历史。修复分两步每轮开始时把约束列表显式塞进系统提示工具调用前由框架校验参数是否与约束冲突。校验可以很简单如果约束里有“30天”而工具参数里出现“90天”直接拦截并让Agent重新生成。5.3 把删除工具暴露给了模型还有一个内部文件管理模拟项目一开始为了演示把删除文档的工具也注册进了工具列表。有一天用户说“把这个临时文件处理掉”模型没有追问是删除还是归档直接调用了删除工具。幸好是演示环境文件能恢复。但这个教训很值钱当执行动作不可逆时你不能让模型自己判断。修复方式就是我反复强调的那条高影响操作不进入模型的工具列表。模型只生成“操作意图加目标清单”由人工确认后执行确认动作必须在框架层完成不靠 prompt 里的承诺。宁可让Agent多问一句也不能让它手滑执行。这三起事故共同指向一件事Agent框架的下半场真正决定成败的不是模型多聪明而是边界定义得多清楚。框架给Agent能力边界给Agent安全。我现在做任何Agent项目会先写20条最容易翻车的评测用例再写核心代码。框架是配角边界才是主角。先把安全边界、记忆边界、成本边界扎紧后面的迭代才有意义。