Agent 这个词这两年听得耳朵起茧,但真到项目里把它跑起来、还要扛住线上流量的人,其实没那么多。大多数人卡在两个地方:一是看不懂框架里那一堆抽象到底在干什么,二是本地 Demo 跑得挺顺,一上生产就各种超时、死循环、成本失控。这篇就围绕国内头部厂商开源的那套 Agent 框架,把我自己从读源码、搭 Demo 到推进生产的完整过程摊开讲:它是什么、能解决什么问题、哪些环节是真正的难点、哪些坑可以提前绕开。不管你是刚接触 Agent 的后端同学,还是已经在做 PoC 想往生产推的负责人,下面这些内容应该都能直接拿去用。
1. 先把 Agent 这个概念拆开看
1.1 从"调一次模型"到"跑一个闭环"
普通的大模型调用是这样一个动作:我给一段文本,模型返回一段文本,结束。这个模式能解决的问题是有边界的——翻译、改写、分类、抽取,都属于"一次输入一次输出"。但真实业务里的需求往往不是这样。用户说"帮我查一下上周的订单异常,把金额超过五千的列出来,再按客户分组发一份汇总",这一句话背后是查库、筛选、聚合、生成报告四个动作,而且第二个动作依赖第一个动作的结果。单次调用做不到,因为模型不知道上周的订单数据长什么样。
Agent 的本质就是把这个"多次调用 + 外部动作"的编排逻辑交给模型自己决定。模型先想一步,决定要做什么,调用一个工具拿到结果,看到结果再想下一步,直到它认为任务完成。这个循环在业内通常叫 ReAct,即"推理 + 行动"交替进行。它解决的不是"模型聪不聪明"的问题,而是"模型能不能接触到外部世界并改变外部世界"的问题。明白了这一点,后面所有框架设计你都能对上号。
1.2 一个 Agent 的最小构成:模型、工具、记忆、循环
把这套东西拆到最简,其实就四个零件。模型是大脑,负责决策;工具是手脚,是模型能调用的外部函数,比如查数据库、调接口、读文件;记忆是上下文,包括当前对话历史、中间结果、以及跨会话的长期信息;循环是骨架,负责把"思考—调用—观察"串起来,并决定什么时候停。
四个零件里,最容易被人低估的是"循环"。因为循环意味着不确定的轮次,而不确定的轮次意味着不确定的耗时和成本。我见过太多团队在设计阶段只想着"能力多强",上线之后被 P99 延迟和账单打醒。所以在动手之前,你心里要有一根弦:Agent 的每一个额外轮次,都是要花钱、花时间、并且增加出错概率的。后面讲工程落地时,很多手段都是围绕"怎么把轮次压住"展开的。
1.3 开源框架到底帮你干了哪些脏活
自己从零写一个 ReAct 循环,代码量其实不大,一两百行能跑起来。但为什么还要用框架?因为能跑和能上线之间隔着一条河。框架真正有价值的部分不在那个 while 循环,而在于它把这几件事标准化了:工具的描述格式与参数校验、多轮消息的拼装与裁剪、流式输出的分片处理、不同模型厂商的接口差异抹平、以及调用链路的钩子(hook)机制——后者是你接入日志、监控、权限校验的入口。
我读这套框架源码时最大的感受是,它的分层做得比较克制:底层是模型适配层,中间是消息与工具抽象,上层是 Agent 编排。这种分层的好处是,你可以在不改业务代码的前提下换模型、换工具实现。判断一个 Agent 框架值不值得用,我一般就看它这三层是不是清晰、钩子是不是够用、以及出问题时能不能拿到足够的信息定位。这三点比"支持多少种花哨的编排模式"重要得多。
2. 框架选型:先看业务形状,再看框架
2.1 三类主流开源 Agent 框架的定位差异
市面上能拿来做生产的开源 Agent 框架,大致可以分成三类,它们的取舍完全不同,选错了后面会很别扭。
| 类型 | 核心抽象 | 优势 | 适合的场景 | 主要代价 |
|---|---|---|---|---|
| 轻量工具调用型 | 工具 + 单 Agent 循环 | 代码少、链路短、好调试 | 单任务、工具数量在十个以内 | 复杂编排要自己写 |
| 工作流编排型 | 节点 + 边 + 状态 | 流程可控、可视化、易审计 | 步骤固定、需要人工介入 | 灵活性差,改流程成本高 |
| 多 Agent 协作型 | 角色 + 消息总线 | 能拆解复杂任务、可并行 | 长链路、多领域协同 | 调试困难、成本高、易失控 |
我的经验是:能用一个 Agent 加几个工具解决的,绝不上多 Agent。多 Agent 的调试成本是线性叠加的,两个 Agent 互相传消息,出问题时你要同时看两边的上下文,定位难度直接翻倍。很多团队一开始就设计出"规划者 + 执行者 + 审核者"的三层结构,结果上线后发现 80% 的请求根本用不到这么复杂,反而被额外的消息往返拖慢了。
2.2 什么场景该用 Agent,什么场景老老实实写代码
这是我最想强调的一条。Agent 不是万能胶,它适合的是"步骤不固定、需要根据中间结果决定下一步"的任务。反过来,如果业务流程是确定的——比如"收到订单→校验库存→扣减→发通知"——那就写普通代码,用工作流引擎,别让模型来决定流程。让模型做确定性的事,只会带来不确定性,没有任何收益。
判断标准可以简化成一句话:如果这件事你没法用 if-else 写清楚,那才轮到 Agent 上场。比如"帮用户从他的知识库里找到相关文档并总结",检索哪几个关键词、要不要换关键词重试、总结到什么粒度,这些都不好写死,模型来判断更合适。而"把这份 JSON 转成固定格式",写个解析函数比什么都靠谱。
2.3 选型时我重点看的五个硬指标
抛开文档写得漂不漂亮,我实际会盯这五点。第一,模型适配层是否解耦,能不能在不改业务代码的情况下换模型,这决定了你未来被单一供应商绑死的程度。第二,工具调用的参数校验是否严格,有没有基于 schema 的校验和错误回传机制,这直接决定线上参数错误的爆炸半径。第三,是否有完整的回调钩子,能挂上 trace、日志、耗时统计。第四,流式输出是否支持工具调用场景,很多框架在流式下遇到工具调用就退化成非流式,体验会断档。第五,社区活跃度和 issue 响应,这不是虚荣指标,而是你踩坑时能不能找到同行。
这五点里,我认为第二点最容易被忽略但最要命。模型生成参数出错是常态,不是异常。如果框架没有把校验错误结构化地回传给模型,模型就没法自我纠正,你只能看到一堆莫名其妙的失败。
3. 从零跑通第一个 Agent
3.1 依赖与项目骨架
先搭一个最小可运行的环境。我的习惯是先把目录结构定好,避免后面工具越加越乱。
# 建议使用独立虚拟环境,避免和现有项目依赖打架 python -m venv .venv source .venv/bin/activate # 安装框架本体与模型 SDK(具体包名以你选用的框架文档为准) pip install agent-framework-core pip install openai-like-sdk目录我会这样分:
project/ ├── agent/ │ ├── builder.py # Agent 构建入口 │ ├── tools/ # 所有工具,一个文件一个工具 │ └── prompts/ # 系统提示词,独立管理 ├── config/ │ └── settings.py # 模型、超时、轮次上限等配置 ├── observability/ │ └── tracer.py # 链路追踪封装 └── main.py # 对外服务入口把提示词和工具从代码里拆出来单独管理,是我踩过坑之后的习惯。提示词改动频率远高于代码,放在独立文件里,改完不用重新走一遍发布流程,也方便做 A/B 对比。工具一个文件一个,是为了让每个工具的描述、参数、实现、错误处理都聚在一起,读代码时不用来回跳。
3.2 工具注册:把函数变成模型能理解的能力
工具注册看着简单,其实是整个 Agent 里最需要精心设计的部分。下面是一个典型的工具定义(伪代码示意,具体装饰器名称以你用的框架为准):
from agent_framework import tool @tool( name="query_order_stats", description="按时间范围和金额阈值查询订单统计。仅支持查询最近90天的数据。" ) def query_order_stats( start_date: str, # 格式 YYYY-MM-DD end_date: str, # 格式 YYYY-MM-DD min_amount: float # 最小金额,单位元 ) -> dict: """查询订单统计信息。""" if not _valid_date_range(start_date, end_date): return {"error": "日期格式错误或范围超过90天,请重新确认"} rows = db.query(...) return {"count": len(rows), "total": sum(r.amount for r in rows)}这里有几个细节值得说。第一,描述里写清楚边界,"仅支持最近90天"这句话必须写进 description,不能只写在代码注释里,因为模型看到的是 description。第二,参数的类型和格式要写在注释或 schema 里,日期格式不写清楚,模型很可能给你传"上周一"这种自然语言。第三,错误要以结构化形式返回,而不是抛异常。抛异常会中断循环,返回 error 字段则给了模型一次自我纠正的机会——这是提升成功率最廉价的手段之一。
注意:工具函数里绝对不要做"猜测式容错"。比如日期解析失败就默认取今天,这种隐式兜底会让模型误以为参数是对的,错误被掩盖到下游才爆出来,排查成本极高。
3.3 跑通第一个 ReAct 循环
工具就位之后,构建 Agent 并跑起来:
from agent_framework import Agent from agent.tools import query_order_stats, send_report agent = Agent( model="your-model-name", system_prompt=load_prompt("prompts/system.md"), tools=[query_order_stats, send_report], max_iterations=8, # 硬性轮次上限,必须有 temperature=0.1, # 工具调用场景建议低温 ) result = agent.run("帮我查一下上周金额超过5000的订单,统计一下总数") print(result.final_answer)max_iterations这个参数看起来不起眼,但它是我认为必须设置的第一道保险。没有它,遇到模型反复调用同一个工具却不收敛的情况,你的服务会一直烧钱到超时。我在测试环境故意构造过这种场景:工具一直返回空结果,模型就一直重试,八轮下来耗时四十多秒,成本是正常请求的六倍。设了上限之后,超限就直接降级返回"当前无法完成,请补充信息",用户体验反而更好。
temperature设低也是实践出来的。工具调用需要的是稳定的参数生成,不是创造性。温度高了,同一个问题两次调用可能给你两种参数格式,日志看起来会非常难受。
3.4 流式输出与中断处理
面向用户的场景基本都要流式,否则用户盯着空白屏幕等十几秒,体验很差。但流式遇上工具调用会有个尴尬期:模型决定调工具的那一瞬间,输出流是断的,然后要等工具返回。这时候前端如果什么都不显示,用户会以为卡死了。
我的处理办法是把 Agent 的运行状态也做成事件流推给前端:thinking、tool_call_start、tool_call_end、answer_delta。前端在工具调用期间显示"正在查询数据",用户就知道系统在干活。这个改动不大,但对体验的提升非常明显。框架一般会提供事件回调,你只要在钩子里把这些状态转发出去就行。
另外要处理的是中断。用户可能中途取消,这时候要确保工具调用能被取消、连接能释放。我见过因为没处理取消,导致工具里的数据库查询一直挂着,连接池被占满的事故。流式场景下,客户端的断开不一定能可靠传到服务端,所以工具本身也要设超时,双保险。
4. 让 Agent 从"能跑"到"跑得对"
4.1 工具描述就是提示词的一部分
很多人调 Agent 效果不好,第一反应是换模型、调温度、加 few-shot,其实大部分问题出在工具描述上。模型选择调用哪个工具,完全依赖 name 和 description 这两个字段。描述写得含糊,模型就只能猜。
我总结了几条写描述的规矩。一是动词开头,说清楚"做什么"而不是"是什么",比如"查询订单统计"比"订单统计工具"好。二是写清楚不适用的情况,"仅用于查询,不支持修改"这种否定描述能挡掉不少误调用。三是工具之间要有明显区分度,如果你有search_docs和find_documents两个工具,模型必然混淆,要么合并,要么在描述里写清楚各自的数据源。四是参数说明里给出示例值,尤其是枚举和格式敏感的参数。
这里有个反直觉的点:工具不是越多越好。我做过对比,工具数量从 5 个增加到 15 个之后,工具选择准确率明显下降。原因是模型要在更多选项里做判断,而且描述之间容易互相干扰。超过十个工具,我会考虑按场景分组,用路由先缩小范围,或者干脆拆成多个专职 Agent。
4.2 上下文管理与记忆分层
Agent 跑多轮之后,上下文会迅速膨胀。每一轮的工具返回结果都往里塞,几轮下来就可能顶到模型的上限。这时候框架一般会触发截断,但粗暴截断的后果是模型忘了最初的任务目标,开始胡来。
我的做法是把记忆分成三层。任务层放当前任务的原始诉求和关键约束,永不裁剪;中间层放工具返回的结果,超过一定长度就做摘要,只保留结构化要点;历史层放更早的对话,按时间衰减,必要时整段丢弃。实现上不用太复杂,一个简单的策略函数就够:判断消息类型,按优先级保留,超长内容走一次小模型摘要。
def trim_context(messages, budget_tokens): # 1. 永远保留系统提示和用户原始诉求 keep = [m for m in messages if m.role in ("system",) or m.pinned] # 2. 工具返回结果优先做摘要而非直接丢弃 rest = [summarize_tool_result(m) if m.role == "tool" else m for m in messages if m not in keep] # 3. 从旧到新填充,直到预算用尽 return fill_by_budget(keep, rest, budget_tokens)提示:摘要本身也要花 token 和时间。如果工具返回结果本来就不长(比如一个数字),直接保留比摘要更划算。别为了架构好看而引入不必要的开销。
4.3 多 Agent 编排:什么时候该拆
前面说过尽量别拆,但有些场景确实需要。判断标准是子任务之间是否有清晰的边界和独立的上下文需求。比如一个客服场景,查询订单和查询物流用的是完全不同的工具集和话术,硬塞给一个 Agent,工具列表长、系统提示词互相干扰,这时候拆成两个专职 Agent 再由一个调度者分发,是合理的。
拆的时候有两个关键点。第一,交接必须结构化。不要让 Agent A 把一大段自然语言甩给 Agent B,而是定义一个明确的交接结构:任务目标、已知信息、约束条件、期望输出格式。自然语言交接会导致信息在传递中失真,而且 B 无法判断哪些是事实哪些是 A 的推测。第二,必须设总预算。多 Agent 的成本是各环节之和,没有全局的轮次和 token 预算,很容易出现"两个 Agent 互相客气地推诿十几轮"的情况。
4.4 结构化输出与参数校验
Agent 的最终产出如果要对接下游系统,就必须是结构化的。我的做法是在系统提示里明确给出 JSON Schema,并且在框架层开启结构化输出约束(如果有的话),最后在代码里再做一次 schema 校验。三重保险看起来冗余,但生产环境里我确实遇到过模型输出多余字段、少字段、类型不对的情况。
校验失败怎么办?不要直接报错给用户。把校验错误信息作为一条新消息喂回模型,让它重试一次。实测下来,一次带错误提示的重试能解决大部分格式问题,成本也可控。第二次还失败,再降级处理。
5. 生产环境落地:难的是工程不是模型
5.1 超时、重试、熔断与幂等
上线之后你会发现,Agent 请求的失败模式比普通接口复杂得多,因为它包含多个外部依赖。我一般分三层设超时:单次模型调用超时(比如 30 秒)、单次工具调用超时(比如 5 秒)、整个会话总超时(比如 60 秒)。三层缺一不可。只设总超时,你无法知道是模型慢还是工具慢;只设单次超时,累积起来可能超出用户耐心。
重试要非常克制。模型调用可以重试,但要区分错误类型:限流和网络抖动可以退避重试,参数错误和内容违规重试没有意义。工具调用只对幂等操作重试,查询可以重试,扣款、发消息这类有副作用的操作绝对不能自动重试,否则会出现重复执行。真要做,就带上幂等键,让下游去重。
幂等键的设计很简单:session_id + tool_call_id。框架一般会给每次工具调用生成唯一 ID,拿它和会话 ID 拼起来传给下游,下游据此判重。这个字段不加,出事之后你对账会非常痛苦。
5.2 并发与成本:把账算在前面
Agent 的成本模型和普通接口不一样,因为一次请求可能触发多次模型调用。上线前必须估算清楚。我的估算公式大致是:
单次请求成本 ≈ 平均轮次 × (输入token × 输入单价 + 输出token × 输出单价) + 工具侧成本举个例子,假设平均 3 轮,每轮输入 2000 token、输出 300 token,输入单价 0.004 元/千 token,输出单价 0.012 元/千 token,那么单次成本约3 × (2×0.004 + 0.3×0.012) ≈ 0.035元。看着不多,但如果日请求十万次,一天就是三千多块。所以必须给每个请求设 token 预算上限,超过就强制收尾,返回当前已有结果。
并发方面,模型服务通常有自己的限流阈值。我一般用信号量在应用层做一道限流,再配合队列削峰。关键是别让请求无限排队,排队时间超过总超时就直接快速失败,否则用户等了一分钟拿到一个超时错误,体验比立刻失败还差。
5.3 可观测性:没有 trace 就别上线
Agent 的调试难度在于它是非确定性的,同样的输入两次跑出来的轨迹可能不同。所以你需要把每次运行的完整轨迹记录下来:每一轮的模型输入输出、调用了哪个工具、参数是什么、返回什么、耗时多少。这些信息汇总成一个 trace,按 request_id 串起来。
我关注的核心指标有这么几个:首 token 延迟(直接影响体感)、端到端 P95 耗时、平均轮次分布(突然变多说明有循环)、工具调用失败率、单请求 token 消耗分布(长尾往往是异常请求)、任务完成率。这几个指标里,轮次分布和 token 长尾是最早能发现问题的,我设了告警:轮次超过 6 的比例超过 5% 就去看日志。
日志里还要注意脱敏。工具参数里可能有手机号、身份证、金额,这些在写日志的时候必须过一遍脱敏规则。这个工作要在第一版就做进去,事后再补,你会发现在几千个日志点里找敏感字段是场灾难。
5.4 上线前的三类测试
我的验收清单里必跑三类测试。第一类是任务完成率测试,用真实场景构造 100 到 200 条评测集,看端到端成功率,低于阈值不放行。第二类是工具选择准确率测试,单独测"给定问题,模型选的工具对不对、参数对不对",这一类能把问题定位到具体工具。第三类是异常注入测试,故意让工具超时、返回错误、返回空、返回超长内容,看 Agent 能不能优雅处理,而不是崩掉或者死循环。
第三类最容易被跳过,但我认为最重要。线上出问题几乎都是异常路径,正常路径你在开发阶段已经跑烂了。异常注入不需要多复杂的工具,写个开关让工具按概率返回错误就行。
5.5 安全边界:工具就是权限
Agent 能调用的工具,本质上就是它的权限范围。这块必须收得很紧。工具白名单是底线,只注册必要的工具;参数校验在服务端做,不能信任模型给的任何值;权限要按最终用户身份透传,而不是用服务账号跑所有请求,否则一个越权查询就能把别人的数据带出来。
还有几件事要防:工具描述里不要写内部系统名和表结构,这些信息可能被诱导输出;返回给模型的数据要过滤敏感字段,模型无法区分"能看"和"能说";循环上限和成本熔断是最后一道闸,异常请求宁可失败也不能无限烧钱。这几条不是理论,是我在代码评审里反复强调的东西,因为它们出事的时候都不是小事故。
6. 踩坑实录与排查速查表
6.1 常见问题速查表
下面这些是我和团队实际遇到过、并且整理成清单的问题,遇到类似症状可以先对着查。
| 症状 | 最可能的原因 | 排查动作 | 处理办法 |
|---|---|---|---|
| 模型不调用工具,直接编答案 | 系统提示没强调必须查数据 | 看 prompt 和工具描述 | 明确写"涉及数据必须调用工具,禁止臆测" |
| 反复调用同一工具不收敛 | 工具返回空或错误信息不明确 | 看工具返回内容 | 返回结构化错误,加轮次上限 |
| 参数格式频繁出错 | 参数说明含糊 | 检查 schema 和描述 | 补格式示例,校验失败回传错误重试 |
| 上下文突然被截断导致跑偏 | 工具返回内容过长 | 统计每轮 token | 对长结果做摘要,保留任务目标 |
| 流式输出中途卡顿 | 工具调用期间无状态推送 | 看事件流 | 推送 thinking 和 tool_call 状态 |
| 同一问题结果差异大 | 温度过高或未固定随机种子 | 查 temperature | 工具场景降到 0.1 以下 |
| P95 延迟突然翻倍 | 某工具变慢或轮次变多 | 看 trace 和轮次分布 | 工具加超时,排查下游依赖 |
| 成本异常上涨 | 长尾请求轮次失控 | 看 token 消耗分布 | 设预算上限和成本告警 |
6.2 几个反直觉的实践经验
最后分享几条不太好写在文档里、但确实影响很大的经验。
第一条,提示词越短不一定越好,但越长一定越难维护。我见过系统提示写到三千字的项目,里面塞了大量边界情况和历史教训,结果是模型抓不住重点,而且每次改动都像拆炸弹。我的做法是把通用规则留在系统提示,场景相关的规则放到工具描述和具体任务提示里,分层管理。
第二条,别迷信"更聪明的模型能解决一切"。模型升级确实能带来提升,但很多问题出在工程层:工具返回格式不统一、错误信息不可读、上下文被截断。这些问题换模型解决不了。我做过对比,把工具返回统一成结构化格式、错误信息改成可读的中文,成功率提升比换模型还明显,而且成本没变。
第三条,给 Agent 一个"认输"的出口。早期我总想着让它尽力完成任务,结果它在信息不足时开始瞎猜。后来在系统提示里明确写:如果缺少关键信息,直接向用户提问,不要假设。这一条加上之后,无效轮次明显下降,用户满意度反而上升。承认做不到,比硬着头皮编一个答案强得多。
第四条,把评测集当成资产来养。每次线上出问题,我都会把那个 case 补进评测集。半年下来积累了两百多条,覆盖了各种边界。这套集子成了我们改提示词、换模型、升级框架时的回归基线,没有它,任何改动都只能靠感觉。
如果你正准备把 Agent 往生产推,我的建议是先在一个流程相对封闭、容错率高的场景里跑两周,把 trace、评测集、成本监控这三样东西建起来,再去碰核心业务。这三样东西建好之后,后面所有的优化才有依据,否则你只是在凭感觉调参。