尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
AI智能体循环工程-第6章第3节-环境搭建与第一个循环-50行跑通最小ReAct循环
第3节 50行跑通最小ReAct循环一句话总结不用任何框架50行Python实现决策→工具→观察→再决策的while循环逐行注释讲解真实运行日志展示循环如何自己走出三步。本文导航一、ReAct到底是什么从一次失败的单发问答说起二、先画设计图循环的五步骨架三、完整代码50行跑通四、逐行解剖与真实运行日志五、三种终止方式与这个循环的局限六、翻车现场模型不按格式输出怎么办七、改造练习三个动手题一、ReAct到底是什么从一次失败的单发问答说起前两节把环境、日志、LLM客户端都备齐了这一节干一件大事亲手写出第一个能自己转起来的循环。不装任何 Agent 框架不抄任何脚手架50 行 Python。先讲个我自己的翻车故事。最早我做帮我查资料并算数的任务思路是把所有资料一次性塞进提示词让模型一口气回答。结果惨不忍睹资料里有 20 个网页片段模型算数算错、引用张冠李戴我加提示词、加示例、换模型折腾两天准确率还是上不去。问题出在单发问答这个形态本身模型看不到中间结果只能凭第一印象一锤定音。而人做这类任务是有节奏的——先查一查看到结果再算一算算完发现不对再回头查。这个做一步、看一步、想一步的节奏就是 ReActReason Act的核心。形态模型看到什么决策次数典型失败单发问答全部资料一次塞入1次算错、引用错、上下文过载CoT 纯推理问题 自己的推理链1次没有外部信息凭空编造ReAct 循环目标 历史观察逐步决策多轮依赖格式约定本节的v1形态ReAct 的论文里那句 interleaving reasoning and acting翻译成大白话就是让推理和行动交替进行每一步的行动结果都成为下一步推理的输入。用伪代码写出来其实就是一个 while 循环。论文Yao et al., 2022里的实验结果也值得记一笔它解释了为什么业界集体转向循环形态任务类型纯 CoTReActReActCoT 自洽说明HotpotQA 问答基线显著提升最优查证后回答减少幻觉FEVER 事实核查基线显著提升最优先检索证据再下结论ALFWorld 决策大幅失败成功率过半-边做边看错了能改WebShop 购物基线明显提升-多轮浏览比一步猜强规律很清楚凡是需要外部信息或中间反馈的任务循环形态碾压单发形态。而偶尔会因为格式或推理跑偏而失败这个短板正好被 CoT 自洽和后面的验证器补掉。我们课程的主线本质就是不断把右边这列的短板补齐。我自己还有一个更朴素的判断标准**这个任务人会不会分步做**人要分步做的任务模型大概率也需要循环人一口气能答完的任务单发就够。别为了用 Agent 而用 Agent杀鸡用牛刀既贵又慢。二、先画设计图循环的五步骨架我的习惯是先画图再写代码。最小循环的骨架就五步一张图讲完完成未完成1 收集上下文目标历史2 决策LLM生成下一步3 终止检查该停了吗5 返回答案4 工具执行观察结果追加进历史注意一个反直觉的细节终止检查放在工具执行之前。模型一旦输出完成这轮就不该再碰工具了。我第一版把终止判断写在循环末尾结果模型都说了完成|答案是42程序还傻乎乎地去解析工具名把完成当工具执行了一次日志里出现一条未知工具完成。这种顺序 bug 很典型记住先判停、再干活。代码结构与五步的对应关系提前给出来读代码时随时回来查代码块对应步骤职责TOOLS字典工具层名字到函数的映射就是最简工具注册表react_loop()循环层五步骨架for 循环兜底最大步数system提示词约束层告诉模型目标、可用工具、输出格式history列表记忆层每轮观察追加充当外部记忆__main__入口层建客户端、设目标、打印调用总结顺带解释两个写法选择。其一为什么用for step in range(1, max_steps 1)而不是while True因为步数上限必须是结构性的写进循环头就永远不会忘用 while 就得靠手写 break漏一处就是一条 doom loop。其二为什么终止判断用startswith(完成)而不是因为模型经常在标记后面跟答案startswith既认标记又留得出余量一行代码的宽容度刚刚好。是否目标 goalsystem 提示词目标工具格式history 历史列表外部记忆LLMClient.generate解析 工具名|参数工具存在?执行工具拿结果返回错误提示三、完整代码50行跑通代码就是下面这些我保留了最朴素的形态——字典当工具注册表、列表当记忆、partition(|)当解析器。先跑通再谈工程化第4节马上给它换上 function calling# src/ai_loop_course/minimal_loop.py50行最小ReAct循环决策 - 工具 - 观察 - 再决策fromai_loop_course.llm_clientimportLLMClient,SHARED_LOGGERfromai_loop_course.logging_configimportsetup_logging,get_logger loggerget_logger(minimal_loop)# 工具层最简注册表名字 - 可调用对象TOOLS{calculate:lambdaexpr:str(eval(expr)),# 简单计算search:lambdaq:f搜索{q}找到3条结果,# 模拟搜索}defreact_loop(goal:str,client:LLMClient,max_steps:int5)-str:最小ReAct循环返回最终答案或终止原因# 约束层把目标、可用工具、输出格式一次交代清楚systemf你是任务执行智能体。目标{goal}可用工具calculate(expr)、search(query) 格式每轮输出一行 工具名|参数 或完成|最终答案history:list[str][]# 记忆层每轮观察都追加进来forstepinrange(1,max_steps1):# 1. 收集上下文目标 全部历史观察contextf目标{goal}\n历史{history}\n请决定下一步。# 2. 决策问模型下一步做什么decisionclient.generate(context,systemsystem).strip()logger.info(f[Turn{step}] 决策{decision})# 3. 终止检查先判停、再干活ifdecision.startswith(完成):logger.info(f✅{step}轮完成{decision})returndecision# 4. 工具执行解析工具名|参数并派发tool_name,_,argdecision.partition(|)toolTOOLS.get(tool_name)resulttool(arg)iftoolelsef未知工具{tool_name}。可用工具{list(TOOLS)}logger.info(f 执行{tool_name}({arg}) →{result})# 5. 观察结果追加进历史成为下一轮的输入history.append(f{tool_name}({arg}){result})return达到最大步数未完成if__name____main__:setup_logging()# 控制台文件双输出第1节的基建clientLLMClient(ollama,qwen3:8b,http://localhost:11434)react_loop(计算 (1527) 的结果,client)SHARED_LOGGER.print_summary()# 程序退出打印调用总结别小看history.append(...)这一行。它就是 ReAct 里 Observation 的全部实现把行动结果写进下一轮的输入。没有它模型每轮都在失忆重来永远不知道自己已经算过 152742。3.1 没有API也能跑MockLLMClient 离线自测写循环最烦的是调试要烧真金白银的 token而且网络一抖断点就白打。我的做法是先用一个剧本客户端把循环逻辑跑通确认五步顺序、终止判断都对再换真模型。这个 Mock 客户端复刻generate(prompt, system)的接口签名按脚本吐决策# tests/mock_client.py剧本客户端不联网、不花钱用于离线验证循环逻辑fromai_loop_course.llm_clientimportSHARED_LOGGERfromai_loop_course.logging_configimportsetup_logging,get_loggerfromai_loop_course.minimal_loopimportreact_loop loggerget_logger(mock)classMockLLMClient:假客户端按预设剧本依次返回决策行为完全确定def__init__(self,script:list[str]):self.scriptlist(script)# 每轮的模型决策self.call_loggerSHARED_LOGGERdefgenerate(self,prompt:str,system:str|NoneNone)-str:decisionself.script.pop(0)logger.debug(fMOCK收到上下文长度{len(prompt)}返回{decision})returndecisionif__name____main__:setup_logging()mockMockLLMClient([search|循环工程,calculate|100*3,完成|搜到3条结果100*3300])answerreact_loop(搜索循环工程的定义然后计算 100*3,mock,max_steps5)print(最终返回:,answer)离线跑一遍的真实输出11:08:20 INFO [Turn 1] 决策search|循环工程 11:08:20 INFO 执行search(循环工程) → 搜索循环工程找到3条结果 11:08:20 INFO [Turn 2] 决策calculate|100*3 11:08:20 INFO 执行calculate(100*3) → 300 11:08:20 INFO [Turn 3] 决策完成|搜到3条结果100*3300 11:08:20 INFO ✅ 3轮完成完成|搜到3条结果100*3300 最终返回: 完成|搜到3条结果100*3300剧本客户端还有个妙用复现线上事故。哪天日志里发现模型第 4 轮开始胡言乱语把当时的决策序列抄进 script就能离线反复重放配合断点逐帧排查。第6节的轨迹回放器会把这个能力做成正式功能。四、逐行解剖与真实运行日志4.1 五步与代码的精确对应代码行循环五步一句话解释context f目标...历史...1. 收集上下文每轮都重发目标全部历史decision client.generate(...)2. 决策模型输出工具名|参数或完成|答案if decision.startswith(完成)3. 终止检查在执行工具之前判停result tool(arg)4. 工具执行字典派发未知工具返回提示history.append(...)5. 观察观察进记忆闭环成立为什么每轮重发全部历史而不是维护一个 messages 列表为了 50 行的极简。代价是 token 随轮数线性增长——这是刻意的教学取舍第4节用标准 messages 结构替换它第9章再系统处理上下文膨胀。4.2 真实运行日志两轮完成uv run python-m ai_loop_course.minimal_loop11:02:10 INFO [Turn 1] 决策calculate|1527 11:02:11 INFO 执行calculate(1527) → 42 11:02:13 INFO [Turn 2] 决策完成|计算结果是 42 11:02:13 INFO ✅ 2轮完成完成|计算结果是 42 11:02:13 INFO 11:02:13 INFO LLM 调用总结 11:02:13 INFO 11:02:13 INFO 总调用次数: 2 | 成功: 2 11:02:13 INFO 输入token | 最大: 96 | 最小: 41 | 平均: 68 11:02:13 INFO 输出token | 最大: 12 | 最小: 8 | 平均: 10 11:02:13 INFO 耗时(ms) | 最大: 1487 | 最小: 1206 | 平均: 1347 11:02:13 INFO 注意第 2 轮的输入 token96比第 1 轮41大——因为 history 里多了一条观察。上下文增长是循环的固有成本这个趋势后面会反复出现。4.3 多步任务循环自己走三步换个需要先搜后算的目标$ uv run python -m ai_loop_course.minimal_loop 目标搜索循环工程的定义然后计算 100*3 11:05:41 INFO [Turn 1] 决策search|循环工程 11:05:41 INFO 执行search(循环工程) → 搜索循环工程找到3条结果 11:05:44 INFO [Turn 2] 决策calculate|100*3 11:05:44 INFO 执行calculate(100*3) → 300 11:05:47 INFO [Turn 3] 决策完成|循环工程的定义有3条结果100*3300 11:05:47 INFO ✅ 3轮完成完成|循环工程的定义有3条结果100*3300三轮下来搜索→计算→总结的节奏是模型自己规划的我只给了目标。这就是循环和脚本的本质区别脚本的步骤是人写死的循环的步骤是模型现场决定的。踩坑提示eval(expr)在教学示例里图省事生产环境是绝对禁止的——一行__import__(os).system(...)就能把你的机器交代了。第4节的 calculate 工具用ast白名单解析只允许加减乘除幂和取负那才是能上线的写法。五、三种终止方式与这个循环的局限5.1 三条出口1. 目标达成模型输出完成|答案主动退出 2. 预算耗尽for 循环到 max_steps 上限被动退出 3. 工具失败未知工具返回错误提示不算终止但会消耗一轮第 2 条是安全带。有人问模型真会停不下来吗太会了。我见过模型连续 8 轮输出calculate|22每轮都再验算一下没有步数上限它能算到天荒地老。第5节会把这条安全带升级成三维预算步数/Token/时间。真实 doom loop 的日志截段长这样每次看我都心疼钱包[Turn 31] 决策calculate|22 执行calculate(22) → 4 [Turn 32] 决策让我再验算一次 calculate|22 执行让我再验算一次 calculate(22) → 未知工具 [Turn 33] 决策calculate|22 ... [Turn 47] 达到最大步数未完成 调用总结47次调用 | 输入token累计 182K | 耗时累计 6分12秒 | 成果0注意第 32 轮那种加了废话的决策——格式跑偏和行为重复经常同时出现这是模型陷入低质量循环的典型信号。后面第5节除了预算还会教你怎么用重复检测提前发现这种苗头。5.2 这个循环的局限清单局限现象后续改进无确定性验证模型自报完成可能假完成第7章验证器工具太简单只有计算和搜索eval 有风险第4节工具注册表无预算控制只有步数上限第5节终止条件无轨迹落盘只打印到控制台不能回放第6节JSONL轨迹无外部记忆历史只在内存长任务撑不住第9章记忆系统格式脆弱靠工具名|参数文本约定第4节function calling**但这 50 行是所有改进的起点。**后面每一节都是往这个骨架上加装保险杠骨架本身从没变过收集上下文→决策→判停→执行→观察。六、翻车现场模型不按格式输出怎么办文本格式约定最大的软肋就是模型不一定听话。我的真实翻车记录[Turn 1] 决策让我来帮你计算 1527 执行让我来帮你计算 1527() → 未知工具让我来帮你计算 1527。可用工具[calculate, search]模型加了句开场白partition(|)找不到分隔符整个字符串被当成工具名。这种事故的根源是输出格式是软约束解析器却按硬格式来。三个层次的对策成本递增、可靠性也递增对策做法成本可靠性提示词加固要求只输出一行禁止任何解释低中容错解析正则提取\w|.忽略杂质低中高结构化输出用 function calling 或 JSON mode中高第4节最简单的容错解析只要一行正则# 从杂乱输出中提取工具名|参数importre mre.search(r(calculate|search)\|(.),decision)tool_name,arg(m.group(1),m.group(2))ifmelse(,)踩坑提示容错解析有个隐性代价——它会静默吞掉格式错误你以为模型很乖其实每天都在垃圾进垃圾出。我的做法是容错解析成功时打 WARNING攒够证据就升级到 function calling。日志基建的价值在这里就体现出来了grep 一下 WARNING 就知道格式错误率。七、改造练习三个动手题每个练习都不改骨架只往里加零件。练习1最简单练习3会提前用到第2节的调用日志。练习1加时间工具fromdatetimeimportdatetime TOOLS[get_time]lambda_:datetime.now().strftime(%Y-%m-%d %H:%M:%S)改完把 system 里的可用工具清单同步加上get_time()然后测目标现在几点。改工具别忘了改提示词里的清单——两边不一致模型根本不知道有这个新工具这是我最常犯的低级错误。练习2加读文件工具TOOLS[read_file]lambdapath:open(path,encodingutf-8).read()[:500]截断到 500 字符是刻意的工具输出会进 history不截断一个大文件就能把上下文撑爆。练习3加token预算total_tokens0forstepinrange(max_steps):contextf目标{goal}\n历史{history}\n请决定下一步。decisionclient.generate(context,systemsystem)total_tokensclient.call_logger.records[-1].input_tokens# 取最后一次调用记录iftotal_tokens100_000:returntoken预算耗尽...client.call_logger.records[-1]能拿到刚刚那次调用的输入 token 数——这就是第2节全字段记录的直接回报。没有那套记录你只能自己拿 tiktoken 去估算麻烦且不准。验证建议三个练习跑通后用这个组合目标一次验完“读取 README.md 的前500字告诉我现在几点然后计算 7*8”。它会逼着模型连用三个工具能暴露提示词清单不同步、上下文溢出等一堆问题。用 Mock 客户端预演一遍预期节奏再上真模型对照mockMockLLMClient([read_file|README.md,get_time|now,calculate|7*8,完成|README开头是项目简介现在是2026-09-25 11:15:307*856,])react_loop(读取 README.md 的前500字告诉我现在几点然后计算 7*8,mock,max_steps6)11:15:30 INFO [Turn 1] 决策read_file|README.md 11:15:30 INFO 执行read_file(README.md) → # ai-loop-course ... 11:15:30 INFO [Turn 2] 决策get_time|now 11:15:30 INFO 执行get_time(now) → 2026-09-25 11:15:30 11:15:30 INFO [Turn 3] 决策calculate|7*8 11:15:30 INFO 执行calculate(7*8) → 56 11:15:30 INFO [Turn 4] 决策完成|README开头是项目简介现在是2026-09-25 11:15:307*856 11:15:30 INFO ✅ 4轮完成完成|README开头是项目简介现在是2026-09-25 11:15:307*856真模型的轮数可能略有浮动比如它把 get_time 和 calculate 合并成一轮连续调用但工具使用顺序应该基本一致。先用 Mock 定义标准答案再看真模型跑得偏不偏这个习惯能省下大量真金白银的调试成本。小结ReAct 推理与行动交替每步的观察结果进入下一步的输入模型做一步看一步。五步骨架收集上下文→决策→终止检查→工具执行→观察50行实现完毕。终止检查在工具执行之前先判停、再干活顺序反了就闹未知工具完成的笑话。history 即记忆一行 append 就是 Observation 机制的全部。格式约定是软约束容错解析兜底function calling 是根治方案。局限清单就是路线图验证器、预算、轨迹、记忆后面章节逐项补上。延伸阅读与思考实践把 50 行抄进自己的项目换成你手边的任意模型跑通。改造完成三个练习重点观察练习3里 token 随轮数的增长曲线。思考如果模型连续两轮输出完全相同的工具调用循环该怎么察觉并止损提示history 里就有答案。下节预告第6章第4节《给循环装上工具function calling与工具注册表》——告别工具名|参数的文本约定用 schema 定义工具、自动注入 API、结构化派发执行还有模型选错工具时怎么让它自己爬回正道的错误返回设计。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~文章编号第6章第3节 | 总进度40/120 | 预计阅读时间12分钟
RELATED

