news 2026/9/28 4:21:13

Agent 概念、原理与构建模式:从 ReAct 循环到可运行代码深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent 概念、原理与构建模式:从 ReAct 循环到可运行代码深度解析

1. 从一次“模型只会聊天”的翻车说起

很多人第一次写 Agent,都会卡在同一个地方:模型能回答问题,但不会“动手”。你让它读一个文件、跑一条命令、查一次接口,它回你一段看起来很像答案的文字,实际什么都没执行。这不是模型不行,而是你只给了它一张嘴,没给它一双手。

Agent 要解决的核心问题,就是把“语言模型”升级成“能推理、能决策、能行动”的闭环系统。它不再是单轮问答,而是让模型在每一轮里先想清楚要做什么(Reasoning),再选一个工具去执行(Acting),拿到真实结果后继续想(Observation),直到任务完成。这个循环就是 ReAct,Reasoning + Acting 的缩写。

这篇面向想从零理解 Agent 内部机制的开发者。我会把 ReAct 循环拆成可复制的伪代码,再给一个最小可运行示例,重点讲清楚三件事:Thought / Action / Observation 每一轮到底长什么样、怎么验证输出符合预期、以及单 Agent、多 Agent 协作、工具调用这几种构建模式的代码骨架差异。读完你能自己搭一个能读文件、跑命令、并打印完整推理链的 Agent。

在动手之前,先解决模型接入这一层。Agent 每一轮都要调模型,如果模型来源不稳定,调试推理链会非常痛苦。我习惯用一个统一的模型入口来跑这类实验,下面先把它配好。

2. TaoToken 前置:给 Agent 一个稳定的模型入口

Agent 和普通聊天最大的区别是调用频率。一个任务跑下来,ReAct 循环可能触发 5 到 20 次模型请求,每次都要带上完整对话历史。如果模型入口不稳定,你会分不清是“推理逻辑写错了”还是“请求失败了”。

TaoToken 在这里的角色是统一模型入口:它兼容 OpenAI SDK 的调用格式,你只要改base_url和api_key,就能让 Agent 用同一套代码切换不同模型。对调试 ReAct 循环特别有用,因为你可以先用一个便宜快速的模型把循环跑通,再换成更强的模型看推理质量。

你需要准备两样东西:一个 API Key,以及确认接入地址。Key 在控制台生成,接入地址用https://taotoken.net/api。注意这个地址不带任何查询参数,直接作为base_url使用。

提示:Agent 调试阶段建议把每轮请求的 messages 长度打印出来。ReAct 循环会把历史不断追加,token 消耗是随轮次增长的,早发现异常能省不少成本。

拿到 Key 之后,不要硬编码在代码里。用.env文件管理,配合python-dotenv读取。这样你分享代码时不会泄露密钥,切换环境也方便。下面进入具体配置。

3. 可复制配置:ReAct Agent 的最小骨架

先建项目结构。我习惯把提示词、工具、Agent 主体分开,方便单独调试:

react-agent/ ├── .env ├── agent.py ├── prompt.py └── tools.py

.env里只放一行:

TAOTOKEN_API_KEY=你的key

tools.py定义工具。工具的本质就是普通 Python 函数,Agent 通过函数名来调用它们:

import subprocess def read_file(file_path: str) -> str: """读取文件内容""" with open(file_path, "r", encoding="utf-8") as f: return f.read() def write_to_file(file_path: str, content: str) -> str: """写入文件内容""" with open(file_path, "w", encoding="utf-8") as f: f.write(content) return "写入成功" def run_command(command: str) -> str: """执行终端命令""" result = subprocess.run( command, shell=True, capture_output=True, text=True ) if result.returncode == 0: return result.stdout or "执行成功" return f"错误:{result.stderr}"

prompt.py是 ReAct 的灵魂。它用 XML 标签约束模型输出格式,让每一轮都能被程序解析:

REACT_PROMPT = """你需要解决一个问题,把它分解为多个步骤。 每一步先用 <thought> 思考要做什么,再用 <action> 决定调用哪个工具。 工具执行后你会收到 <observation>,继续思考,直到能给出 <final_answer>。 可用工具: {tools} 严格使用以下格式输出: <thought>你的思考</thought> <action>工具名(参数)</action> <observation>工具返回结果</observation> <final_answer>最终答案</final_answer> """

agent.py是核心循环。注意这里用base_url指向 TaoToken,其余调用方式和 OpenAI SDK 完全一致:

