Agent相关的“八股文”在社区里流传很多但大部分版本都停留在概念层面什么是Tool Calling、什么是ReAct循环、什么是Plan-and-Execute背得出来不代表做得出来。这篇记录的是LLM应用开发里Agent智能体部分下事先把基础循环和提示词框架放一边重点讲六个真正影响Agent能不能上生产的话题记忆怎么落、工具调用有哪些暗坑、任务规划与反思怎么做、多智能体协作什么时候该上、Agent该怎么评估、以及生产环境里的最后几道保险。适合的读者是正在做LLM应用落地、被Agent的“不可控”折磨过或者面试前想把这块知识补完整的开发者。下面进入正题。1. 先说清楚Agent的“记忆”到底是存什么的很多文章把记忆说得特别玄其实只要抓住一个事实问题就简单了大模型接口本质上是无状态的每次调用传入的messages内容就是模型“临时拥有”的全部信息。所以记忆不是模型自带的能力而是一套工程读写策略。在我看过的Agent项目里至少六成的问题都出在“把上下文窗口当记忆用”这句话值得反复强调。1.1 上下文窗口能当记忆用吗上下文窗口的真正定位是“工作台”不是“档案柜”。工作台上只能放当前正在处理的东西档案柜才能长期保存资料。如果硬把整个历史都塞进工作台结果就是超过长度限制后被粗暴截断而截断往往丢掉的是最关键的早期用户信息。举一个具体例子。用户说“我昨天问过退货规则现在想按那个规则操作”如果昨天的对话已经在截断中被丢掉模型就只能含糊回答“建议您查看退货政策”整个任务直接失败。更麻烦的是这种情况从日志上几乎看不出来因为模型看起来是在“正常回答”。我的落地经验是把记忆拆成三层工作记忆当前这一轮用户请求、正在执行的任务上下文、刚生成的中间结果短期对话记忆最近几轮原始messages用于保持对话连贯长期记忆跨会话沉淀下来的用户画像、业务台账、历史偏好存在外部存储里。三层的读写频率完全不同。工作记忆每轮都重写短期记忆滚动保留最近N轮长期记忆只在特定时机写入。很多失败案例是因为没有区分这三层什么都往长期记忆里塞结果检索出来的全是过时信息反而干扰了当前决策。1.2 长期记忆的落地姿势向量检索不是唯一解一说长期记忆大部分人的第一反应是搞向量库、搞embedding。但向量检索只适合“相似语义召回”不适合精确事实查询。比如用户ID、订单号、账户状态、订阅到期日这些字段适合放结构化存储里做精确匹配知识库文档、历史工单记录、产品说明这类内容才适合用向量召回。我常用的记忆存储方案是混合式的不同数据类型走不同通道记忆类型存储方式检索方式典型内容用户固定画像结构化KV精确匹配用户ID、会员等级、常用地址事实型业务记录关系型/文档库精确查询订单状态、退款记录、工单信息知识型内容向量库相似度召回产品文档、规则说明、历史FAQ对话摘要向量库文本按ID读取上次会话的压缩总结这里有个容易踩的坑向量检索的相似度阈值不是越松越好。我见过不少项目把阈值设到0.5结果每次都能召回一堆语义沾边、实际无关的旧记录。模型拿到这些噪声后会把无关信息当成决策依据。我一般先设0.72左右再用小样本case验证根据召回结果微调不同embedding模型分布差异很大阈值必须实际跑一遍再定。写入时机同样不能大意。不要每轮对话都往长期记忆里写那会让记忆库迅速膨胀检索质量下降。我的习惯是在“事件完成”后写入用户确认了某个偏好、任务成功结束、用户明确纠正过某个信息。更重要的是写入前要过滤掉模型自己生成的推断只写“经过确认的事实”。1.3 记忆写入前的那一步摘要压缩与重要性筛选对话一旦超过十几轮即使只保留最近N轮token占用也很可观。所以需要一套压缩机制把早期的对话压成摘要摘要放进上下文的最前面最近的具体对话保留在后面。压缩通常用一个小模型单轮完成也可以用规则抽取关键信息。我最终拼出来的Memory Payload长这样每次请求前动态组装{ fixed_profile: { user_id: u_1024, vip_level: gold, shipping_address: ... }, summary: 用户上周咨询过退款规则客服已说明生鲜商品不支持7天无理由退换, recent_turns: [ ... ], retrieved_context: [ ... ] }fixed_profile是稳定不变的画像summary是过去会话的压缩总结recent_turns保留最近4到6轮原文retrieved_context是刚检索到的相关知识片段。组装顺序上越稳定的信息越靠前越临时的内容越靠后这样模型对信息的优先级判断会更准确。压缩必须异步做不能等到用户发起请求时才开始跑总结否则首token延迟会高得离谱。我的做法是任务结束后立即在后台生成摘要并写入记忆库下次请求直接读。还有一点容易被忽略过期的记忆要及时清理。举个例子用户上个月说“常去北京出差”这个月已经改成“调到上海”如果旧记忆还在库里而且相似度还很高检索出来反而会误导模型。定期给记忆打上时间戳、加失效策略比盲目堆更多记忆重要得多。2. 工具调用里那些文档不会写的坑工具调用是Agent能和真实世界交互的通道也是生产事故的高发区。模型层面已经把JSON Schema、参数调用这些事封装得比较成熟了真正决定成败的往往不是调用方式而是你对工具的定义方式和返回内容的组织方式。这部分网上讲得少但对线上稳定性的影响极大。2.1 Tool Schema设计参数描述决定第一次就成功的概率模型是“照着说明书使用工具”的。很多开发者写工具描述时只写“查询订单状态”参数类型写“string”然后就等着模型自己领悟。结果就是模型经常缺参数、传错格式或者干脆编一个不存在的订单号。我给工具写描述的一条原则是要在描述里告诉模型“从哪里拿参数、什么条件下不能用、业务上有什么限制”。比如查询订单的工具描述我建议写成这样{ name: query_order_status, description: 根据订单号查询订单当前状态。订单号在前文对话中出现过通常是数字串如果没有获得明确订单号不要猜测应请求用户提供。仅限查询当前登录用户自己的订单。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号通常是12位数字串来自用户消息或历史上下文 } }, required: [order_id] } }这段描述里最关键的两句话是“订单号从前文提取不要猜测”和“仅限当前登录用户自己的订单”。前者显著减少了模型现场编造订单号的概率后者规避了越权查询的风险。参数层面还有一些细节。能用枚举就别让模型自由发挥比如订单状态、支付方式、运输公司全部列枚举值存在互斥关系的参数要在描述里点名参数之间的依赖关系也要写清楚。多个工具之间的描述要有差异化如果两个工具都叫“查询信息”模型很容易选错。命名尽量带业务前缀描述里明确“本工具负责哪一类场景”。2.2 工具返回内容的“投喂”规范工具返回内容不是给用户看的而是给模型看的。很多项目直接把底层数据库的原始JSON丢回给模型十几KB的响应里包含大量无关字段模型读完更糊涂还会占用大量的上下文token。我的返回格式三要素是状态、关键数据、辅助信息。状态字段要显式给出SUCCESS、FAILED、NOT_FOUND这几种机器可读的结果关键数据只保留当前决策需要的字段辅助信息最多一两行。下面是我常用的一种包装逻辑用服务端代码先压缩再返回def build_tool_result(raw_record: dict) - str: if not raw_record: return state: NOT_FOUND\n说明未查询到该订单可能订单号有误或订单不属于当前用户。 compact ( state: SUCCESS\n f订单号: {raw_record[order_id]}\n f状态: {raw_record[status]}\n f物流进度: {raw_record[tracking] or 暂无}\n f预计送达: {raw_record[eta] or 未知} ) return compact工具调用方在服务端做压缩而不是让模型去“理解”一坨大JSON这一点非常重要。我见过一个真实案例工具返回6000 token的原始日志模型被里面互不相关的字段带偏连续三次都没提取出关键状态。把返回精简到300 token之内以后同样的问题再也没出现。这里还涉及一个token成本问题。工具返回内容占的token几乎每次都要付钱压缩返回内容不仅是“喂得好”还是在实实在在地省钱。同一个场景下工具输出平均从3000 token压到400 token之后调用成本能下降接近一半因为上下文更短、更聚焦后续几轮模型也更不容易跑偏。2.3 调用失败的兜底与重试链路工具调用失败是常态问题在于失败后模型会怎么反应。最常见的恶性循环是工具返回了一段面向人类的错误文案模型没看懂于是拿着同样的参数又调了一次同样工具失败后再调直到把预算烧穿或把用户等得不耐烦。错误信息必须“对模型说话”告诉它具体失败原因和下一步选项。比如“服务暂时不可用”这种话人类能理解模型却不知道该怎么改但如果你返回“state: FAILED\n原因: 上游查询服务超时\n建议: 等待后重试一次若再次失败告知用户稍后再试”模型就能做出合理决策。除了错误信息本身循环控制必须有硬约束。我的简化工具循环长这样max_turns 8 max_same_tool_failures 2 same_tool_fail_count {} for turn in range(max_turns): response call_llm(messages) if response.tool_calls: for tool_call in response.tool_calls: tool_name tool_call.name result execute_tool(tool_call) if result.is_failed: same_tool_fail_count[tool_name] same_tool_fail_count.get(tool_name, 0) 1 if same_tool_fail_count[tool_name] max_same_tool_failures: append_control_message(messages, 该工具连续失败请改用其他工具或提交人工处理) break else: same_tool_fail_count[tool_name] 0 else: break这套逻辑里全局轮次上限和“同一工具连续失败次数”是两条独立的生命线。很多线上事故都是因为没有后者模型才会在同一个坑里反复跌倒。上线前把这些上限设计好比在prompt里反复强调“不要重复调用”可靠得多。3. 任务规划与反思让Agent不是“指一步走一步”基础Agent循环解决的是“下一步做什么”但复杂任务需要的是“整体怎么做”。只靠逐轮决策的Agent经常在第三步就忘了最初目标。任务规划和反思机制就是为了让Agent从“走一步看一步”升级为“心里有张地图”。3.1 先规划后执行Plan-then-Execute的拆分粒度Plan-then-Execute的思路很简单第一轮让模型不急着动手而是先输出一个任务清单之后再逐步完成。难点在于清单的粒度。拆得太细光计划就要花大量token步骤间还可能相互冲突拆得太粗任务清单只是装饰模型照样走一步算一步。我的经验是按“可独立验证的结果”来切分。举个例子用户要求“帮我对比两款产品的套餐差异并生成建议”。我不会让Agent拆出“打开产品页面、解析页面内容、提取字段”这种过程性步骤而是拆成“收集A产品套餐信息”“收集B产品套餐信息”“生成差异对比表”“输出最终建议”这四步。每一步都能单独验证“有没有做完”而不是“做没做某个动作”。拆好之后任务清单要在上下文里维护状态字段用来管理进度[ {task: 收集A产品套餐信息, status: completed}, {task: 收集B产品套餐信息, status: in_progress}, {task: 生成差异对比表, status: pending}, {task: 输出最终建议, status: pending} ]执行完一步就更新一步状态不要每次循环都让模型重新规划整个任务。只有当前步骤失败或被阻塞时才需要对剩余任务做局部调整。3.2 反思机制怎么嵌入而不只是加一句“请检查”很多人会在提示词末尾加“请仔细检查你的回答”这根本不是反思模型只会象征性地回头看一眼。有效反思必须是结构化的给模型一组明确的检查维度要求它对症下药。我常用的反思模板长这样请对照以下维度检查刚才的执行结果 1. 用户最初的目标是否完全达成列出已完成和未完成的部分。 2. 是否有约束被违反例如使用未授权数据、跳过必要步骤、输出格式不符合要求。 3. 当前掌握的信息是否足够得出结论如果不足缺少哪些关键信息 4. 是否存在更优路径例如某个工具调用失败后是否有其他工具可以替代。 输出格式 [结论] 继续执行 / 修正后重试 / 终止并请求用户补充 [理由] 不超过3条每条一句话 [修正动作] 下一步具体要做什么如果没有则写“无”这个结构最大的价值是“可控”。模型必须在结论、理由、修正动作三个框里作答而不是泛泛地说“我检查了没问题”。反思的结果会作为新的上下文片段追加进去让模型带着具体修正动作进入下一轮而不是重复上一轮的思路。3.3 任务清单的完整闭环示例把规划和反思串起来一个典型的闭环是这样的模型生成四步任务清单第一步“收集A产品套餐信息”状态为running调用查询工具失败返回原因是“套餐信息需要登录权限”模型进入反思结论从“继续执行”改为“修正后重试”修正动作是“询问用户是否已登录或改用公开页的数据”模型尝试公开数据页工具成功任务状态更新为completed进入下一步后续步骤全部完成后模型汇总输出对比表和最终建议。如果把反思结果只是简单地“再试一次”第2步之后就会原地打转。差别的本质在于反思里有没有“新信息增量”。没有增量就没有真正的修正只有同一动作的重复播放。当然规划和反思不是免费的每一步都要消耗额外的token和延迟。任务少于三步、目标非常直接时直接执行反而更好。规划是为复杂度付费不为简单任务背锅。4. 多智能体协作什么时候该上什么时候是过度设计多智能体是看起来最美、用起来最重的一类方案。很多项目一上来就设计三五个角色的Agent结果连最基本的业务场景都跑不稳。我的态度是多智能体是手段不是目的先搞清楚它解决的是什么问题再决定要不要上。4.1 三种常用协作架构的取舍我在实践中接触比较多的是三种架构中心协调者一个主管Agent负责任务拆解、分发、汇总其他专职Agent负责执行。职责清晰、分工明确但主管Agent常成为瓶颈所有消息都经过它延迟和token消耗都比较高流水线每个Agent只处理上一阶段的结果输出给下一阶段像工厂流水线。适合“信息加工链路”固定的场景比如先把用户问题分类再抽取关键实体再生成回复对等协作多个Agent互相发消息自由协商。这种最灵活也最难控制消息一多上下文爆炸行为基本不可预测。这三种架构没有绝对优劣只看匹配不匹配。业务路径固定、角色边界清楚的时候中心协调者最稳内容加工链路天然分阶段时流水线更高效对等协作我只在小范围实验里用过至今没有在正式业务里落地过因为它太不可控了。我个人更想强调的是超过六成场景单个Agent加一条工具链就够用了。把客服、检索、总结塞进一个Agent加工具的架构里配合记忆和规划效果往往比三四个Agent协作更好。多智能体的意义在于“职责隔离”和“并行能力”不是为了让架构图更好看。4.2 协作中的通信协议与共享状态多智能体最怕各自为政互相之间传一堆不可解析的对话文本。我的做法是给所有消息统一结构不管是哪个角色发出的{ from: planner, to: data_fetcher, type: task_request, payload: { task_id: t_001, query: 查询A产品的套餐信息, context_ref: conversation_ctx_0713 } }context_ref是用来指向共享上下文里的某一段而不是把整个上下文随消息复制一遍。如果不做裁剪每个Agent都收到全量历史信息过载会让每个角色的判断都变差。共享状态我习惯用“黑板/工作区”的模式一块公共区域按命名空间隔离每个Agent只读写自己的范围。数据抓取Agent写完结果规划Agent去读而不是直接嵌套调用另一方的函数。这样后续排查时能看得清清楚楚谁写的、写的什么、谁读的。通信协议还要约定消息超时和重复消息的处理。我见过一个实验项目两个Agent互发“你确认一下”的消息来回四轮谁都没有真正推动任务本质就是消息协议没有约束“每条消息必须携带进展增量”。多智能体的效率完全建立在“每条消息都要推动事情往前走”这个纪律上。4.3 成本与延迟的现实账单多智能体的账单必须提前算。一个四个角色的Agent协作可能会触发从模型角度二十到四十次调用。如果每个角色都用最大号的模型单次任务的成本可能比单Agent方案高一个数量级延迟也几乎等比例上涨。我通常在方案评审前做一张成本预估表角色模型规格预估调用次数单次请求token预估成本主管/规划大模型63500较高数据获取大模型45000较高内容总结中模型22000中等回复生成大模型23000较高能并行执行的子任务一定要并行发出请求而不是串行等待中间结果先压缩再传给下一个角色重活里的琐碎环节用小模型甚至确定性规则处理。多智能体不是不能上而是不能不看账单就上。架构好看到最后还是要为成本买单。5. 评估一个Agent比评估一个模型难得多模型层面的评估基本是“输入输出对”Agent层面则是“一串决策过程的整体效果”。同一个任务路径可能完全不同有的路径碰巧成功有的路径过程合理但结果失败。如果不把评估做细很容易被表面成功率骗过去。5.1 任务级指标怎么定先把最直接的任务级指标定好包括任务成功率、答案正确率、关键字段完整率。但“成功”的定义不能太模糊要具体到可验证。比如“用户问退货规则模型回答了有关退货的内容”这算成功但没回答到点子上要判断它有没有回答“生鲜是否支持7天无理由”这个关键点就得加字段断言。评估集至少要有三类case标准流程case正常提问、正常工具调用验证主路径稳定边界case模糊表述、缺参数、信息不全、用户重复提问验证边界处理失败恢复case工具返回异常、上游超时验证Agent能不能理智地换路或转人工。结果判分不能只靠人工。我通常用“硬字段断言 LLM裁判”的组合硬字段断言判断关键信息有没有出现LLM裁判判断回答质量和过程合理性。LLM裁判在打分时必须输出评分依据而不是只给一个分数这样一旦分数和预期不符还能倒查是哪一步导致的。5.2 轨迹指标比成功率更早暴露问题成功率是结果指标太滞后了。真正能提前暴露系统劣化的是轨迹指标。我日常监控的主要是这几项指标定义异常信号平均工具调用次数单任务平均调用工具的次数数值明显上升说明模型在反复试错无效调用率调用结果不是FAILED但数据未被后续使用的比例工具调了一大堆最后结论没有引用单任务重启率同一任务重新开始的比例说明模型把自己绕晕了想重新来过最大轮次占比触顶轮次上限的任务比例接近上限说明循环退出的逃生通道可能在失效平均token成本单任务总token数异常上涨代表上下文被无效内容撑大首token延迟从用户提问到模型开始输出的时间过高代表阶段等待或消息过长这些指标往往在任务成功率还没变化之前就开始恶化。比如某次提示词改动导致模型更倾向于调用某个工具前两三天成功率可能没变化但平均工具调用次数已经在悄悄翻倍。盯住轨迹指标能在用户大规模抱怨之前把改动回滚。5.3 让Agent开发进入“可回归”状态Agent项目做得越久越需要一套能重复跑的回归集。我的做法是把每一次线上有价值的case沉淀下来包括用户的原始提问、Agent的完整轨迹、人工标注的期望结果分层标准case进主回归集失败恢复case进特殊回归集。每次改动无论改了prompt、工具Schema、记忆策略还是多Agent的消息协议都全量跑回归然后对比轨迹diff。轨迹diff指的是同一条case在改动前后的路径对比选择的工具是否变了参数是否不同是否多了一次无意义调用是否走了反思分支最终结论是否一致。有了trace之后定位问题会快很多。我给trace里的关键节点打上tag比如“first_planning”“replan_triggered”“tool_failed”“final_answer”。后续在日志系统里按tag搜可以秒级过滤出所有“工具连续失败并进入反思”的case不用一条条翻对话记录。这里还要提醒一句回归集不是一次建好就完事。线上Agent的输入分布会漂移用户会提出当初评估集里完全没有的新问题。我习惯每个月从线上日志里抽一批新case人工标一批滚入回归集。评估集本身要跟着业务一起长否则回归跑得再勤也只是自我安慰。6. 上生产前的最后几道保险Agent上生产最忌讳的是把它当成普通API一样直接暴露出去。它是一段会自我循环的程序任何一环颤抖都可能被放大成整个任务的崩溃。所以我在发布前会重点检查超时、熔断、人机回退、Token预算和缓存这几道保险。6.1 超时、熔断与人机回退Agent循环是把模型调用、工具调用、记忆读写串起来的链路任何一个环节超时都会卡住整条任务链。所以每个环节都要单独设超时不能只靠全局超时兜底模型调用超时、工具调用超时、记忆库查询超时分别设置合理的阈值。工具连续失败的次数一旦达到上限就要触发熔断不要再让模型继续尝试。熔断后系统应该转入一个预设好的人工兜底流程把当前对话上下文、工具调用轨迹、失败原因打包发给人工处理并给用户一个明确的等待信号。最怕的是Agent把用户晾在那里不吭声。无论内部流程怎么跳用户侧必须有一个确定的返回要么给结果要么告诉用户“正在处理/需要转人工”。我见过一个Agent在工具失败后沉默了几十秒用户还以为服务挂了直接流失。上生产前用脚本模拟工具全失败的情况看Agent能不能在几秒内给出兜底话术这是很值得做的演练。6.2 Token预算与缓存策略给单次Agent任务设置Token预算是必须的。任务开始前算出一个预算上限比如普通任务2万token复杂任务6万token实际消耗接近上限时强制触发总结或转人工。别以为只有钱的问题token失控往往意味着上下文里塞了太多垃圾模型输出质量的下降比账单更难处理。缓存是省成本的大头。系统提示词、工具定义、检索结果这些不常变的内容利用精确前缀缓存可以大大减少重复计费。工具返回结果用模板化结构固定字段顺序和格式也便于缓存命中。我强调过很多次的一个例子某内部场景把工具输出从平均3000 token压到400 token并启用缓存后成本下降接近一半模型输出质量反而更稳定了。原因不复杂短内容更容易被模型准确“看到”。但缓存也有坑提示词或工具Schema更新后必须主动清掉相关缓存否则线上跑的还是老一套逻辑排查起来非常冤枉。6.3 我踩过的一次“Agent自我循环”事故最后讲一个真实教训。某内部自动答疑Agent上线三个月后有一天凌晨日志量突然暴涨排查发现它陷入了非常典型的自我循环同一个查询工具被连续调用了15次参数几乎一样每次工具都返回“上游服务暂时不可用”模型就继续调同一个工具。日志里整整齐齐重复了15行像复读机一样。当时根因有三层。第一层工具返回的错误是面向用户的一句话文案模型能读懂“暂时不可用”但不知道“那我该换什么办法”第二层循环没有任何单工具重试次数的硬限制prompt里那句“如果失败请重试”被模型当成了无限重试的许可第三层全局步骤上限设置得太宽中途没有任何熔断信号。修复方案也很直接工具返回改成结构化错误信息明确原因和建议动作同一工具连续失败两次后系统级拦截并强制模型换路或转人工加上全局调用次数限制和触发告警。上生产前还专门加了一条自动化故障演练模拟“所有工具都失败”的场景确保Agent能在几轮内自动收敛。把这段经历写出来是想说明一件事Agent这条路上稳定比聪明值钱。把循环的逃生通道设计好比把单轮回答调漂亮更重要。当你下次再给Agent加新工具、新记忆或新角色时先问一句如果它在这里卡住了它会怎么出来想清楚这个问题Agent才能真正从Demo变成可用产品。
阅读完成 · 觉得有帮助?