相关推荐

AI智能体循环工程-第5章第7节-循环的五大原语与生态-原语可移植性同一个循环跑遍ClaudeCodeCodexTRAE

AI智能体循环工程-第5章第7节-循环的五大原语与生态-原语可移植性同一个循环跑遍ClaudeCodeCodexTRAE

第7节 原语可移植性:同一个循环跑遍Claude Code/Codex/TRAE 一句话总结:“一旦发现形状相同,就别再争论用哪个工具”——五大原语在三大工具的映射总表;TRAE等国产工具的原语对照;设计可移植循环的抽象原则。 本文导航…

📅 2026/10/12 2:22:33
蓝鲸智云配置平台 bk-cmdb API 实战:bind_host_agent 将 GSE Agent 绑定到主机

蓝鲸智云配置平台 bk-cmdb API 实战:bind_host_agent 将 GSE Agent 绑定到主机

后端企业应用运维 【免费下载链接】bk-cmdb 蓝鲸智云配置平台(BlueKing CMDB) 项目地址: https://gitcode.com/gh_mirrors/bk/bk-cmdb 点击查看 免费下载 导读 bind_host_agent 是蓝鲸智云配置平台(bk-cmdb)提供的开放 API 之一&#xff0c…

📅 2026/10/12 2:17:32
Semantic Router Operator 运维实战:状态协调、安全更新、扩缩容与故障排查

Semantic Router Operator 运维实战:状态协调、安全更新、扩缩容与故障排查

后端API网关模型推理服务AI Agent 【免费下载链接】semantic-router An open, programmable decision layer for models and compute. 项目地址: https://gitcode.com/gh_mirrors/sem/semantic-router 点击查看 免费下载 本指南面向已经通过 Kubernetes Operator 部…

📅 2026/10/12 2:17:32
MORE NEWS

更多资讯

📰

roLabelImg源码解析:旋转框标注工具从安装到二次开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

数据库图书管理系统实训全流程:从E-R图到JDBC事务与并发控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

图像质量评估模型Python实战:PSNR/SSIM/BRISQUE量化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

CLion搭建树莓派Pico C/C++开发环境:从零到断点调试全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ESP32 上实现 ONVIF 相机:从组件搭建到 NVR 添加实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

UC网盘下载不限速办法:亲测有效的免费提速思路与操作指南

UC网盘下载不限速的办法,亲测有效的免费加速思路都在这了用UC浏览器的人几乎人手一个UC网盘,平时存点资料、传个文件确实方便,但真到下载大文件的时候,那进度条走得叫一个折磨。明明家里宽带是五百兆,眼见着其他App下载…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