import os import re from openai import OpenAI from dotenv import load_dotenv from tools import read_file, write_to_file, run_command from prompt import REACT_PROMPT load_dotenv() client = OpenAI( base_url="https://taotoken.net/api", api_key=os.getenv("TAOTOKEN_API_KEY"), ) TOOLS = { "read_file": read_file, "write_to_file": write_to_file, "run_command": run_command, } def build_system_prompt(): tool_desc = "\n".join(f"- {name}" for name in TOOLS) return REACT_PROMPT.format(tools=tool_desc) def parse_action(text): match = re.search(r"<action>(.*?)</action>", text, re.DOTALL) if not match: return None, None raw = match.group(1).strip() name_match = re.match(r"(\w+)\((.*)\)", raw, re.DOTALL) if not name_match: return None, None return name_match.group(1), name_match.group(2).strip() def run_agent(task, max_steps=8): messages = [ {"role": "system", "content": build_system_prompt()}, {"role": "user", "content": f"<question>{task}</question>"}, ] for step in range(max_steps): resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, ) content = resp.choices[0].message.content messages.append({"role": "assistant", "content": content}) thought = re.search(r"<thought>(.*?)</thought>", content, re.DOTALL) if thought: print(f"[Step {step}] Thought: {thought.group(1).strip()}") if "<final_answer>" in content: answer = re.search( r"<final_answer>(.*?)</final_answer>", content, re.DOTALL ) return answer.group(1).strip() name, args = parse_action(content) if not name: print("未解析到 action,终止") break try: observation = TOOLS[name](args) except Exception as e: observation = f"工具执行错误:{e}" print(f"[Step {step}] Action: {name}({args})") print(f"[Step {step}] Observation: {observation[:200]}") messages.append( {"role": "user", "content": f"<observation>{observation}</observation>"} ) return "达到最大步数,未完成" if __name__ == "__main__": print(run_agent("读取 tools.py 并告诉我里面有几个函数"))

这段代码就是 ReAct 循环的最小可运行版本。它做了四件事:把工具列表注入提示词、每轮请求模型、解析 Thought 和 Action、执行工具并把 Observation 追加回对话历史。循环的终止条件是模型输出<final_answer>或达到最大步数。

4. 验证请求:每轮 Thought / Action / Observation 是否符合预期

跑起来之后,重点不是看最终答案,而是看每一轮的中间输出。这才是理解 Agent 内部机制的关键。用上面那个“读取 tools.py 并数函数”的任务,正常输出应该长这样:

[Step 0] Thought: 我需要先读取 tools.py 文件的内容,才能知道里面有几个函数。 [Step 0] Action: read_file(tools.py) [Step 0] Observation: import subprocess def read_file(file_path: str) -> str: ... [Step 1] Thought: 文件内容已获取,我数一下 def 开头的函数定义。 [Step 1] Action: run_command(grep -c "def " tools.py) [Step 1] Observation: 3 [Step 2] Thought: 已经确认有 3 个函数,可以给出最终答案。

验证时盯三个点。第一,Thought 是否在描述“下一步要做什么”,而不是直接给答案。如果模型跳过思考直接输出 final_answer,说明提示词约束不够强。第二,Action 的函数名是否在工具列表里,参数格式是否合法。第三,Observation 是否是工具的真实返回,而不是模型编造的。第三点最容易出问题,因为模型有时会在没有执行工具的情况下“假装”收到了结果。

你可以加一个断言来强制校验。在追加 observation 之前,检查它确实来自工具执行:

assert observation is not None, "Observation 不能为空" assert name in TOOLS, f"未知工具:{name}"

如果发现模型连续两轮调用同一个工具、参数也一样,说明它陷入了循环。这时候要么在提示词里加“不要重复调用相同工具”,要么在代码里检测重复 action 并强制终止。我试过在run_agent里维护一个seen_actions集合,重复就跳出,比单纯靠 max_steps 更早发现问题。

5. 三种构建模式的代码骨架差异

理解了单 Agent 循环,再看多 Agent 协作和工具调用,就只是骨架的排列组合。

单 Agent 就是上面那套:一个循环、一份工具列表、一条对话历史。适合任务边界清晰、步骤不多的场景,比如“读文件 + 改内容 + 跑测试”。

