ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从对话到干活:LLM自主智能体的ReAct循环与工具调用实战

从对话到干活:LLM自主智能体的ReAct循环与工具调用实战 读 Hello Agents 前三章的时候我心里一直有个疑问这些代码不就是在调大模型 API 吗把 temperature 调低、把 prompt 写长和普通聊天机器人有什么区别直到翻到第四章我才意识到自己把 Agent 想简单了。第四章的主题非常明确——从你问我答的单轮对话跨到把一个任务完整跑完的自主循环agent loop也就是让 LLM 驱动的自主智能体LLM powered autonomous agents真正长出手脚。这一章信息量很大涉及 ReAct 模式、工具调用、OpenAI Agents API 的用法还附了一个可以照着抄的 Demo 骨架。这篇笔记不是复述教程而是我边学边补的全过程哪些地方教程一句带过但我实际踩了坑哪些设计一开始没看懂后来想通了。无论你跟我一样刚学完前三章还是只想快速了解 agent 项目怎么落地这篇都能给你一个可对照的坐标。1. 第四章到底讲了什么从会对话到能干活的关键一跃1.1 普通对话与 Agent 循环的本质区别前三章的用法基本上都是输入一段话大模型回一段话。这种模式再花哨本质上还是文本生成你说帮我写个摘要模型就给你输出一段摘要它并不知道摘要这两个字背后需要去读哪篇文章、文件在哪里。第四章把这个问题拆开了如果你想让它真正干活就不能只靠一次模型调用而是要让模型能够调用外部工具、拿到真实结果、再根据结果决定下一步动作并且循环这个过程直到任务完成。打个比方普通对话是你拿着地图问路Agent 是你雇了一个会开车的司机——他需要自己看路牌、踩油门、打方向盘遇到堵车还要自己换路线。这个转变背后是一个很朴素的事实模型的知识停留在训练时的那一刻它不知道今天的天气、不知道你的日程、更没法替你发邮件。想要 Agent 完成真实世界里的任务就必须给它接上手——也就是工具调用function calling / tool use。1.2 Agent loop 的四步套路第四章里反复出现的一个循环业界通常叫 ReAct也就是 Reasoning推理 Acting行动。它的套路可以用四步概括思考Thought模型根据当前对话状态判断我现在需要什么信息才能推进任务。行动Action模型选择一个工具并给出参数比如调用get_weather(city北京)。观察Observation系统执行工具后把真实返回结果拼回对话里再交给模型。重复上面的过程直到模型认为不需要再调用工具直接给出最终回答或者达到设定的最大步数强制结束。这个循环最反直觉的地方是模型每一步的思考过程和工具调用意图其实都是用自然语言或结构化指令表达的。你不需要写一堆 if-else 来判断用户问天气时就调天气 API——模型自己会学会在什么场景下调用什么工具你只需要把工具清单给它。1.3 最小可运行的 ReAct 循环代码我自己照着第四章的思路写了一个最小版本去掉所有装饰大概长这样import json import openai client openai.OpenAI() WEATHER_TOOL { type: function, function: { name: get_weather, description: 查询指定城市的实时天气, parameters: { type: object, properties: { city: {type: string, description: 城市名如北京} }, required: [city] } } } def run_agent(user_query, max_steps5): messages [ {role: system, content: 你是一个能调用工具的助手任务完成后直接用中文回答。}, {role: user, content: user_query}, ] for step in range(max_steps): response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, tools[WEATHER_TOOL], ) message response.choices[0].message # 模型没有要求调用工具说明可以给出最终回答了 if not message.tool_calls: print(最终回答:, message.content) return # 把模型的工具调用意图追加进对话 messages.append(message) # 逐个执行模型请求的工具调用并把结果拼回去 for tool_call in message.tool_calls: args json.loads(tool_call.function.arguments) if tool_call.function.name get_weather: observation f{args[city]}晴25℃东南风2级 else: observation 未知工具 messages.append({ role: tool, tool_call_id: tool_call.id, content: observation, }) run_agent(北京今天适合出门跑步吗)第一次跑通这个代码的时候我最大的感受是模型居然会自己判断我需要天气数据才能回答跑步建议然后主动发起一次get_weather(city北京)调用拿到结果后再回来组织语言。整个过程里我只给了工具清单没有写一条判断逻辑。这就是 ReAct 的核心价值——把决策权交给模型把执行力留给代码。2. 工具调用Agent 的手是怎么长出来的2.1 工具定义三段式名称、描述、参数 Schema学完第四章我意识到工具定义本身才是 Agent 工程里最需要花心思的部分。一个工具的长相基本是固定的三段式名称name给模型的唯一标识最好用动词开头比如create_calendar_event、send_email不要用含糊的do_stuff。描述description告诉模型这个工具是干什么的、什么时候该用、什么时候不该用。参数 Schema声明这个工具有哪些参数、每个参数的类型和含义模型会照着这个 schema 生成 JSON 参数。下面的例子比较典型{ type: function, function: { name: create_calendar_event, description: 在日程系统中创建一个新的日程事件。仅当用户明确要求安排某时间的事件时使用。, parameters: { type: object, properties: { title: {type: string, description: 日程标题}, start_time: {type: string, description: 开始时间ISO 8601 格式}, duration_minutes: {type: integer, description: 持续分钟数默认 60} }, required: [title, start_time] } } }有个细节我一开始没注意参数的默认值不要写在代码里要写在 schema 的 description 里。因为模型是根据 schema 生成参数 JSON 的它不会去看你的 Python 函数默认值。如果想让某个参数可选最稳妥的做法是既在 schema 里标注非必填又在 description 里说明默认行为。2.2 描述写得好不好直接决定工具会不会被用错我自己踩过一个很典型的坑。最初我给一个查询用户订单工具写的描述是查询订单信息结果模型在用户问我最近买了什么的时候调用了它参数却传了user_idnull。后来我把描述改成查询指定用户的订单列表。调用前必须先从对话中提取用户 ID若对话中未出现用户 ID请先调用 find_user 工具获取 ID。效果立刻不一样了。这说明工具描述其实是在教模型什么时候用、怎么凑参数。描述里如果能包含以下内容会更好触发条件什么场景该用、什么场景不该用。参数来源每个参数从哪来是对话里直接有还是需要先调用另一个工具。一个示例比如例如get_user_orders(user_idu_123)。可以把工具描述理解为给新同事写的交接文档——写清楚了他才知道怎么干活写得太抽象他就只能瞎猜。2.3 手写循环和 OpenAI Agents API 怎么选第四章里还介绍了用官方 SDK 的更高层封装来写 Agent也就是openai-agents这套 API。和手写循环相比它把工具注册、参数解析、循环调度全部封装成了装饰器和 Runner代码量少很多from agents import Agent, Runner, function_tool function_tool def get_weather(city: str) - str: 查询指定城市的当前天气。city 必须是中文城市名如北京。 return f{city}晴25℃ agent Agent( nameWeatherAssistant, instructions你是天气助手查询到天气后直接用中文回答用户。, tools[get_weather], ) result Runner.run_sync(agent, 上海热吗) print(result.final_output)这里的机制很有意思function_tool装饰器会把 Python 函数转换成合法的工具 schema——函数名变成工具名docstring 变成描述带类型标注的参数变成参数 schema。也就是说你用普通 Python 函数写工具SDK 自动帮你生成 JSON Schema 和调用链。我的建议是手写循环适合学习阶段和需要完全掌控每一步的场景比如你要在循环里加自定义的记忆管理、调试日志、特殊终止逻辑而 OpenAI Agents API 适合快速搭原型和生产环境它能减少大量样板代码。第四章的顺序是先讲手写再介绍 API我觉得这个安排是合理的因为不懂底层循环的话用高层封装出问题都不知道去哪排查。3. 一个能照抄的个人日程助手 Demo从拆解到跑通3.1 需求拆解与工具设计学完循环之后我决定脱离教程自己做一个 Demo验证能不能让它真正帮我安排日程。需求定为三个核心能力查看当前时间、查看已有的日程、在日程里插入新事件。工具设计如下工具名作用关键参数get_current_time获取当前日期和时间无参数get_schedule查询某天的日程列表dateYYYY-MM-DDcreate_event在指定时间创建日程title,start_time,end_time为什么这样设计因为帮我安排下午三点的会这句话里藏着多个隐含步骤Agent 首先得知道下午三点对应今天的几点所以要get_current_time然后要确认那个时间段是否有空所以要get_schedule最后确认空闲了才create_event。这个链路是模型自己推理出来的不是我写死的这就是 Agent 和传统对话机器人意图识别的本质差别。3.2 代码骨架与关键实现我用的还是openai-agentsSDK项目结构很简单schedule-demo/ ├── main.py ├── tools.py └── requirements.txttools.py里用function_tool定义三个工具from datetime import datetime, timedelta from agents import function_tool function_tool def get_current_time() - str: 获取当前日期和时间格式为 YYYY-MM-DD HH:MM。 return datetime.now().strftime(%Y-%m-%d %H:%M) function_tool def get_schedule(date: str) - str: 查询指定日期的日程列表。date 格式为 YYYY-MM-DD demo_events { 2025-01-10: [10:00-11:00 产品评审, 14:00-15:00 客户沟通], } return \n.join(demo_events.get(date, [今天暂无安排])) function_tool def create_event(title: str, start_time: str, end_time: str) - str: 创建日程事件。title 为事件标题start_time/end_time 为 ISO 8601 格式。 return f已创建日程{title}{start_time} 至 {end_time}main.py里把三个工具挂到 Agent 上from agents import Agent, Runner from tools import get_current_time, get_schedule, create_event agent Agent( nameScheduleAssistant, instructions( 你是个人日程助手。安排日程时必须先获取当前时间再查询用户指定的日期是否有空 确认没有冲突后才能创建日程。所有事件都使用以下默认时区。 ), tools[get_current_time, get_schedule, create_event], ) result Runner.run_sync(agent, 帮我在今天下午 3 点安排一个 30 分钟的产品评审) print(result.final_output)跑一次就能看到完整的推理链路先调get_current_time确认今天是哪天再调get_schedule查当天下午有没有安排确认空闲后再调create_event创建日程。三个工具调用被模型在后台自动编排我一行 if-else 都没写。3.3 跑通时的三个容易忽略的环境细节第一次跑这个 Demo 时我花了半小时在环境上记录一下给后来者第一API Key 设置。SDK 默认从环境变量OPENAI_API_KEY读取密钥用export OPENAI_API_KEY...设置即可不要把密钥写进代码里。我自己图省事硬编码后来推代码的时候差点传上去这是坏习惯。第二模型选择。用gpt-4o-mini这类模型跑 Demo 足够工具调用能力稳定且便宜。但注意模型的工具调用能力是有差异的后面有机会我专门测了不同模型在同一套工具上的成功率差别比想象中大生产环境选模型一定要先拿自己的工具集跑一遍。第三一定要开启中间过程调试。我在每个工具执行前都加了一行print打印参数这一行输出让我直观看到模型传给工具的 JSON 到底长什么样。很多 Agent 问题比如参数传错了、格式不对都是靠这种打印迅速定位的。这个习惯我保留到了现在。4. 调试实录Agent 循环里的四类翻车现场4.1 死循环Agent 停不下来了我遇到的第一个问题是死循环。场景是这样的我让 Agent把今天的日程整理成一段文字发我结果它反复调用get_schedule和get_current_time却始终不输出最终回答。排查过程不是直接看代码——先看打印出来的中间消息模型每轮都问让我再确认一下今天的日程调完工具之后又觉得还需要再次确认。根因有两个。第一个是没有在系统提示里给出明确的终止条件我只写了你是日程助手没写当你已经获得足够信息时必须直接回答用户不要再调用工具。第二个是模型把查询日程当作了一个可以无限刷新的动作缺少一种够了的信号。修复方式是双管齐下一是在 instructions 里补上终止规则二是在Runner.run_sync的外层包一个最大步数限制比如超过 8 步直接报错或退化为最后一轮回答。实际项目中绝不能无限循环尤其是生产环境一步不控制token 成本就爆了。4.2 工具报错被当成正常结果喂回模型第二个坑更隐蔽。我故意让get_schedule在日期格式不对时抛异常结果发现 Agent 不但没有察觉异常反而一本正经地回答该日期的日程查询失败建议您稍后再试。它甚至把报错信息当成了查询结果本身来理解。这里的关键认知是工具返回的字符串会原封不动地进入对话上下文模型没有对报错的强敏感度。如果你在工具里抛 Python 异常且这个异常没有被捕获消息要么是空的要么是一段堆栈信息模型反而更懵。我的修法是在所有工具函数入口包一层 try-except把异常转换成明确的观察文本function_tool def get_schedule(date: str) - str: try: # 正常逻辑 ... except Exception: return f错误查询日程失败日期 {date} 可能格式不正确请使用 YYYY-MM-DD 格式后重试。这样模型就能从观察里得到这次调用失败了、失败原因是什么、接下来该怎么办的信息。永远记住对 Agent 来说工具返回的任何字符串都是事实你要保证事实的质量。4.3 参数靠猜不靠查第三个问题是模型在工具参数上偷懒。我的 Agent 需要按用户 ID 查订单但用户提问时说的是我上个月买的耳机发货了没对话里根本没有用户 ID。结果模型直接猜测了一个user_idu_123传进去返回了一堆错误订单。排查之后发现问题出在工具描述上。我当时只写了查询某用户的订单状态没有告诉模型如果对话中缺少用户 ID必须先调用 find_user 工具通过用户名或邮箱查找。这个教训在前面 2.2 里已经写过——描述是模型的操作手册。补上参数来源规则之后模型会老老实实先去查 ID 再查订单准确率高了不少。4.4 Token 悄悄烧完的教训最后一个翻车现场是成本层面的。我把单次任务的上下文积累到几十条消息之后每次调用都要把所有历史工具调用结果重新发给模型token 消耗成倍上涨一次复杂任务跑下来成本比我想象中高一个数量级。这背后的逻辑是Agent 循环的每一轮都是一次完整的大模型请求消息列表只增不减所有历史记录都会重复计费。我后来专门给循环加了长度监控当历史消息超过一定阈值时对早期工具调用做摘要压缩只保留最终结论。这类优化第四章里只是一笔带过但实际操作时非常关键。把四类问题整理成一张速查表方便复盘问题典型现象根因修复思路死循环反复调用同一工具缺少终止条件增加终止规则 最大步数硬限制报错被当结果Agent 把异常当正常输出异常未转成清晰观察文本工具内部 try-except 返回错误说明参数靠猜瞎传 ID/日期描述里缺参数来源描述中说明参数获取流程token 飞涨请求越来越大历史消息无限累积摘要压缩 长度阈值监控5. 记忆问题教程没细讲但实操一定会遇到的坑5.1 上下文窗口是硬约束第四章讲完循环和工具调用后我自然想到一个问题如果用户说帮我记一下以后安排会议默认在周三下午Agent 怎么把这个偏好留到下一次会话这在传统程序里是一次数据库写入在 Agent 里却牵扯到上下文窗口这个硬约束。Agent 不是没有记忆而是它的记忆只存在于当前会话的消息列表里。一次会话长度受模型上下文窗口限制即便像gpt-4o-mini这样能装下不少 token也无限制地累积业务信息。更现实的问题是跨会话时一切历史都消失了。最简单的方案是把记忆拆成两层短期记忆当前会话的所有消息直接留在 messages 里。长期记忆关键事实和偏好用摘要或结构化数据存到外部比如本地 JSON 文件、Redis、数据库。5.2 一个轻量的结构化记忆方案第四章之后我给自己做了一个很小的记忆模块每次对话结束时单独用一次模型调用提取用户偏好和待办事项写进一个 JSON 文件下次会话开始时再把这个 JSON 注入系统提示。{ user_preferences: { meeting_day: 周三下午, timezone: Asia/Shanghai }, pending_items: [ 周五前给客户发报价单 ] }注入方式也很简单把 JSON 转成文本放在 system prompt 末尾即可memory_text json.dumps(memory, ensure_asciiFalse) agent Agent( nameScheduleAssistant, instructionsf以下是用户的长期记忆回答时请遵守\n{memory_text}, tools[...], )这套方案没引入任何重型组件效果却立竿见影。它告诉我一个道理不要把记忆想得太玄乎本质就是哪部分信息需要在每轮请求里带着走。5.3 什么时候才值得上向量库我也认真评估过向量数据库最后的结论是只有当你的业务确实需要按语义相似度召回历史片段时才值得上。比如用户说之前我提过关于出差报销的想法而你需要在几千条历史记录里找到大部分匹配但措辞不同的内容这时候用 embedding 向量召回才有意义。如果只是固定偏好、固定配置、最近 N 轮对话摘要用 JSON 文件或 Redis 反而更简单可靠。向量库会带来额外的数据同步、索引更新、召回质量评估成本对刚起步的项目并不友好。先把结构化记忆做好等到真正有语义检索需求再引入向量库这个顺序我是亲测有效的。6. 从第四章往外看多智能体、移动端与语音 Agent6.1 多智能体协作的三种常见模式第四章的背景设定里已经提到单个 Agent 能做的事情有限真实项目往往需要多个 Agent 协作。我自己梳理下来常见的协作模式有三种主从模式Orchestrator-Worker一个主 Agent 负责任务拆解把子任务分发给多个工作 Agent最后汇总结果。适合写一份周报需要分别统计代码提交、收集客户反馈、汇总指标这种需要多个专业能力的任务。流水线模式Pipeline任务按顺序经过多个 Agent每个 Agent 只处理一段比如翻译 Agent → 校验 Agent → 润色 Agent。辩论/评审模式多个 Agent 针对同一个任务给出独立判断再互相评审选取最优方案适合需要质量把关的场景。多 Agent 不是免费的它的直接代价是 token 成倍增长、调试复杂度指数上升。我的建议是能单 Agent 解决的尽量单 Agent只有当子任务需要不同指令、不同工具、不同提示词时才拆。6.2 移动端与桌面端 Agent 的新动向学完第四章后再去看最新的 Agent 项目会发现一个明显趋势越来越多的 Agent 正在从纯文本接口走向直接操作应用界面。最近看到的一篇研究讨论了移动端 Agent 如何通过与界面层协作来减少用户界面的暴露核心思路是让 Agent 和专门的界面理解模块配合而不是让大模型自己硬读屏幕像素。这种界面协作的思路其实是对第四章工具调用思想的延伸——把UI 上的按钮和输入框也抽象成一组工具Agent 按需调用。桌面端同样如此市面上已经出现一些桌面 Agent 工具把本地应用的操作封装成可调用接口。虽然这类项目成熟度参差不齐但方向是一致的Agent 的手会越来越长从调用 API 逐步扩展到点按钮、填表单、操作桌面软件。6.3 语音 Agent 与 LiveKit 这类实时管道另一个让我觉得第四章后劲很大的方向是语音 Agent。文本 Agent 的循环是模型思考 → 调用工具 → 返回文本而语音 Agent 在循环前后还要各加一道转换语音转文字STT和文字转语音TTS。LiveKit Agents 这类框架解决的就是这个实时管道问题。它的思路是帮你把音频流接入、STT、LLM 推理、TTS、音频流输出这条链路管理起来你只需要专注定义 Agent 的行为逻辑和工具集。简单理解就是把第四章学到的那一套 ReAct 循环原封不动地搬到语音场景里额外处理掉音频的实时性问题。具体使用上和文本 Agent 差别不大核心仍然是定义 Agent、挂工具、跑循环只是输入输出从字符串变成了音频流。这个方向对实时性要求很高也是我现在正在尝试的方向。6.4 给后续学习的一点路线建议最后分享一下我自己接下来的学习路径也给读者一个参考。我不打算立刻追求多 Agent 集群或复杂记忆系统而是先把第四章这个循环的稳做扎实——包括更严谨的工具描述、更完善的错误兜底、更可控的 token 预算。在此基础上再做两件事一是把日程助手换成真实接口真正接上日历服务二是尝试把同一个 Agent 包装成语音入口体验一遍 LiveKit 这类实时管道的完整流程。我自己的体会是Agent 学习最忌讳看得多、跑得少。第四章的 Demo 再简单跑通一遍获得的体感比读十篇架构文章都有用。尤其是当你亲眼看到模型自己编排出三步工具调用链的时候你对Agent 到底是什么的理解会完全不同。如果读完这篇笔记你想做点什么那就从一个最小循环开始给它挂上第一个真正有意义的工具然后跑一遍试试。踩几个坑比背下所有概念重要得多。
返回列表