我见过不少朋友第一次接触AI Agent时的反应:给ChatGPT提个问题,它能答;让它“帮我查一下明天的机票”,它也能一本正经地给出建议——但这并不是Agent,这只是聊天。真正的Agent,是让它自己决定“我要几步才能把事做完、每一步调用什么工具、拿到结果之后下一步怎么办、失败了怎么重试”,最后把一件多步骤的完整任务跑通。作为“ai-agent教程”系列的第二篇,这篇不讲花活,专注把两个问题讲透:Agent为什么非要这么设计,以及当前主流的Agent架构到底长什么样。
这篇内容适合两类人:一类是刚看完上一篇教程、想深入理解Agent内部机制的初学者;另一类是已经在做技术选型、需要在单Agent、多Agent、分布式架构之间做判断的开发者。我会从最底层的循环机制讲起,逐层拆解现代Agent的模块组成,再对比主流架构模式的优缺点,最后给一个从零搭建的最小可用Agent代码,带你完整走一遍真实调用链路。
1. Agent与LLM的分水岭:从“一句话回答”到“闭环执行”
1.1 普通对话与Agent任务的本质区别
很多人觉得Agent就是“给LLM套一层壳,让它能调用工具”,这话对了一半,但漏掉了最核心的东西:循环。
普通的LLM对话,本质是一次性的概率预测。你输入一段文本,模型根据训练学到的模式,预测下一个token最可能是什么,输出一段回答,结束。整个过程没有目标、没有状态、没有反馈,就像你问一位顾问“北京到上海怎么走最划算”,他给你一份方案,但你真正去执行时遇到航班取消、换乘失败,他并不知道,也无法帮你兜底。
Agent则完全不同。它围绕一个明确的目标,把任务拆成若干步骤,每个步骤都产生行动,行动之后观察环境反馈,根据反馈调整下一步。这个“行动-观察-再行动”的闭环,才是Agent的本质。这也是我反复跟团队强调的一句话:LLM是大脑,Agent是大脑加手和眼睛,通过循环把三者串起来。
用一个类比帮助理解:LLM像一个坐在办公室里只能看报告写建议的顾问,Agent则像一个项目经理。项目经理手里有目标、有进度表、有能调动的人手(工具),每做完一件事就检查结果,发现偏差立刻调整,直到项目交付或者确认无法完成。
1.2 Agent主循环里的“感知-推理-行动-观察”
几乎所有Agent架构,不管外面包了多复杂的框架,最底层都是下面这个循环:
- 感知:把当前的目标、历史对话、工具能力描述拼装成提示词,发给LLM。
- 推理:LLM根据当前信息决定下一步动作,可能是给最终答案,也可能是“我需要调用某个工具”。
- 行动:如果是调用工具,Agent执行对应的函数或API,拿到结果。
- 观察:把工具结果作为新消息追加回上下文。
- 回到第1步,继续下一轮推理,直到LLM认为任务已完成、达到最大迭代次数、或遇到无法恢复的错误。
这个循环在代码里通常长这样:
def run_agent(user_message: str, max_iterations: int = 5): messages = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message}] for i in range(max_iterations): # 1. 推理:把全部上下文交给LLM,让它决定下一步 response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=TOOLS, tool_choice="auto", ) message = response.choices[0].message messages.append(message) # 2. 如果LLM没有要求调用工具,说明任务完成 if not message.tool_calls: return message.content # 3. 行动:遍历所有工具调用请求,逐一执行 for tool_call in message.tool_calls: result = execute_tool(tool_call) # 4. 观察:把工具执行结果回填到上下文 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) # 5. 回到循环顶部,进行下一轮推理 return "达到最大迭代次数,任务未完成。"注意第2步的判断:LLM一旦不再请求工具,就意味着它认为可以给出最终答案了。这时候Agent退出循环,把一个完整的结果交还给用户。很多初学朋友写循环时会漏掉这个退出条件,结果Agent在“该停的时候不停”,不停地猜用户还要什么,白白消耗token。
1.3 ReAct为什么能减少幻觉
这个循环模式在学术界的名字叫ReAct——Reasoning and Acting,让模型在推理痕迹(Thought)和行动(Action)之间交替。你可能听过这个词,但不一定清楚它为什么有效。
关键在于:它把LLM的思考过程暴露在了工具反馈的真实结果之中。模型不再依赖自己“想象”出来的事实,而是每一步都有外部环境提供的硬数据。比如模型如果只靠内部知识回答“北京现在多少度”,它很可能给一个过时甚至编造的数字;但在ReAct模式下,它会先调用天气查询工具,拿到真实返回值,再基于这个值组织回答。这本质上是用外部世界约束模型的幻觉空间。
我在实测中一个很深的感受是:ReAct对工具结果能给出上下文极大的增强。原本模型容易在常识性问题上自信地胡说,可一旦工具返回“订单状态:已退款”,模型就只能基于这个事实继续,想编都编不圆。这也是为什么即使最简单的最小Agent,也明显比纯Prompt工程调出来的“伪Agent”可靠得多。
2. 拆开Agent看:核心LLM、规划、记忆与工具层
2.1 核心LLM只是“大脑”,不是全部
很多人以为Agent的价值全在模型,换个更强的模型,Agent就自动变强。真实情况是:模型决定的是Agent的“推理天花板”,而工程架构决定的是Agent能触到多高。哪怕是最强的商用模型,如果你不给它工具、不给它状态、不设计循环,它依然只是聊天机器人。
所以现代Agent实践里,核心LLM通常只负责两件事:一是根据当前上下文决定下一步动作,二是把若干工具结果整合成最终回答。其它的能力,都靠外部模块补齐。这也是为什么架构设计比选模型更值得投入时间——模型可以随时换,架构却决定了你的Agent能做多少事、能扛多复杂的任务。
我建议做选型的朋友不要陷入“只追最强模型”的误区。很多Agent场景(比如查单、填表、做简单分析)用gpt-4o-mini级别的模型就够了,成本只有旗舰模型的几十分之一,速度还快一倍。真正的瓶颈往往在工具设计、记忆策略和循环控制上,这些地方出了问题,换再大的模型也救不回来。
2.2 规划模块:拆解、编排与自我反思
当任务稍微复杂一点,比如“帮我分析这份文档,按月份生成销售趋势图,再写成一段摘要发给主管”,只靠一个简单的工具循环是不够的。Agent需要一个规划模块,把大任务拆成子任务,并确定子任务的执行顺序。
规划分两种实现路线:一种是完全让LLM在运行时动态规划——模型看到用户请求,自己决定分几步走,每步调用什么工具;另一种是开发者用代码预先定义好工作流,比如用LangGraph画一个状态机,明确“先取数、再分析、后生成”,LLM只在每个节点内部做决策。
两种路线各有适合的场景。动态规划灵活,能应对意外情况,但不可控——我见过模型把“查天气”的任务规划出四五步无关操作。预定义工作流稳定可控,但应对不了计划外的用户请求。成熟项目通常两者结合:主线流程用代码固定,异常分支和分支内的细节交给LLM自行决策。
规划模块还包含一个容易被忽略的能力:自我反思。就是在Agent执行完计划后,增加一步“审视自己刚才的输出,判断是否合理,不合理就修正”。业界有几套实现,比如LLM对自己的回答提出批评性意见再修改,或者用另一个模型当裁判打分。我建议在业务影响大的Agent上加上这层——它相当于给Agent装了一道质检,虽然增加一点延迟,但能明显降低输出被滥用的风险。
2.3 记忆系统:短期上下文、长期知识与工作区
记忆是Agent架构里最容易被低估的部分。聪明的读者可能已经发现,前面那个主循环之所以能工作,靠的就是把历史消息不断追加给LLM——这个上下文窗口就是Agent的短期记忆。但短期记忆有两个硬伤:一是窗口有限,装不下长时间跨度的信息;二是对话一断,记忆全丢。
所以真正的Agent系统必须区分三层记忆:
- 短期记忆:当前会话的上下文窗口,直接存消息数组。管理要点是控制长度,防止溢出。
- 长期记忆:跨会话保存的用户偏好、历史事实、业务知识。通常用向量数据库存embedding,需要时做相似度检索,这就是RAG的核心应用。打个比方,短期记忆像便利贴,长期记忆像档案柜。
- 工作记忆:当前任务执行过程中的中间状态,比如已经完成哪些步骤、正在处理哪份文件、下一步待办是什么。在代码层面就是一个状态对象,LangGraph里的State就是这个角色。
我自己的经验是:长期记忆的检索质量,直接决定Agent在真实场景里的可用度。两个Agent用同一套代码,检索做得好和做得差的,体验可以天差地别。需要花时间调切分粒度、embedding模型、相似度阈值和重排逻辑,这些属于RAG的范畴,但记住一句话:记忆系统不是“存进去再捞出来”那么简单的搬运,它是Agent能否在长期任务中保持一致性和个性化的地基。
2.4 工具层:从Function Calling到模型上下文协议
工具层是Agent的“手脚”,决定了它真正能对世界做什么:查数据库、发邮件、调第三方API、操作浏览器、执行代码……但工程上要解决的远不止“写一个函数然后让LLM调用”这么简单。
首先是工具定义问题。LLM并不知道你的函数长什么样,你需要用结构化描述告诉它:这个工具叫什么、做什么用、参数有哪些、参数类型是什么。以OpenAI兼容接口为例,工具描述是JSON Schema格式,名字、描述、参数缺一不可。我踩过的坑是:描述写得太笼统,比如“查询天气”,模型不知道该传“北京”还是“beijing”还是“Beijing, China”;写得太啰嗦,又白白消耗几百个token。好的工具描述要做到“让一个没见过这个函数的人也能准确调用”。
其次是工具注册和调度。Agent可能同时有几十个工具,你不能用一堆if-else硬编码。常见做法是维护一个工具注册表,每个工具注册时带上名称、处理函数、参数Schema,运行时根据LLM返回的函数名动态查找、执行、返回结果。这样新增一个工具只需要写一个函数加一行注册,主循环完全不用改。这个做法在成熟的Agent框架里已经是标配,LangChain的@tool装饰器、OpenAI的function calling、以及我正在用的简单注册表模式,本质都是这套逻辑。
第三层是工具互联协议。2024年底到2025年,行业内出现了一个很重要的趋势:把Agent和工具之间的关系标准化。Anthropic提出的模型上下文协议(MCP)就是典型代表,它把“工具接入”做成标准协议:数据源和工具服务通过MCP server暴露能力,Agent通过MCP client统一发现和调用。这意味着你为A项目写的一个数据库工具,理论上可以直接复用到B项目,不再需要为每个Agent重写适配层。如果你在做企业级Agent,这个方向值得重点关注。
这里整理一个模块对照表,方便做架构设计时快速查漏:
| 模块 | 核心职责 | 常见失控点 | 常用对策 |
|---|---|---|---|
| 核心LLM | 决策与生成 | 幻觉、决策漂移 | ReAct循环、外部工具反馈约束 |
| 规划模块 | 任务拆解与编排 | 规划步骤过多或过少 | 设置步骤上限、预置工作流兜底 |
| 记忆系统 | 短期上下文、长期知识、任务状态 | 上下文溢出、检索失效 | 摘要压缩、向量检索、状态快照 |
| 工具层 | 执行外部操作 | 参数错误、调用失败 | Schema校验、重试机制、注册表动态调度 |
3. 主流Agent架构模式:单兵、班组与流水线
3.1 单Agent的ReAct模式:最快上线但不抗复杂
目前最主流、最简单、也最容易被理解的Agent架构,就是去中心化的单Agent模式:一个Agent,一个LLM,若干工具,一个主循环。第1章的代码就是一个典型实现。
这种架构的优势是:逻辑直观、调试容易、token消耗相对可控、上线速度快。我做过不少内部工具,都是先用单Agent模式跑通,第一天就能让业务方看到效果。如果任务是单机单域,比如“查询订单状态并解释给用户”“从网页抓取信息并总结”“执行一个计算并把结果格式化”,单Agent是最理性的选择。
它的短板也很明显:一个人同时处理太多角色时,会顾此失彼。你把“数据采集”“数据清洗”“报告撰写”“质量审核”四个角色塞进同一个Agent,它的系统提示词会变得巨长,模型需要在多个职责间来回切换,容易角色混淆——前一轮还在严谨地核对数据,后一轮就开始放飞文风。这就是为什么更复杂的场景需要往多Agent方向走。
3.2 多Agent协作架构:跟手、裁决还是流水线
当一个任务确实跨界,需要多个专业角色配合时,单Agent就不够用了。多Agent架构把不同角色拆成独立Agent,每个Agent有自己的系统提示词、工具集和记忆,角色之间互相协作,像一个小型团队。
从协作方式看,常见的多Agent架构有三种:
第一种是“跟手/组织者模式”。一个主Agent(通常叫Orchestrator或Planner)负责理解用户意图,把任务拆解后分派给不同子Agent,每个子Agent完成任务后把结果汇报回来,由主Agent综合成最终答案。AutoGen里最常用的就是这种模式。这套架构的可控性最好,因为所有调度决策集中在一个Agent手里,其它Agent都是“执行者”。
第二种是“辩论/讨论模式”。多个Agent从不同视角或立场对同一个问题进行分析,比如一个扮演乐观派、一个扮演风险派,再有一个综合者把两边观点汇总成平衡的结论。这套模式在处理开放性问题、需要多角度验证的方案评审时效果很好,但token消耗也非常可观——每一轮讨论都是一次完整的多Agent调用。
第三种是“流水线模式”。任务严格按阶段递增,比如Agent A负责抓数据,Agent B负责清洗,Agent C负责建模,Agent D负责输出报告。每个Agent只吃前一个Agent的输出,它的输出又成为下一个Agent的输入。CrewAI里对这种链式协作封装得非常好,经常被用来做数据处理报告类任务。这套模式稳定性高、职责清晰,但一旦中间某个环节出错,错误会顺着流水线往下传,所以必须要在每个节点输出时做质量检查。
团队里我经常说一句话:多Agent不是目的,是手段。如果单Agent能解决问题,就别为了“架构看起来很高级”而拆成多Agent——每多一个Agent,你就多一份延迟、多一份token开销、多一个出错点。
3.3 分层与分布式架构:企业落地的MCP和网关模式
聊完“班组”,再聊聊企业级落地常见的分层与分布式架构。当Agent数量从几个涨到几十上百个,你会发现单靠多Agent组队也不够了,需要从团队管理升级到组织治理。
这时候的架构设计,参考的是微服务和分布式系统的老经验,只不过服务变成了Agent。典型的分层结构是这样:最上层是接入层,负责和用户产品交互;中间是编排层,由调度服务统一路由请求给合适的Agent;下层是执行层,各种专业Agent和工具服务;再往下是基础设施层,包括模型网关、向量数据库、任务队列、日志与监控系统。
这套架构里最关键的组件是智能体网关(Agent Gateway)。它有点像微服务时代的API网关:负责统一鉴权、限流、Agent路由、超时控制和审计日志。用户不需要知道具体是哪个Agent在处理请求,只需要面对网关暴露的一个稳定接口。这对企业来说意义重大:业务方可以随时升级、替换、下线某个Agent,外层完全无感。
另一个重要的基础设施是MCP(模型上下文协议)的服务化部署。在分层架构里,工具不再和Agent物理绑定,而是以MCP Server的形式独立部署,Agent通过MCP Client去发现和调用。这样工具层就变成了一个可以独立伸缩、单独维护的服务集群。比如你的团队有“查订单”“查库存”“发消息”三个工具,它们各自是一个MCP服务,订单Agent要用查订单工具时,通过MCP协议远程调用就行。
在分布式场景下,需求量大时还可以做Agent的多实例部署,用任务队列(比如RabbitMQ或Kafka)把请求分发到多个相同的Agent实例上并行处理。这里要提醒一件事:Agent是有状态的,多实例部署时要么确保每个实例之间的长期记忆共享(通常通过向量数据库实现),要么用粘性会话把同一用户的所有请求路由到同一个实例,否则用户会明显感觉到Agent“失忆”。
3.4 选型判断:别用复杂度换收益
最近市场上有大量多Agent框架和分布式架构的讨论,几乎每个平台都在推“新一代Agent编排框架”,很容易让人产生“不用多Agent就落伍了”的错觉。我的建议恰恰相反:从最简单的架构起步,用数据说话,只有当瓶颈真的出现时再升级。
| 架构模式 | 适用场景 | 优势 | 短板 | 代表实现 |
|---|---|---|---|---|
| 单Agent ReAct | 单一职责、工具数量少、任务链路短 | 简单直观、调试容易、成本低 | 多角色任务容易混淆、扩展性有限 | 手写主循环、LangChain |
| 多Agent协作 | 任务需要多视角交叉验证、专业分工明确 | 角色隔离、专业度高、可独立优化 | 延迟高、token开销大、协作逻辑复杂 | AutoGen、CrewAI |
| 分层/分布式 | 企业级多Agent规模化、多团队共享 | 治理能力强、工具复用、便于扩容 | 建设成本高、运维复杂 | 自研网关+MCP、LangGraph服务器化部署 |
我见过一些团队,业务还只有一个“查信息并总结”的需求,就强行上了多Agent加消息队列,结果运维成本超过了Agent本身带来的收益。反过来,也见过用单Agent硬扛几十个工具的,最后系统提示词写了两千行,每次请求光让模型读懂规则就花了大量token。架构选型的关键是匹配真实复杂度:工具少就单Agent,任务线多就拆多Agent,规模大到需要稳定性和治理能力再上分布式。
4. 手把手搭一个最小可用Agent:代码与实测
4.1 搭建目标和环境准备
理论讲了这么多,现在动手。我们目标很明确:搭一个能用自然语言完成任务的最小Agent,它的Toolbox里有两个工具——一个查天气(演示用静态映射),一个做四则运算(演示用解析计算)。用户用中文提问,Agent自行判断该调哪个工具,拿到结果后再组织自然语言回复。
环境方面,需要Python 3.10以上,安装openai库,并配置好API Key。我这里以OpenAI兼容接口为例,因为国内用兼容接口的人也很多,逻辑完全一样,只需要在代码里多传一个base_url参数指向你的服务网关。
pip install openai python-dotenv把API Key写入环境变量或在代码里用os.getenv读取。这里提醒一句:很多第一次做Agent的人会把密钥写死在代码里然后上传到公开仓库,这是非常危险的习惯,务必养成用环境变量或密钥管理服务存密钥的习惯。
4.2 先给Agent准备两个工具
工具定义分为两部分:一部分是给LLM看的“使用说明”(JSON Schema),另一部分是真正执行的Python函数。定义工具时要记得:描述一定要具体,参数要清晰,这样模型才不至于理解偏。
# 给LLM看的工具定义 TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气,包括温度和风力", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名,例如'北京'、'上海'"} }, "required": ["city"] } } }, { "type": "function", "function": { "name": "calculate", "description": "执行数学四则运算,自动处理加减乘除和括号", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式,例如'23*17'"} }, "required": ["expression"] } } } ] # 真正的执行函数 def get_weather(city: str) -> str: weather_map = { "北京": "晴,25℃,无持续风向微风", "上海": "多云,28℃,东南风3级", "广州": "雷阵雨,30℃,南风2级" } return weather_map.get(city, f"{city}暂无数据,请稍后再试") def calculate(expression: str) -> str: import re # 安全校验:只允许数字和基本运算符号,防止表达式注入 if not re.match(r"^[\d+\-*/(). ]+$", expression): return "表达式包含非法字符,仅支持数字和四则运算" try: result = eval(expression) return str(result) except Exception as e: return f"计算失败: {e}"这里特别说明一下为什么calculate要做字符白名单校验。因为Python的eval直接执行表达式的过程,如果有人传一个包含恶意代码的字符串(比如删文件的命令),后果会很严重。Agent工具本质上是一个“可以由外部输入触发的函数”,它必须有和Web API相同的安全意识:校验输入、限制权限、记录日志。这也是我反复强调的工具层防御思想。
4.3 用十几行代码实现Agent主循环
主循环的逻辑和第1章描述一致,这里直接给完整实现。这个循环足够支撑很多真实场景,也是理解通用Agent框架的一把钥匙——市面上的框架再复杂,本质都是这个东西的增强版。
from openai import OpenAI import json client = OpenAI() # 通过环境变量读取API Key def execute_tool(tool_call): """根据LLM返回的tool_call动态执行对应函数""" fn_name = tool_call.function.name args = json.loads(tool_call.function.arguments) if fn_name == "get_weather": return get_weather(args["city"]) elif fn_name == "calculate": return calculate(args["expression"]) return f"未知工具: {fn_name}" def run_agent(user_message: str, max_iterations: int = 5): messages = [ {"role": "system", "content": "你是一个能调用工具完成用户请求的AI助手。" "当用户需要天气信息时,调用get_weather;需要数学计算时,调用calculate。" "工具返回结果后,再根据结果组织简洁自然的最终回复。"}, {"role": "user", "content": user_message} ] for i in range(max_iterations): response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=TOOLS, tool_choice="auto", ) message = response.choices[0].message messages.append(message) if not message.tool_calls: return message.content # 模型不再要工具,任务完成 for tool_call in message.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) return "达到最大迭代次数,任务未完成,已安全终止。" if __name__ == "__main__": print(run_agent("北京天气怎么样?另外帮我算一下 23*17 等于多少。"))代码里最值得注意的细节有三处:
第一,message.tool_calls可能同时包含多个工具调用请求。LLM在上下文充足、工具定义清晰的情况下,有时会“聪明地”一次请求同时调用两个工具来提速。我们这里用循环逐一执行,并把每个结果都以role为“tool”的消息回填,带上对应的tool_call_id。tool_call_id是关联LLM请求和工具结果的关键字段,漏了它模型就不知道这个结果对应哪次调用。
第二,max_iterations必须设置。Agent像一把会自我迭代的锯子,如果没人喊停,它会一直锯下去。我在生产环境里默认设10,简单任务用5就够。设置上限不仅是为了防死循环,也是在防token成本失控——每一轮调用都是真金白银。
第三,tool_choice="auto"的含义是让模型自己决定是否调用工具、调用哪个。如果你想强制某个步骤必须用某个工具,也可以设置成tool_choice={"type": "function", "function": {"name": "get_weather"}},但一般别这么做,会破坏Agent的灵活性。
4.4 实测一轮完整调用链
我实际运行了这段代码,打开调试日志,能看到完整的调用链路:第一步,LLM收到用户请求“北京天气怎么样?另外帮我算一下 2317”,然后返回两个tool_calls——一个是get_weather,参数是“北京”;一个是calculate,参数是“2317”。第二步,Agent执行两个函数,拿到“晴,25℃,无持续风向微风”和“391”。第三步,把这两个结果回填给LLM。第四步,LLM组织出最终回答:“北京今天天气晴朗,气温25℃,微风。另外你问的23乘以17等于391。”
如果你自己跑一遍,可能会发现第一次请求的响应偶尔只调用其中一个工具,这是正常的——模型对工具的描述理解有细微差异,或者它选择“分两步走”。这种情况下一轮没做完,主循环会继续下一轮,让模型看到已有结果后补齐剩余动作。这正是循环的价值所在:它不要求一次完美,而是通过多轮反馈逼近正确结果。
4.5 三个值得调的关键参数
搭建完成后,我建议你把注意力放在三个参数上,它们对最终效果的影响远大于换模型:
第一个是温度(temperature)。默认0.7是通用推荐值,但Agent场景我建议设低一点,比如0.2到0.4。因为Agent的工作偏向“准确执行”而不是“创意发挥”,温度过高会让模型在决定“要不要调工具”时飘忽不定,出现“明明该查天气却自己编一个”的怪事。降低温度能让决策更稳定。
第二个是系统提示词里的工具使用说明。前面说过,工具的描述要具体但不啰嗦。实测下来,描述里明确给出“当出现xx关键词时调用xx工具”这种条件式指引,效果会明显好于只是罗列工具功能。因为模型在决策时走的是语义匹配,条件越清晰,匹配越准。
第三个是max_tokens(单次回答的最大token数)。Agent不像纯聊天,它每一步的回复要么是一次工具调用请求、要么是一段最终结果,都不需要太长。给模型设一个合理的单轮输出上限,能防止模型在中间步骤长篇大论解释自己的推理过程,白白消耗预算。我们这里没写,是因为OpenAI新版本接口会默认给一个值,但如果你用的是别的API或自建模型,这个参数必须手动控制。
5. 真正的坑都在工程侧:调试、记忆、成本与延迟
5.1 Agent行为不确定,调试要“录屏式”观测
做Agent和做传统程序最大的区别是:它跑一次一个样,同一个输入、同一个模型,两次结果可能不同。你很难用“打断点、看变量”的传统方式调试,因为问题往往出在“模型那一轮到底是怎么想的”。
我的实际做法是给Agent加“录屏式”日志:每轮循环都把完整的messages数组、模型返回的原始响应(包括tool_calls参数和tool_call_id)、工具执行结果、耗时,全部结构化落盘。这样无论Agent在线上出了什么问题,我都能事后“回放”它每一步的思考过程和动作,快速定位是工具描述不清、参数传错,还是模型决策策略跑偏了。
市面上有一些Agent可观测性工具,比如Langfuse、LangSmith、Phoenix,它们做的正是这件事:自动追踪每一步LLM调用、token消耗、工具调用链。如果你用的是自研框架,哪怕只用logging模块记录JSON日志,也要保证日志结构足够完整,别为了省事只记一句“调用了get_weather”——等到出问题时你会后悔的。
另外,复现问题也有技巧。遇到Agent行为不稳定,不要只把temperature设为0就以为万事大吉。更可靠的做法是固定输入、固定模型版本、固定temperature,同时记录模型返回时的seed参数(如果接口支持),这样能最大概率复现。但坦白说,Agent的随机性无法完全消除——这也是为什么生产环境里要给关键路径加上人工审核或规则兜底,而不是完全信任一次模型输出。
5.2 上下文窗口溢出:压缩与摘要策略
第二个坑是上下文窗口溢出。Agent每一轮都会把之前所有消息重新发给模型,轮数一多、工具结果一长,token数很快逼近窗口上限。你必须主动管理上下文,而不是等到报错再做处理。
常用的招数有三种:一是截断,只保留最近N条消息,把更早的历史直接丢掉。适合历史对当前任务无影响的场景。二是摘要,用一个轻量模型把老对话归纳成一段摘要,放进系统消息里,既保留关键信息又大幅压缩token。适合长期对话场景。三是关键信息提取,比如只保留用户的核心诉求、已完成的工具结果摘要、当前任务状态,丢弃无关寒暄。
我自己的默认策略是:普通任务保留最近10轮消息,超过就触发摘要压缩;当任务跨天或跨会话时,把长期记忆落到向量数据库,从“对话上下文依赖”切到“知识检索依赖”。这里提醒一句:压缩本质上是有损的,如果被压缩掉的内容对后续决策很关键,Agent就会“失忆”。所以压缩策略要配合工作记忆(明确记录已完成步骤和待办事项)一起用,把“过程的细节”丢掉、把“状态的骨架”留住。
5.3 成本与延迟失控:预算上限和并行调用
Agent的调用次数天然比普通LLM多,成本也被放大。一个用户请求,内部可能经历5到10次LLM调用,每次调用又附带工具结果,token消耗很容易是肉眼预估的3到5倍。控制成本是每个Agent项目必须做的功课。
监控报告是第一步。只要记录每次调用的模型、token数、工具名、耗时,你就能在一周内找出“成本黑洞”。我经历过的情况是:一个Agent在设计时看起来每个工具都很合理,但跑了两周一看明细,超过半数的token花在了“模型反复校验和犹豫”上——它每轮都在重新分析同一个上下文,迟迟不决定调用工具。这种问题靠降低温度、精简系统提示词、设置单轮输出上限就能优化掉大半。
延迟方面,最大的浪费来自串行等待。很多工具的调用是互相独立的,比如用户同时问“北京和上海的天气怎么样”,程序完全可以在同一轮里并行执行两个工具请求,而不是等第一个返回再发第二个。上一节的演示代码里,LLM如果想并行,会一次性返回多个tool_calls,我们用了for循环串行执行,虽然在示例里感觉不到差异,但在真实场景里,这个for循环换成并发执行可以把整段延迟降低一半以上。如果你用Python,可以改写成asyncio.gather统一执行所有工具调用,再统一回填结果。
还有一个小技巧是结果缓存。对天气查询、汇率、股票价格这类短时间内不会变化的数据,可以用一个简单的TTL缓存把最近的结果存下来,命中缓存就直接返回,不走工具、不用再用模型消化一次结果,既省token又省延迟。这个优化虽然简单,但回报率在Agent场景里非常可观。
5.4 工具调用失败的兜底逻辑
工具调用不是每次都成功的。网络超时、参数类型不对、第三方接口返回500、用户提供的城市名在数据源里不存在……这些失败如果处理不当,Agent会陷入两个极端:要么直接把错误原样抛给用户,让用户看一堆技术报错;要么用想象力补全一个“看起来正确”的结果,把人带进坑里。
正确的做法是设计“失败回传”。工具执行捕获异常后,不要抛出,而是把错误信息作为正常的tool消息回传给模型,让模型基于错误信息决定下一步:可以换一个参数重试,也可以转变策略用备选方案,还可以如实告诉用户失败原因。比如查询城市不在列表里,工具返回“该城市暂无数据”,模型看到后会回复“抱歉,目前数据库中还没有这个城市的信息”,而不是编一个温度数字给用户。这部分在第4章的代码里已经体现——get_weather用get方法设置了默认返回,calculate里也把异常转换成了字符串返回,这些都是失败兜底的雏形。
生产环境里还需要考虑重试策略。对瞬时性错误(网络超时、偶尔的5xx),可以重试一到两次;对确定性错误(参数非法、数据不存在),重试也没意义,直接让模型换路。另外,那些会产生外部副作用的工具(比如发邮件、转账、下单)必须格外小心:调用成功但网络返回超时,这种情况千万不能盲目重发,否则可能造成重复操作。这个场景下要做幂等控制,给每次调用生成一个唯一请求ID,让服务端去重。这是金融级Agent必须考虑的问题,虽然很多项目用不到,但只要你做的是“能对世界产生真实改变”的工具,就必须把副作用管理提上日程。
我在实际项目中还有一个体会:工具层的稳定性顶半边天。再聪明的Agent,如果工具的API时好时坏、参数定义含糊、返回结构不统一,整个系统的体验都会被拖垮。与其反复调整模型Prompt,不如把精力花在把工具定义做规范、把工具的容错和监控做扎实,这才是性价比最高的优化方向。