资讯动态

Agent核心循环:从API调用到多步决策的完整实现

发布时间:2026/10/20 17:27:23 来源:尧图企业网站定制
平时不少人问过我“我都能写接口调用了为什么一说到写 Agent 就感觉使不上劲” 这个问题我琢磨了很久答案其实就一句话你缺的不是调用 API 的能力而是缺少一个让模型“干活→拿结果→再干活”的循环。会调 API 是把单次对话交给模型会写 Agent 是把一整串任务的决策权交给模型这两件事之间差的就是标题里说的“这一个循环”。这篇文章会把这个循环掰开揉碎地讲清楚它为什么是 Agent 的骨架循环里每一步到底在做什么以及怎么用几十行代码把它从零手搓出来。不管你是刚接触大模型应用开发还是已经跟着框架文档写过几个 Demo这篇文章都能帮你把“Agent”从玄学变成可以一条条讲清楚的技术方案。1. 会调 API 与会写 Agent 的分水岭一个循环1.1 单次调用解决不了的事先想一个问题用户问“帮我查一下某城市现在的气温换算成华氏度再告诉我今天出门要不要带伞”。这个任务三个子问题互相依赖——不知道气温就没法算华氏度不知道天气状况就没法判断要不要带伞。如果你只写一次模型调用模型很可能直接回复“我无法获取实时天气信息”任务就断在这里了。这正是“会调 API”和“会写 Agent”的分水岭前者是“一次请求一次响应”后者是“一个任务多次请求每轮根据上一轮的结果决定下一步”。哪怕你已经把查天气的接口封装得清清楚楚只要没有循环模型就只是一个能聊天的接口壳子干不了需要多步推理和外部信息的活。1.2 循环是什么感知、思考、行动、观察我习惯把 Agent 的循环拆成四个步骤感知、思考、行动、观察。感知是把用户问题和历史对话整理成消息列表发给模型思考是模型根据上下文决定“现在该直接回答还是需要调用某个工具”行动是执行模型要求调用的工具函数拿到真实数据观察是把工具结果回传给模型让它在新的信息基础上继续推理。这四个步骤构成了一个 while 循环只要模型的决策是“调用工具”循环就不停一旦模型觉得信息够了输出最终答案循环就终止。这个模式在学术上有个朗朗上口的名字叫 ReActReasoning Acting翻译成人话就是“边想边干”。你不需要背什么高深理论只要记住Agent 的本质就是“把决定权交给模型然后在循环里替它跑腿拿数据”。1.3 与工作流的区别很多人会把 Agent 和工作流混为一谈这里必须分清。工作流是把步骤预先编排死先查天气再算华氏度再判断带伞每一步都是代码写死的顺序。Agent 则不一样模型自己决定“下一步调用哪个工具”甚至可以不按你预设的顺序走。同样是“查天气再算温度”的任务工作流是铁轨Agent 是司机——铁轨固定了路线司机可以根据路况临时换道。这个区别决定了选型方向。步骤固定、路径清晰的场景用工作流就够了又稳又省 token步骤不确定、需要模型临场发挥的场景比如“帮我安排明天一天的活动先看天气再看交通再看餐馆”才轮到 Agent 上场。我见过不少团队第一版就把所有任务都做成 Agent结果模型反复横跳成本还高得离谱——后来改成“固定流程走工作流开放场景走 Agent”效果反而好得多。2. Agent 循环的两个关键设计工具与终止条件2.1 把“工具”交给模型循环里最核心的接口设计是你给模型提供的“工具清单”。现在的 LLM 服务商基本都支持 function calling也就是在请求里附带一份结构化的工具列表模型只负责输出“我想调用哪个函数、参数是什么”真正的执行由你的代码完成。工具清单怎么写得专业直接影响 Agent 的可用性。每个工具要说明三件事函数名要一眼看懂description 要写清楚“什么时候该用这个工具”parameters 要把字段类型和取值范围标明白。举个例子查天气工具的参数不能只写“city: string”要写“city: string城市名称如‘北京’‘上海’不要带‘市’字”。模型读的是文字工具描述就是它的操作手册手册写得越清楚它就越少犯低级错误。2.2 让模型自己决定下一步循环能不能跑起来还取决于你怎么设置“决策开关”。一般 tool calling 接口都有类似“是否强制调用工具”的参数默认是 auto意思是让模型自己判断“这轮该调用工具还是直接回答”。这个默认值对大多数 Agent 场景都合适因为它把“下一步做什么”的自主权完全交给了模型。但有个坑要注意有些开发者为图省事把 tool_choice 设成“必须调用工具”结果模型被逼着每轮都调一次最后任务完成了还在空转。反过来如果你发现模型明明需要工具却不调用先别急着改设置大概率是工具描述写得太模糊模型压根不知道有这号工具可用。设置只是兜底工具描述才是决定模型行为的主要因素。2.3 循环停在哪里循环不能永远跑下去得有终止条件。终止条件有两个一个自然发生模型不再返回工具调用输出一段最终回答另一个是程序兜底设一个最大迭代次数比如 8 轮超过就直接返回“任务未完成”。最大迭代次数怎么定我建议看任务复杂度。单工具能解决的简单问题5 轮足够涉及多工具依赖的复杂任务10 轮左右。不要为了省 token 设成 3 轮我试过模型稍微多思考一步就被强砍体验极差。也不要设得太高比如 20 轮以上模型如果一直在调工具不收敛每一轮都在烧钱。8 是一个比较稳的起步值跑一段时间后再根据日志调整。另外判断“模型是不是在空转”有一个经验信号同一轮调用出现重复的工具名、参数来回变但不收敛就要考虑在循环里加一个“如果和上一轮完全重复就中断”的检测。3. 从零手搓一个最小 Agent 循环3.1 准备工作接口与工具函数接入 Agent 循环之前你需要一个大模型接口。现在各家服务商大多都支持 tool calling接口风格大同小异核心都是“传入消息列表和工具清单返回模型决策结果”。下面代码里的llm_client.chat(messages, tools)就是这一调用的抽象你换成自己接入的服务即可。接下来准备两个示例工具一个查天气一个做四则运算。查天气为了方便演示直接用写死的假数据真实项目里替换成对应的天气服务接口即可计算器我用正则限制了字符范围只允许数字和四则运算符号避免任何注入风险。import json # ---- 工具函数 ---- def get_weather(city: str): # 演示用假数据真实项目替换为天气服务接口 fake_data { 北京: {temperature: 26, condition: 晴}, 上海: {temperature: 30, condition: 多云}, 广州: {temperature: 33, condition: 阵雨}, } data fake_data.get(city) if not data: return {error: f未找到城市 {city} 的天气数据} return {city: city, **data} def calculator(expression: str): import re # 只允许数字、四则运算符、括号和空格 if not re.fullmatch(r[0-9\-*/(). ], expression): return {error: 表达式包含非法字符} try: return {result: eval(expression)} # 仅用于白名单校验后的计算 except Exception as e: return {error: f计算失败: {e}} # ---- 工具清单发给模型的结构化描述 ---- TOOLS [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气返回温度摄氏度和天气状况, parameters: { type: object, properties: { city: {type: string, description: 城市名称如北京不要带市字} }, required: [city] } } }, { type: function, function: { name: calculator, description: 执行四则运算表达式返回数值结果, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式如 (26*9/5)32} }, required: [expression] } } } ]3.2 核心循环代码下面这段就是 Agent 的心脏。整个循环的逻辑其实很简单把消息发给模型如果模型要求调用工具就执行工具并追问如果模型给出了最终回答就返回。注意两个容易写错的细节带工具调用的模型消息必须完整放回消息列表工具执行结果要附带对应的调用 ID。# 这里替换为你实际接入的大模型服务 def llm_chat(messages, toolsNone): # 调用你的模型服务返回对象包含 # content: 模型生成的自然语言内容 # tool_calls: 模型请求的工具调用列表每项有 id, function.name, function.arguments pass SYSTEM_PROMPT ( 你是一个能调用工具的智能助手。 当用户的问题需要外部数据或计算时请调用合适的工具来获取结果 当所有信息都已齐备时停止调用工具直接用中文给用户完整、易读的最终回答。 ) TOOL_FUNCTIONS { get_weather: get_weather, calculator: calculator, } def execute_tool(name: str, args: dict): if name not in TOOL_FUNCTIONS: return {error: f未知工具: {name}} return TOOL_FUNCTIONS[name](**args) def run_agent(user_input: str, max_iterations: int 8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for i in range(max_iterations): message llm_chat(messages, toolsTOOLS) # 模型决定调用工具 if message.tool_calls: # 先把模型的工具调用消息放进历史再逐条执行 messages.append({ role: assistant, content: message.content, tool_calls: [ { id: tc.id, type: function, function: {name: tc.function.name, arguments: tc.function.arguments} } for tc in message.tool_calls ] }) for tc in message.tool_calls: try: args json.loads(tc.function.arguments) result execute_tool(tc.function.name, args) except Exception as e: result {error: str(e)} print(f[调试] 第 {i1} 轮调用工具: {tc.function.name}({tc.function.arguments})) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) continue # 模型不再调用工具视为最终回答 return message.content return f达到最大迭代次数 {max_iterations}任务未完成请检查工具调用是否合理。 if __name__ __main__: print(run_agent(北京现在多少摄氏度换算成华氏度是多少今天出门需要带伞吗))3.3 代码逐段解读这段代码最关键的不是“调用了模型”而是消息列表的组装方式。模型每次决策都依赖完整的历史所以必须把每一轮的工具调用请求都追加进messages。这里有个很多新手会犯的错拿到模型返回后不保存原始消息只把工具执行结果拼成普通文本塞回去接口直接报错因为你破坏了 tool calling 的回话格式要求。工具结果的content我用json.dumps序列化尽量保留结构化数据。模型读 JSON 比读散装的“温度26度天气晴”更容易对齐字段。如果你发现模型在后面的轮次里把工具结果里的数字看错优先检查是不是 content 格式不够结构化。另外tool 消息里的tool_call_id必须和模型返回的调用 ID 一一对应这个 ID 是模型用来对号的错一个接口就会拒绝。3.4 一个完整运行示例拿代码跑一遍“北京现在多少摄氏度换算成华氏度是多少今天出门需要带伞吗”任务你会看到这样的执行轨迹[调试] 第 1 轮调用工具: get_weather({city: 北京}) [调试] 第 2 轮调用工具: calculator({expression: (26*9/5)32})第一轮模型查天气拿到“温度 26 摄氏度、晴天”第二轮模型把 26 代入华氏度公式算出结果第三轮模型发现天气是晴天不再调用任何工具直接输出“北京当前 26 摄氏度华氏度天气晴朗不需要带伞”。整个过程没有任何一步是代码预先写死的全凭模型在循环里自己串联——这就是“这一个循环”的威力。4. 把循环跑稳的实战经验4.1 工具描述决定 Agent 的上限同一个循环换一套工具描述效果天差地别。我给这个系列总结了一条经验工具描述不是写给代码看的是写给模型看的。描述里要包含三层信息——这个工具解决什么问题、什么时候该用、参数怎么填才合法。比如计算器工具的 description 里我特意写“执行四则运算表达式返回数值结果”后面接一个参数示例(26*9/5)32模型看到示例就知道该怎么格式化表达式瞎传字符串的概率大幅下降。另一个经验是工具命名要符合直觉。get_weather比query_weather_info_v2好使calculator比math_eval好使。模型对语义清晰的函数名有天然的偏好命名越像真实 API它理解得越快。说到底模型是用自然语言理解工具列表的你给它看一堆a1_b2_c3这种函数名它大概率会选择性忽略。4.2 上下文膨胀与 token 管理循环轮次多了上下文必然膨胀。假设每个工具结果 200 token8 轮就是 1600再加上系统提示、用户问题和历史回话三四千 token 轻轻松松。在长任务里上下文超长还会影响模型对早期信息的注意力它可能忘了用户最开始提的约束开始自作主张。这里我建议两条路。轻量方案是控制工具结果的大小能返回摘要绝不返回原始数据能返回 5 条绝不返回 50 条。进阶方案是给循环加“历史压缩”当消息数超过阈值时把前面的消息交给模型做一次总结用一段摘要替换掉原始历史。需要注意执行 history 压缩时被压缩的消息里如果包含 tool_calls必须小心处理不能把不完整的历史片段留给模型——我踩过这个坑压缩后模型开始胡言乱语就是因为丢了工具调用的对应关系。4.3 异常与空转识别循环里最难受的问题不是报错而是“模型在瞎转”。工具返回了错误模型不换思路反复用同样的参数调用同一个工具或者模型已经拿到了答案但还是继续调用工具。针对前者我在execute_tool外面包了一层异常捕获把错误信息作为正常文本回传给模型让它自己读取错误并对症下药。针对后者我建议加一个重复调用检测记录上一轮的工具名和参数如果本轮完全一致直接中断并把上一轮的结果当作最终可用数据同时提示“检测到重复调用已终止循环”。4.4 常见问题速查表症状可能原因处理办法模型不调用工具直接硬答工具描述不清模型不知道有工具可用检查 TOOLS 定义把描述写成“何时该用”模型反复调用同一工具不收敛缺少终止指令或上下文里已有答案但模型没发现system prompt 增加“信息足够就停止”并加重复调用检测接口报错 tool_call_id 不匹配消息顺序错乱assistant 工具调用消息未完整保留先追加 assistant 原始消息再追加 tool 结果工具参数解析失败模型生成的 JSON 不合法或字段名不符try/except 捕获把“解析失败”返回给模型重试上下文超长报错循环轮数太多工具结果太大限制 max_iterations精简工具返回必要时做历史压缩5. 一点个人体会这个循环跑通之后你会突然发现以前觉得“框架好神秘”的东西其实就那么回事Agent 不是一个大型软件它就是 while 模型接口 工具函数这三块的组合。真要说有什么门槛不是代码怎么写而是你怎么替模型考虑——它的决策依赖什么信息、它会不会被模糊的描述带偏、它需要什么止损机制。我自己带人做这个系列时第一步都要求他们把工具描述逐字逐句写清楚写完了再谈其他优化。这步做好了后面加记忆、加规划、加多工具协作都会顺手很多下一篇系列我们就顺着这条路继续往下走。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