【真香警告】大模型Agent开发全攻略:模型选择、工具设计、护栏搭建,一文搞定Agent开发全流程
如果你已经在Agent项目里扑腾过一阵,大概率经历过这种阶段:刚接触时觉得“大模型Agent不就是调个API吗”,真动手之后才发现模型选型、工具定义、安全护栏环环踩坑,随便一个环节不严谨,整套系统就在线上表演什么叫“一本正经地胡说八道”。
这篇文章我不打算写教科书式的概念堆砌,而是把整个Agent开发流程拆成三条主线:模型选择、工具设计、护栏搭建。为什么是这三块?因为模型决定Agent的“脑子”够不够用,工具决定它“手脚”能不能干粗活,护栏决定你敢不敢让它直接面对真实用户。内容会覆盖从选型思路到代码实现再到问题排查的完整闭环,适合两种人:一是想从零开始搭第一个Agent的初学者,二是已经写了几个Demo但总觉得效果不稳定的开发者。你可以把它当成一份可以反复对照的实操手册,而不是读了就忘的科普文。
1. 先别急着写代码:把Agent全流程拆成三块拼图
很多新手拿到Agent项目的第一反应是打开代码编辑器,直接往里面塞一大堆Prompt,然后用某个模型API跑通一个DEMO,发现“能聊了”就以为大功告成。这个流程十有八九会在后面翻车,因为Agent发布和聊天机器人完全是两码事。
真正跑在业务里的Agent,内部其实是一条流水线:接收任务、拆解意图、规划步骤、调用工具、读取结果、校验输出、生成回复,每一步都有独立的判断逻辑,而所有判断都围绕一个核心——大模型本身。用个不太严谨但贴切的比喻:LLM是员工的大脑,工具是它的双手,护栏则是公司制度和合规流程。大脑负责想怎么做,双手负责把想法落地,制度负责保证它别把事情搞砸。
1.1 Agent不只是“调大模型”,它是把大模型变成操作员
纯聊天的模型你问它“今天天气怎么样”,它只能回答“我无法获取实时数据”,因为它的知识截止在训练时刻,也没有主动获取外部信息的能力。Agent和普通聊天的关键区别,就是给模型装了一整套“可以动手”的机制。
具体来说,Agent接到用户请求后,会经历类似这样的循环:模型理解用户意图,决定需要调用某个工具,然后按照约定好的格式输出一个“调用请求”;系统把这个请求转成真实的API或函数调用,拿到返回结果后再塞回给模型;模型基于这个结果继续推理,决定是再调下一个工具还是整理答案回复用户。这个循环可能执行一次,也可能执行十次,直到模型认为任务完成。
所以Agent开发本质上是在做两件事:一是让模型在正确的时候做出正确的“工具调用决策”,二是保证整个循环过程中的数据和行为可控。前者靠模型能力和工具设计,后者靠护栏体系。这就是为什么我不会一上来就让你写代码——先把这三块拼图搞清楚,后面每个环节都能少走弯路。
1.2 三个决定性问题的权衡
模型、工具、护栏三者不是割裂的,它们相互制约。模型能力不够时,工具设计得再标准,模型也可能调错参数;工具定义一团乱麻时,再强的模型也会被误导;护栏搭得太死,Agent会变得畏手畏脚干脆不干活;护栏太松,你敢让它处理真实业务吗?
模型层面要回答的问题是“我用多大能力、多高成本的模型来承担这个任务”。工具层面要回答的是“我把哪些能力暴露给模型、用什么格式暴露才最不容易出错”。护栏层面要回答的是“哪些动作绝对不能做、哪些输出必须拦截、哪些成本必须封顶”。
三个问题的答案加在一起,就决定了这个Agent最终是“能用”还是“能上线”。注意这两个词差着一个量级。“能用”意味着你本地测试10次成功8次;“能上线”意味着面对几千个真实用户的随机输入,系统依然能稳定安全地工作。大部分Agent项目就死在从“能用”到“能上线”这条路上,而这条路恰恰需要一开始就做好三者的平衡设计。
2. 模型选择:别只看榜单分数,要按任务场景挑选手
模型选型可能是整个Agent开发里最容易让人纠结的环节。每次新模型发布,一堆榜单分数满天飞,看着哪个都强,结果真上手一测,有的连JSON都返回不规整,有的工具调用总把参数填错,有的贵得让人肉疼。
我的建议很简单:把“哪个模型最强”这个问题,换成一个更实际的问法——“我的Agent最常做什么动作,哪个模型做这个动作最划算”。不同模型的定位完全不同,理解这些定位比盯着分数有意义得多。
2.1 三类模型怎么选:闭源旗舰、开源基座、轻量专用
我习惯把当前能接触到的模型分成三个大类,各有各的适用边界。
第一类是闭源商业旗舰模型。这类模型的能力天花板最高,复杂推理、长文本理解、工具调用稳定性都做得最好。代价是成本高、有数据外发风险,而且它的服务完全依赖厂商,接口不稳定或者产品策略调整都会波及你。适合的场景是:面向真实用户的客服Agent、复杂任务编排、对效果要求极高且数据不敏感的公开服务。
第二类是开源基座模型。参数规模大、可以私有化部署,能在数据不出内网的场景里跑,也能针对自己的业务做下游微调。代价是部署和维护成本高,需要比较强的工程团队,效果和顶级闭源模型在复杂推理上通常还有差距。适合的场景是:数据敏感的企业内部Agent、政务和金融等合规要求严格的领域、需要长期定制优化的项目。
第三类是轻量专用模型。参数量小、速度快、成本极低,在简单意图识别、格式转换、关键信息抽取这类单一任务上表现出色。代价是复杂推理和长上下文处理能力有限,多轮对话中容易“失忆”。适合的场景是:作为Agent的“辅助引擎”存在,比如用大模型做规划、用小模型做分类,或者承担高并发低延迟的简单子任务。
在真实项目里,这三类模型往往同时出现,配合使用。这也是我在选型时给读者的第一个忠告:别迷信单一模型,Agent系统里完全可以多模型共存。
2.2 模型选择的四个硬指标
如果抛开榜单,只看工程指标,我会建议你按四个维度给候选模型打分。
第一个是上下文长度。它决定了模型在一次会话里能“记住”多少信息。很多人只看这个数字有多大,却忽略了一个关键问题:上下文越长,模型注意力越容易被分散,而且费用随token线性增长。实际使用时,一个Agent的上下文通常要被系统提示词、工具描述、对话历史、工具返回结果四类内容挤占。如果选了“可以装下一整本书”的模型,但对工具调用格式的理解不到位,效果反而不如短上下文但执行精准的模型。
第二个是工具调用能力。这是Agent项目里最重要的单项指标。你需要检验模型是否严格遵守工具调用格式,是否能在一次回复中同时发起多个工具请求,遇到工具返回异常时是否能正确处理而不是自说自话。别信宣传页上的“支持工具调用”,拿你自己的真实工具定义去测,测出来才算数。
第三个是推理成本。它不只是简单的“1次调用多少钱”,而是一笔动态账:模型输出质量差,会导致Agent反复重试,总成本翻好几倍。聪明的做法是给项目定一个“单任务成本预算”,比如“完成一次客服问答,模型调用总成本不能超过X元”,再反过来倒推选哪个模型。
第四个是延迟。Agent和纯聊天的差别在于,它经常需要多轮“模型<->工具”循环,一轮循环就是2到4次模型调用。每次如果多花1秒,用户感受到的就是好几倍的延迟放大。对延迟要求高的实时场景,轻量模型往往是更优解。
2.3 我的选型心得
我个人做Agent项目的经验是:如果你想快速验证业务闭环,直接选市面上公认的闭源旗舰模型跑通全流程,先不要考虑优化成本和速度,重点是验证“这件事到底能不能用Agent做”。验证通过后,再做一轮性能剖析:把调用记录拉出来,看看哪些子任务消耗了绝大多数token,哪些步骤延迟最长,然后针对这些热点子任务尝试替换成更便宜或更快的模型。
举个例子,一个自动查单客服Agent,大模型全程顶着,费用可能很高。但如果你把“用户意图识别”这一步单独抽出来,换成一个轻量模型来分类,只有分类结果需要复杂推理时才调用旗舰模型,总体成本能下降一半以上。这就是“多模型协同”的实用价值。等到需要私有化部署时,再考虑开源基座模型,但要做好心理准备:效果调试成本会明显上升,不是换个模型就行的事。
3. 工具设计:把Agent的“手”打磨到指哪打哪
选好模型后,第二件大事就是设计工具。这里的“工具”指的是Agent可以调用的外部能力,比如查数据库、调业务API、发邮件、执行计算。工具设计得好不好,直接决定了Agent能不能稳定完成实际任务。
我在实际项目里见过太多失败案例:有的工具没有参数校验,模型乱传参数就白调一次;有的返回结果是一堆嵌套JSON,模型读完一脸懵;有的工具描述含糊,模型压根不知道什么时候该调用它。这些都是工具设计不到位导致的。
3.1 工具的本质是给模型开一扇受控的门
理解工具设计,首先要理解Agent系统里“工具”和普通API的区别。普通的API是给人调的,人知道参数含义、知道返回格式,容错性高。Agent里的工具是给模型调的,模型并不知道你的业务背景,它只能依据你提供的“工具说明”来判断怎么用。
所以工具设计本质上是在做一件事:用结构化的方式,让模型清晰理解“什么场景下应该调用这个工具、参数怎么填、拿到结果后怎么理解”。这个结构化描述在技术上是一份工具Schema,里面写清楚工具的名称、功能描述、参数列表和返回值格式。
一个标准的工具Schema通常包含几个关键字段:工具名,参数名,类型,是否必填,还有每个参数的详细描述。注意,这个描述要用模型能理解的语言去写,不能照搬后端接口文档的口吻。用户说“帮我查一下单号12345到哪了”,模型要先识别出“12345”对应订单查询工具的order_id参数,然后把值填进去。这个识别能力一半靠模型本身,一半靠你的参数描述写得够不够清楚。
3.2 工具描述的玄学:好的描述比好模型更能提升命中率
很多人以为工具调用失败是模型笨,其实问题往往出在工具描述上。模型的工具调用准确率,很大程度上取决于它对工具“什么时候用、怎么用”的理解是否到位。
我的第一个经验是:工具命名要用“动词+名词”的结构,而且语义要直白。类似“fetch_orders”“query_user_balance”“send_email”这样的命名,模型一看就知道是干嘛的。千万别用那种只有程序员才懂的缩写,比如“sys_ord_qry”,模型大概率会蒙圈。
第二个经验是:工具描述里要写清楚触发条件、边界条件和返回内容。什么叫触发条件?“当用户想查看订单物流状态时,调用此工具”。什么叫边界条件?“仅支持近30天订单,超过范围请告知用户无法查询”。这些信息看起来啰嗦,却能大幅减少模型在边缘情况下的乱调用。
第三个经验是关键且容易被忽视的:在工具描述的末尾加“示例”。比如订单查询工具后面加一句“示例:用户询问订单20240901到哪了,参数order_id传入20240901”。模型看到示例后,对参数格式的理解会清晰很多,这个技巧在开源模型上效果尤其明显。
3.3 参数设计与返回格式:一切为了模型少犯迷糊
参数设计有几个实用原则。第一,能不设计成必填项就不要必填。Agent调用工具时如果漏填了参数,模型会尝试自己瞎编一个值填进去,或者直接报错。能设默认值的参数尽量给默认值,比如时间范围默认最近7天。第二,能用枚举约束的就别开放自由文本,比如状态字段设置成只能传“pending、shipped、completed”三个值,模型的选择空间小了,出错空间也小了。第三,参数描述要写单位写边界,比如“amount请以分为单位传入,且不超过10000000”。
返回值设计同样关键。工具返回给模型的不是给人看的界面,而是给模型“阅读理解”的数据。所以返回内容一定要结构化,建议统一用JSON格式。
拿我做过的一个订单Agent举例,工具返回如果是一大段自然语言“您的订单已于2024年9月1日发货,物流公司为某快递公司”,模型虽然能读懂,但后续如果要二次计算就很费劲。但改成结构化返回:
{ "order_id": "20240901", "status": "shipped", "shipped_at": "2024-09-01 10:30:00", "carrier": "SF", "tracking_no": "SF1234567890" }模型一眼就能找到需要的字段,后续的推理压力小很多。
差点忘了一个原则:工具的返回信息也要包含“错误情况”。真实世界里API调用经常失败,所以工具返回格式里最好预留error字段:调用成功返回空或null,调用失败返回具体错误码和原因。Agent拿到error信息后,才能向用户解释“为什么查不到”,而不是自作聪明地编一个结果。
3.4 工具协议的实用选择:原生函数调用与MCP
现在实现工具调用,协议层有两种主流做法:一是模型厂商提供的原生函数调用功能,也就是你在API请求里把工具Schema传给模型,模型输出结构化的工具调用请求;二是通过MCP这类开放协议来实现模型与外部工具源的标准化连接。
原生函数调用的优点是简单直接,厂商帮你处理了大部分格式化细节,同一个模型生态内工具和推理配合自然。缺点是一旦你换了模型厂商,工具定义可能需要跟着调整,不易跨模型共享。
MCP的思路是:把“工具”统一封装成一套标准化服务,模型通过一个统一的客户端接口去发现和调用“任何已注册的工具”。它最大的价值在于生态复用:别人写好的一个工具服务,拿到你的项目里改一改配置就能直接用,不用关心底层实现。
我的建议是:项目起步阶段,先用原生函数调用快速跑通,工具数量少,改动频繁,原生方案成本最低。当你的Agent需要接入的工具源越来越多,比如要同时接数据平台、内部OA、第三方API,工具数量膨胀到十几个以上,这时候再引入MCP来治理一地散落的工具会轻松很多。但注意,MCP不是银弹,它解决的是“连接标准化”问题,你自己的工具逻辑该设计还得设计。
4. 护栏搭建:让Agent在“安全赛道”里自由发挥
很多Agent项目开发时一切正常,一上真实环境就翻车,原因基本是同一个:缺少护栏。所谓护栏,就是在Agent运行路径上提前设置好的检查点、限制条件和熔断机制,确保它不会做出危险动作、不会产出违规内容、不会失控地烧钱。
我会从四个维度搭建护栏:输入护栏、输出护栏、行为护栏和数据护栏。每一层解决一类问题,合在一起就是一套完整的“安全赛道”。
4.1 四类护栏对照
输入护栏解决的是“什么请求可以进来”。它主要做过滤和降级处理,比如把明显恶意或跑偏的输入挡在Agent逻辑之外,避免模型回答不相关或有害内容。
输出护栏解决的是“什么回复可以出去”。模型是概率生成系统,不管模型多强,都可能产生幻觉、漏答、格式错误甚至输出敏感内容。输出层检查是最后防线。
行为护栏解决的是“Agent能做什么动作”。它限制工具权限、控制高风险操作,比如禁止Agent直接删除库表、禁止执行金额超过阈值的操作、要求高危操作必须二次确认。
数据护栏解决的是“数据在Agent回路里如何流转”。包括上下文隔离、最小权限原则、日志脱敏,核心是防止A用户的数据跑到B用户的上下文里,也防止敏感信息被模型带出。
四层护栏的关系是递进互补的,靠单层的加固检查不够。比如输入护栏挡掉了一部分恶意输入,但总有个别漏网之鱼,输出护栏能兜住对应的输出风险;行为护栏防止的是模型“做错事”而不是“说错话”,输入输出护栏管不住它调用危险工具。
4.2 输出护栏三件套:格式校验、敏感信息过滤、自洽性检查
输出护栏里最常用的是三个组件。
格式校验适用于要求模型输出固定结构的场景。比如你要求模型必须返回JSON,那么模型输出后,程序应先用JSON解析器验证一遍,而不是直接塞给前端。这一步在代码里就是几行try/except,但能挡住很多偶发的格式错乱。
敏感信息过滤是必要环节。Agent经常查询真实数据,模型回填答案时有可能把中间计算结果里的手机号、身份证号直接输出。处理方式是在输出层挂一个关键词和正则规则的过滤器,检测到敏感格式就把对应内容打码或拒绝输出。对于金融和医疗类Agent,这一步强烈建议做到“强制脱敏”,不在进程里持续保留原始明文。
自洽性检查是稍高级一些的护栏。做法是让模型对自己的回答做个二次校验,比如“请检查你刚才的回答是否严格基于工具返回数据,是否存在编造”。简单场景可以让模型自己复核一遍,或者针对关键数据用规则做一致性比对,比如工具返回的金额字段和回答中的金额字段是否一致。
4.3 行为护栏:权限分级与人机确认
给Agent搭行为护栏,核心是权限分级。你不能让一个Agent拥有你后端服务的全部权限,这就好比你不能让每个实习员工都直接操作生产数据库。
实际操作时,我会把Agent可调用的工具分成两类:只读类工具,比如查询订单、检索文档,Agent可以自由调用;写操作类工具,比如修改订单、发送退款指令、删除文件,Agent调用前必须触发人工确认。具体实现可以是:当Agent的规划结果中出现了写操作,系统先暂停,把请求推给用户在界面上点一次“确认执行”,确认后才真正放行。
别觉得这样降低了Agent的“智能感”,真实业务里“可靠”永远排在“酷”前面。自动客服自动提交了退款申请但没校验余额,后果谁来承担?有了人工确认机制,就多了一道真正的安全网。
行为护栏还包括“流程上限”控制。第一是调用轮次上限:Agent在一个任务里最多循环多少次,超过就强制终止,避免出现“死循环”空转。第二是成本上限:单任务累计token或费用超过预算,自动熔断。第三是权限上限:所有写操作默认关闭,走显式授权。
4.4 数据护栏:上下文隔离与最小权限
数据护栏最容易被忽略,因为它出问题往往是隐性事故。我见过最典型的低级事故:一个共享后台里,用户A查自己的订单,Agent却把用户B的订单查出来了。原因是工具调用时没有把当前会话的用户身份绑定到查询参数上,模型或者工程层漏传了身份标识。
解决方案是:上下文注入。每个用户的会话在Agent里都有独立的上下文隔离区,工具调用时强制从当前会话里取出用户ID,作为工具参数一并传入。后端工具也要做二次校验,不能只依赖前端传值,确保只有本人能查本人的数据。
另一个细节是日志脱敏。开发时为了排查问题,我们经常把Agent的完整调用链打成日志,但日志里如果直接打印用户手机号、身份证号,后面排查越方便,数据泄露风险越大。我的习惯是:日志落库前先做脱敏处理,只用“138****0000”这种形式保留必要标识;原始明文要么不落库,要么单独存到安全级别更高的存储里且严格控制访问。
数据护栏还有一个容易踩的坑:模型上下文爆炸后“串味”。如果没有对上下文长度做截断,长时间运行的会话会把很早之前的无关内容一起塞进模型输入,既费钱又容易让模型被早期信息干扰。所以数据护栏必须包含会话记忆的清洗机制,比如按窗口截断历史、摘要化旧对话等。
5. 实操装配:从零搭一个会自动查数据的小助手
理论聊了不少,下面进入动手环节。这个项目我选一个最常见的场景:搭一个“查询订单状态的小助手”,用户发一句“帮我查一下订单12345的状态”,Agent能自己调用订单查询工具并返回结构化结果。
为便于复现,我尽量用通用的Python和HTTP服务来示范。整体架构是:一个Agent编排主程序,负责接收用户输入、调用模型、解析工具请求、执行工具、回填上下文;一个工具网关服务,用最小化的工具实现来演示;一层输出护栏做格式和敏感信息校验。
5.1 准备环境和模型入口
假定你已经有一个可以调用的大模型API,支持对话补全和工具/函数调用能力。跑这个Demo只需要在一台能联网的机器上装好Python环境和常用的HTTP请求库即可,不需要额外部署数据库或消息队列。
模型入口建议封装成一个独立的模块,避免在主逻辑里到处写API的地址和密钥。你可以先定义一个非常简单的封装函数:输入消息列表和可用的工具列表,输出模型回复的文本内容和工具调用请求。这一步写好后,后面替换模型供应商只需要改这一个模块。
5.2 定义第一个工具
先在工具网关服务里实现一个最简单但贴近真实场景的订单状态查询接口。它接收订单号,返回订单状态、发货时间和物流单号。为了演示参数设计,我把订单号设置成必填字符串,并做了格式规则校验。
对应的工具Schema这样定义:
{ "type": "function", "function": { "name": "query_order_status", "description": "根据订单号查询订单的当前状态、发货时间及物流单号。当用户询问订单物流、发货、签收状态时使用。示例:用户问'订单20240001发货了吗',参数order_id传入20240001。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为8-20位字母或数字。" } }, "required": ["order_id"] } } }注意我特意在description里写了示例,并把触发条件说清楚。这些都是为了提升模型一次调用正确的概率。工具网关的实际处理函数就是按order_id从假数据库里取一条记录,然后按约定的统一JSON格式返回:
成功时返回:
{ "success": true, "data": { "order_id": "20240001", "status": "shipped", "shipped_at": "2024-09-01 10:30:00", "carrier": "SF", "tracking_no": "SF1234567890" } }查不到时返回:
{ "success": false, "error": { "code": "ORDER_NOT_FOUND", "message": "订单不存在,请检查订单号是否正确" } }错误也保持结构化,模型读到error后能理解“查不到”是客观事实,而不是自己推理出了问题。
5.3 写Agent编排主逻辑
核心编排逻辑是一个循环:模型判断要不要调工具,要就调,调完把结果回填给模型,模型再继续判断。这里我用伪代码展示关键流程:
def run_agent(user_input, tools, max_rounds=5): messages = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input}] for round_idx in range(max_rounds): response = call_model_with_tools(messages, tools) if response.get("tool_calls"): for tool_call in response["tool_calls"]: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call["id"], "content": json.dumps(result, ensure_ascii=False) }) continue else: final_text = response["content"] return final_text return "抱歉,我无法在有限步骤内完成该任务"循环的结构是固定的:每次模型只要发起了工具调用,系统就把工具结果追加到对话里然后继续下一轮;只有当模型不再要求调工具而是直接产出回答时,才把那段回答作为最终结果返回。max_rounds这个上限必须存在,否则模型一旦陷入“查A->再查B->再查A”的循环,没有人喊停就会一直烧钱。
execute_tool这个函数在真实项目中干两件事:根据工具名路由到对应的执行函数,再用当前会话里的用户身份做参数装配。后者很容易被忽略,但数据隔离就靠它。比如工具不知道用户是谁,查询函数就得从上下文的会话变量里取user_id,然后作为附加参数一起去调订单服务,防止串号。
5.4 把护栏织进去
有了主逻辑后,再把护栏嵌到编排层的几个关键位置。
请求进来时做输入护栏:先做一轮“意图范围”检查,比如用关键词或一个小分类模型判断用户是否在问订单类内容,完全无关的输入直接返回“我只能帮你处理订单相关问题”,不进主循环。这一步挡掉大部分无效请求,也避免模型自由发挥。
工具调用前做行为护栏:main里的execute_tool执行前,判断工具是只读还是写操作。query_order_status属于只读,直接放行;如果未来加了一个“refund_order”这类写操作工具,就必须在execute_tool里增加一个confirm层,先暂停执行并询问用户确认。
工具调用后做输出护栏:拿到工具的返回结果后,不要直接拼进模型上下文,而是先做一步脱敏处理。比如订单的手机号字段,统一替换成脱敏格式再喂给模型。这样就算模型“忍不住”想把完整信息念出来,上下文里也没有原始明文可用。
最终回复给用户前再做一次输出检查:把模型生成的自然语言里,所有可能出现的手机号、金额等敏感格式用正则扫一遍,发现有疑似敏感串就直接拦截,改成模板化的提示。这个过程虽然简单,但对“从能用变到能上线”非常关键。
6. 常见问题与排查技巧实录
这部分我直接用踩坑记录的方式来写,都是我在Agent项目迭代中真实遇到过的典型问题。整理成一张速查表,方便你哪天遇到类似现象时快速定位。
6.1 现象、原因、解法速查表
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| Agent反复调用同一工具但结果一样 | 模型没理解“重试无意义”,或上下文里缺少“已尝试过”的状态标记 | 在系统提示里增加“若工具返回相同结果则停止重试”,或者在编排层对重复调用计数 |
| 模型不调用工具,直接乱答 | 工具描述与用户问题的关联性不够强 | 检查工具的description和示例,是否覆盖了用户意图的表达方式 |
| 工具参数频繁填错 | 参数描述不够明确,或模型本身工具调用能力弱 | 简化参数数量,给每个参数加边界和示例,必要时换工具调用更稳的模型 |
| 模型输出非JSON | 系统提示没有强调格式,或模型稳定性差 | 输出层加解析器和重试逻辑;在系统提示里明确“必须返回JSON” |
| Agent上下文越积越长后效果变差 | 没有做历史截断或摘要 | 实现滚动窗口,只保留最近N轮对话+历史摘要 |
| 返回给用户的内容里混入其他用户数据 | 工具调用时没有注入当前用户身份 | 检查所有工具调用是否都从会话上下文里取了user_id并传入 |
| 单任务费用超出预算 | 模型无效重试次数太多 | 加max_rounds上限、加重复调用检测、优化工具返回减少二次查询 |
6.2 排查方法论:先分环再二分
遇到Agent行为诡异时,不要直接怀疑模型“智商不够”,先定位故障发生在哪个环节。我习惯把问题拆成三层:模型层、工具层、编排层。
模型层的问题表现为:该调工具时没调,不该调时乱调,或者输出内容和工具返回结果对不上。这种情况先做最小化测试:把同样的上下文直接发给模型,看它单独的输出是什么,绕过编排逻辑。
工具层的问题表现为:工具调用确实发出去了,但返回结果错误、超时、甚至服务端报错。这种情况要把工具单独拿出来调一遍,脱离Agent环境,用真实参数测。
编排层的问题表现为:模型和工具单独测都正常,组合起来却兜不住。这时去看日志里message数组,重点检查是不是把历史内容、工具返回顺序、角色标签搞错了。
6.3 几个容易踩的坑
第一个坑是上下文扩容之后模型反而“变笨”。单纯为了提高“记忆力”把所有历史都塞进去,模型输入长度大大增加,噪音也成倍增加,注意力分配被稀释。我的经验是:能摘要的摘要,能截断的截断,只有当前关联度高的内容放前部,模型一般会看开头看得最仔细。
第二个坑是工具描述太长反而干扰调用。工具描述写得太详细会挤压上下文空间,甚至模型在判断用哪个工具时被无关信息带偏。工具描述够用就好,关键信息放到参数的描述里,不要在description里写长篇大论的使用手册。
第三个坑是编排层没有设置并发限制。Agent主循环内部如果并发执行多个实例,平摊到工具网关的压力会很大,经常导致数据库连接爆掉。务必要给Agent层加信号量或令牌桶限流,尤其当Agent面向公共用户开放时,这个坑几乎是必踩的。
第四个坑是日志里打印了完整上下文。调试时想方便,把整段对话、工具返回甚至API密钥都打出来,结果一上线日志被拖走,敏感数据全泄了。我现在的习惯是日志只记必要信息:用户ID(脱敏)、任务ID、工具名、调用耗时、结果摘要,敏感内容一律打码。
结尾
每次做完一个Agent项目,我最深的体会都是同一句话:模型能力只是起点,工程精度才是生死线。选一个好模型能让你少费很多嘴皮子,但真正决定项目能不能扛住真实用户和业务的,是工具定义得够不够清楚、护栏搭得够不够密、日志和监控设计得够不够周全。
最后再分享一个小技巧:Agent开发时,先把“错误路径”测透再测“正确路径”。多数人习惯先跑通“用户正常提问->正常调用工具->正常返回”这条黄金链路,等上线了才撞见各种异常输入。反过来做:一开始就用空参数、重复参数、超范围参数、敏感词、多轮恶意输入去测你的护栏,等这些用例全过了,黄金链路大概率也能顺理成章地通过。这个过程虽然不够“炫酷”,但它是把Agent从 Demo 阶段推向可上线阶段最扎实的一条路。