资讯动态

从零手写 ReAct Agent:用 70 行 Python 实现工具调用循环(awesome-agentic-ai-zh 练习 3 实战解析)

发布时间:2026/10/9 7:39:17 来源:尧图企业网站定制
教程文档AI Agent人工智能大模型【免费下载链接】awesome-agentic-ai-zhA trilingual (繁中 / English / 简中) learning roadmap for agentic AI: from LLM basics to multi-agent systems, with 240 curated resources and hands-on examples. 中文 AI agent 學習地圖。项目地址https://gitcode.com/gh_mirrors/aw/awesome-agentic-ai-zh点击查看免费下载本文以 awesome-agentic-ai-zh 仓库 Stage 3 的「练习 3从零实现 ReAct不用 framework」为骨架完整拆解如何用约 70 行 Python 亲手写出「思考 → 调用工具 → 观察结果 → 再思考」的 Agent 循环对应 Stage 3 — 工具使用与第一个 Agent Loop。读完本文你将掌握 OpenAI 与 Anthropic 两套 Tool Use 消息协议的差异、messages累积与tool_use_id配对机制、max_iter安全阀设计以及基于 AST allowlist 的防eval注入安全计算器并能直接运行本仓库的 starter 与离线 Mock 测试完成验证。为什么必须从零写一次 ReActReActReasoning Acting是现代 Agent 的基础 pattern其核心就是一条朴素的循环while not done: thought LLM 看完目前 context、讲出下一步要做什么 action LLM 调用一个 tool observation tool 执行结果、喂回去给 LLMLangGraph、CrewAI 这类框架把这个 loop 藏在了框架内部。只有自己亲手写过一次你才能真正回答下面四个问题为什么 messages array 一直长因为每一轮的 assistant 回复和 tool 执行结果都必须追加进历史LLM 才有完整的上下文决定下一步如果丢掉任何一环模型就失忆了。tool_use_id 跟 tool_result 怎么配对一次回复里可能同时发起多个工具调用每个调用有唯一 ID工具结果必须回带该 ID模型才知道哪个结果对应哪个调用。stop_reason 为什么是tool_use或end_turn模型一回合的输出要么是请求调用工具需要继续循环要么是给出最终答案循环结束两个终止信号对应两种分支。max_iter 为什么是 safety net工具结果写得不好时模型可能无限调用工具迭代上限是必须存在的硬性保险。本练习的 starter.py 用约 70 行 Python 把这些全部交代清楚Stage 3 章节 还给出了对应的 13 行最小循环骨架供对照理解。学习模式遵循 docs/HOW_TO_USE.md 的每次只改一件事方法先运行 starter 与测试再做一个小改动用测试结果验证你的理解。怎么跑两条运行路径练习目录提供两条并行的运行路径共享同一套工具定义与循环逻辑只在 SDK 与消息协议上不同详见下文源码走查。依赖统一声明在 requirements.txtopenai3.5,4 anthropic1.1,2 # Only Anthropic starters need this package.Path A默认、本机免费Ollama qwen2.5:3bpip install -r requirements.txt ollama pull qwen2.5:3b ollama serve python starter.py模型默认值MODEL os.environ.get(MODEL, qwen2.5:3b)starter.py可用环境变量MODEL覆盖。客户端构造OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama)starter.py即通过 Ollama 提供的 OpenAI 兼容端点访问本机模型api_key 仅为占位。预算$0 API 费用不包含硬件、内存与电力成本Ollama 首次 pull 模型需要下载空间Stage 3–6 默认 tagqwen2.5:3b约 1.9 GB核查日期 2026-08-31详见 examples/README.zh-Hans.md。Path BAnthropic、云端比较Claudepip install -r requirements.txt $env:ANTHROPIC_API_KEY your-key python starter_anthropic.py模型默认值MODEL os.environ.get(MODEL, claude-haiku-4-5-20251001)starter_anthropic.py。预算每次先预留$0.05。实际费用按输入 tokens × $1 / 1,000,000 输出 tokens × $5 / 1,000,000计算Tool Use 还会加入 prompt tokens价格查核日2026-08-27。云端调用可能使用额度或产生费用运行前查看当天官方 pricing 页面并设置上限不要把 key 写进程序或 commitexamples/README.zh-Hans.md。两条路径均需注意Windows 的 cp950 控制台无法直接输出中文与 emoji因此两个 starter 都通过sys.stdout.reconfigure(encodingutf-8, errorsreplace)强制 UTF-8 输出starter.py。预期看到Path A、本机❓ 问题台北人口 除以 纽约人口、答案保留 4 位小数。 ------------------------------------------------------------ [step 0] thought: 我先查台北人口... tool: lookup_fact({query: 台北人口}) → 2602000 [step 1] thought: 接着查纽约人口... tool: lookup_fact({query: 纽约人口}) → 8336000 [step 2] thought: 计算比例... tool: calculator({expression: 2602000 / 8336000}) → 0.3121... [step 3] thought: 答案是 0.3122 ------------------------------------------------------------ ✅ 最终答案台北人口除以纽约人口约 0.3122 共 4 轮 ✅ 练习 3 通过 — ReAct loop 自己连用了 lookup_fact 跟 calculator这展示了 ReAct 的核心价值单一 LLM 调用无法可靠完成的查两个事实 精确计算任务通过多轮工具调用即可拆解完成。不花钱验证程序逻辑离线 Mock 测试运行真实模型之前先跑离线测试验证循环逻辑两条路径各自对应一个测试文件python test.py # 验 Path A (Ollama) starter.py 逻辑 python test_anthropic.py # 验 Path B (Anthropic) starter_anthropic.py 逻辑两条 test 都用unittest.mock、不打真 API、$0/run。区别在于 Mock 响应的形状test.py用 OpenAI-compat response shapechoices[0].message.tool_callsfinish_reasontest_anthropic.py用 Anthropic content blockscontent中typetext与typetool_use块 stop_reason。以 test.py 为例其通过SimpleNamespace构造假响应对象、用MagicMock取代 OpenAI client例如test_react_loop_multi_step用side_effect依次注入查台北 → 查纽约 → 算比例 → stop 给答案四轮假响应断言最终答案含0.3122、总步数为 4、工具调用序列恰为[lookup_fact, lookup_fact, calculator]test.py。README 中给出的预期输出✅ test_calculator_basic ✅ test_calculator_rejects_eval_injection ✅ test_lookup_fact ✅ test_react_loop_single_tool_call ✅ test_react_loop_multi_step ✅ test_react_loop_respects_max_iter 全部通过 — 你的 ReAct loop 逻辑正确源码中实际还包含两组额外断言test_invalid_tool_call_is_returned_as_data非法 JSON、未知工具名、多余参数、参数类型错误、空字符串参数均返回error:前缀与test_non_stop_terminal_reason_is_not_a_final_answerfinish_reason length时final必须为None且truncated为True防止把截断的半截话当作答案test_anthropic.py 则额外验证了错误 tool_result 会带is_error: true标记回传test_anthropic.py。Stage 3 章节给出的练习 3 完成条件stages/03-tool-use-and-hello-agent.zh-Hans.md是测试能证明没有 tool call 就停止与超过MAX_STEPS会报错——这正是test_react_loop_single_tool_call与test_react_loop_respects_max_iter所覆盖的。程序结构走查源码级README 用一张表格概括了程序的分段结构本文结合源码逐段展开段行在做什么tool_calculator~30-40安全的计算器whitelist 过滤、避免eval漏洞tool_lookup_fact~42-50假事实库教学用、避免依赖外部 APITOOLS_SPEC~52-75tool schema 给 LLM 看TOOL_IMPL~77-80name → callable 对应表dispatchreact_loop~85-130主循环、含 max_iter safety、messages累积、tool result 接回去安全计算器AST operator allowlist 而非 evaltool_calculator及其底层_evaluate_arithmeticstarter.py是练习的安全重点实现思路是把模型文本解析成 AST只允许白名单内的算子求值绝不把模型文字当代码执行。具体防御包括四层长度上限表达式长度超过MAX_EXPRESSION_LENGTH 200直接报错starter.py。AST 规模上限ast.walk统计节点数超过MAX_AST_NODES 50拒绝防止超长嵌套表达式耗尽资源。深度上限递归求值时深度超过MAX_AST_DEPTH 12拒绝。数值边界中间值与结果都须满足_within_bounds——整数绝对值不超过MAX_ABS_NUMBER 1_000_000_000_000浮点数必须math.isfinite且绝对值在界内starter.py杜绝10**1000这类溢出。白名单只放行ast.Constantint/float、ast.UAdd、ast.USub、ast.BinOp下的Add/Sub/Mult/Div且显式拦截除零starter.py其他任何节点包括**、//、函数调用、属性访问一律抛出ValueError由tool_calculator转成error: ...字符串返回给模型starter.py。对应测试test_calculator_rejects_unsafe_expressions一次性验证了2 ** 3、7 // 2、10 ** 1000、__import__(os).system(ls)、1 / 0等注入向量全部被拒test.py。README 特别强调不要靠字符 whitelist 或ast.literal_eval做算术——字符过滤可被编码绕过literal_eval只解析不执行但无法约束表达式复杂度必须用明确的 AST operator allowlist 并同时限制输入长度、AST 深度、节点数与数字/结果大小。假事实库教学用、零外部依赖tool_lookup_fact内置三条假事实台北人口 2602000、纽约人口 8336000、光速 299792458 m/s未命中时返回unknown: ...前缀starter.py。这样练习不依赖任何外部 API 或数据库任何机器都能复现。TOOLS_SPEC两套 SDK 的 schema 差异TOOLS_SPEC是喂给 LLM 的工具说明。注意两个文件格式不同Path AOpenAI-compatiblestarter.py工具包裹在{type: function, function: {name: ..., description: ..., parameters: {...}}}里参数用propertiesrequired声明并设置additionalProperties: False。Path BAnthropicstarter_anthropic.py直接是{name: ..., description: ..., input_schema: {...}}顶层结构。两边参数对象结构相似但外层包装与键名parametersvsinput_schema不同——不要直接复制旗标名称这也是本项目刻意保留两条路径的原因之一。TOOL_IMPL 与 execute_tool白名单派发 输入校验TOOL_IMPL是name → callable的字典映射dispatch 表calculator与lookup_fact分别用 lambda 取出inp[expression]/inp[query]调用实现starter.py。在派发之前execute_tool扮演了不信任输入的校验层starter.py工具名不在TOOL_IMPL中 → 返回error: tool not allowed: ...参数必须能json.loads成对象Path A 的 arguments 是字符串否则报 JSON 错误参数必须是 dict且恰好只包含 schema 声明的那一个字段set(args) ! {field}即拒绝多余/缺失字段字段值必须是非空字符串实现抛出KeyError/TypeError/ValueError时统一转成error: invalid arguments: ...。返回值是三元组(args, observation, is_error)其中observation.startswith(error:)会作为 is_error 标记供 Anthropic 路径在 tool_result 块上设置is_error。react_loop主循环与两套消息协议react_loop(question, max_iter6, clientNone)返回{final, trace, steps, ...}starter.py每一轮携带完整messages含toolsTOOLS_SPEC调用 LLM取出 assistant 文本与 tool_calls把 assistant 的完整回复含 tool_calls追加进 messages——OpenAI 格式下需重建{role: assistant, content: ..., tool_calls: [{id, type, function: {name, arguments}}]}若没有 tool_callsfinish_reason stop时返回最终答案否则返回terminal_reason并标记truncated如length若有 tool_calls逐个执行execute_tool把{role: tool, tool_call_id: tc.id, content: obs}追加回 messages——这就是 tool_use_id 与 tool_result 的配对方式循环跑满max_iter仍未收尾则返回finalNone, truncatedTrue。Path B 的对应实现starter_anthropic.py协议差异清晰可见终止信号是stop_reasonend_turn表示完成max_tokens表示截断同样不当作最终答案assistant 回复整体原样追加messages.append({role: assistant, content: resp.content})工具结果以 content block 形式回传{type: tool_result, tool_use_id: call.id, content: obs}错误时加is_error: True并作为user 消息追加messages.append({role: user, content: tool_results})。trace只记录可观察的 assistant 文本、工具名、输入与结果{step, assistant_text, tool, tool_input, obs}不记录私有 Chain-of-Thought——这是仓库刻意维持的日志契约stages/03-tool-use-and-hello-agent.zh-Hans.md。常见坑与防御README 列出四个最经典的坑源码均有对应防御忘记把 assistant response 加进 messages→ 下一轮 LLM 看不到自己上一轮讲过什么会 loop forever。防御messages.append(assistant_entry)在每次调用后立即执行starter.py。tool_result 没带tool_use_id→ LLM 无法配对哪个 result 对应哪个 call。防御OpenAI 路径用tool_call_id: tc.idAnthropic 路径用tool_use_id: call.id。while True没 max_iter→ 工具结果写得不好时 LLM 无限调用。防御for step in range(max_iter)硬性封顶跑满即truncatedTrue返回test_react_loop_respects_max_iter用永不收尾的假响应验证了这一点test.py。未过滤的 eval→ 模型文本可能造成 RCE。防御AST operator allowlist 长度/深度/节点数/数值边界四重限制见上文安全计算器小节。换模型与扩展方向换模型Path B 预设用固定 IDclaude-haiku-4-5-20251001想比较 sonnet 时$env:MODEL claude-sonnet-5; python starter_anthropic.py或在starter_anthropic.py改MODEL ...那行Path A 同样支持$env:MODEL覆盖例如切到更大尺寸的本地模型。注意换模型会影响工具调用格式的遵循程度与答案质量属于 docs/HOW_TO_USE.md 六步循环里每次只改一件事的独立变量。加更多 tool在TOOLS_SPECTOOL_IMPL各补一个 entry 即可schema 描述新工具的参数TOOL_IMPL登记 name → callableTOOL_FIELDS声明参数键名execute_tool的校验逻辑自动生效。加 streaming把client.messages.create(...)换成with client.messages.stream(...) as s:边跑边打印 token观察工具调用文本的流式输出。加 prompt cache在system或tools上带cache_control{type:ephemeral}重复 call 可省约 90% 输入 token成本结构中的输入侧开销与 Path B 的计费方式直接相关。接框架用 LangGraph 或 Pydantic AI 重写同一任务对照观察框架如何把这 70 行循环藏进声明式 API——这正是 Stage 4 — Agent Frameworks 的内容衔接点。延伸学习资源本练习是章节式深度教材的入口而非替代品。仓库 Stage 3 章节的「精选 Projects」 列出了从零实现方向的延伸资源如 pguso/ai-agents-from-scratch、arunpshankar/react-from-scratch 等项目及其维护状态与许可证说明建议在完成本练习的 mock 测试与一次 live call 之后再对照阅读 ReAct 原论文Yao et al. 2022的第 3 节加深对 loop 设计动机的理解。完成检查能解释为什么 messages 一直长、tool_use_id 如何配对、stop_reason 两个值各代表什么、max_iter 为什么是 safety net——做到这四点本练习的核心目标即达成。赞分享教程文档AI Agent人工智能大模型【免费下载链接】awesome-agentic-ai-zhA trilingual (繁中 / English / 简中) learning roadmap for agentic AI: from LLM basics to multi-agent systems, with 240 curated resources and hands-on examples. 中文 AI agent 學習地圖。项目地址https://gitcode.com/gh_mirrors/aw/awesome-agentic-ai-zh点击查看免费下载相关推荐从零手写 ReAct Agent Loopawesome-agentic-ai-zh 练习 3 的 70 行实现与双 SDK 路径验证从零手写 ReAct Agent Loopawesome agentic ai zh 练习 3 的 70 行实现与双 SDK 路径验证 本篇文章围绕 awes教程文档AI Agent人工智能大模型MinIO 集群监控实战Prometheus 接入到告警落地一条链路讲透MinIO 集群监控实战Prometheus 接入到告警落地一条链路讲透 MinIO 是 S3 兼容的分布式对象存储跑上生产之后总得有人盯着它。我印象最深教程文档AI Agent人工智能大模型AReaL 分布式训练调试指南FSDP2/TP/CP/EP 场景下的 Hang、OOM 与通信错误排查实战AReaL 分布式训练调试指南FSDP2/TP/CP/EP 场景下的 Hang、OOM 与通信错误排查实战 AReaL 的分布式训练基于 PyTorch 原生教程文档AI Agent人工智能大模型上一篇LX Music 桌面版免费聚合音乐播放器完整指南5分钟上手听遍多平台歌曲下一篇老旧 Mac 多屏输出 OCLP 教程4 步打通投影仪与 iPad 副屏创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

免费获取报价 →
↑