多 Agent 协作的核心变化是“谁持有对话历史”。常见做法是拆出一个协调者(Orchestrator)和若干执行者(Worker)。协调者不直接调工具,而是把子任务分发给 Worker,每个 Worker 是独立的 ReAct 循环:

class Worker: def __init__(self, name, tools): self.name = name self.tools = tools def run(self, subtask): # 内部就是一个完整的 ReAct 循环 return run_agent(subtask) def orchestrate(task): workers = { "reader": Worker("reader", {"read_file": read_file}), "runner": Worker("runner", {"run_command": run_command}), } plan = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": f"把任务拆成子任务:{task}"}], ).choices[0].message.content # 按 plan 分发,汇总结果 return plan

多 Agent 的坑在于上下文传递。Worker 之间不共享对话历史,协调者必须把上一个 Worker 的关键输出显式传给下一个,否则执行者会“失忆”。这也是为什么多 Agent 更适合子任务之间耦合低的场景。

工具调用模式则是把“选工具”这一步交给模型的原生 function calling,而不是靠 XML 解析。骨架差异在于:你不再解析<action>标签,而是把工具定义成 JSON Schema 传给模型,模型返回结构化的tool_calls字段。好处是解析更稳,坏处是推理过程(Thought)不再显式暴露,调试时看不到模型的思考链。想看清内部机制,还是 XML 标签的 ReAct 更直观。

6. 本篇常见错排查

报错一:KeyError: 'read_file'。模型输出的 action 函数名和工具字典的 key 对不上。常见原因是提示词里工具描述带了括号或参数,模型照抄了。检查build_system_prompt里注入的工具名是否干净,只保留函数名。

报错二:模型一直不输出<final_answer>。要么是提示词没强调终止条件,要么是任务本身需要的信息工具给不了。先在提示词里加一句“当你有足够信息时必须输出 final_answer”,再检查工具返回是否为空。

报错三:Observation 被模型忽略。如果模型下一轮 Thought 完全没提上一轮的 Observation,通常是消息角色用错了。Observation 要以user角色追加,而不是assistant,否则模型会以为那是自己说过的话。

报错四:请求 401 或连接失败。检查.env里的 key 是否被正确加载,base_url是否写成了https://taotoken.net/api。注意不要在这个地址后面拼多余的路径,SDK 会自动补全/v1/chat/completions。

报错五:循环停不下来。除了 max_steps 兜底,建议在代码里检测连续重复的 action。一旦发现相同工具加相同参数出现两次,直接中断并打印当前 messages,方便定位是提示词问题还是工具返回有问题。

7. 下一步:把循环跑通,再谈优化

Agent 的门槛不在概念,而在把第一轮循环跑通。你不需要一上来就搞多 Agent 协作,先用单 Agent 加两三个工具,把 Thought / Action / Observation 的打印看清楚,确认每一轮输出都符合预期,再考虑扩展。

调试阶段建议固定一个模型,把循环跑顺。等推理链稳定了,再通过统一入口切换模型对比效果。需要生成 Key 和查看接入方式,可以从控制台和 API Keys 页面入手;想先直观感受模型在对话里的表现,可以用模型对话页面试几轮;如果打算把 Agent 长期用在编码或自动化任务上,Coding Plan 更适合持续跑循环的场景。接入细节都在接入文档里,照着改base_url就能复用现有代码。

最后留一个实用习惯:每次改完提示词,先跑一个只有一步就能完成的任务,比如“读取某个文件的第一行”。一步能跑对,再逐步加复杂度。Agent 的 bug 大多藏在多轮循环里,从最短路径验证起,比一上来就跑复杂任务高效得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:19:54

第一次面试,记录面经:用 TaoToken 统一 Key 跑通面试复盘配置

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

作者头像 李华
网站建设 2026/9/28 4:17:15

CInfoFile 参数化配置实战:改一改就能用的 TaoToken 接入骨架

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

作者头像 李华
网站建设 2026/9/28 4:17:05

完美卸载OpenClaw后,如何用TaoToken清理CLI网关服务与hash残留

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

作者头像 李华
网站建设 2026/9/28 4:15:51

如何关闭端口被占用的进程

文章目录&#x1f449; 作者&#xff1a;辣椒 &#x1f4cc; 个人网站&#xff5c;项目资源&#xff5c;技术分享 &#x1f449; 点击访问 [https://qiyex.com](https://qiyex.com)背景操作步骤一、打开命令提示符&#xff08;Command Prompt&#xff09;二、查看占用端口的进程…

作者头像 李华