[{"data":1,"prerenderedAt":46},["ShallowReactive",2],{"article-agent\u002F04-react-text-protocol":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"body":14,"_type":40,"_id":41,"_source":42,"_file":43,"_stem":44,"_extension":45},"\u002Farticles\u002Fagent\u002F04-react-text-protocol","agent",false,"","04-ReAct 文本协议对照版：用正则解析实现 Agent 心跳","不借助 function calling，用纯文本协议（Thought\u002FAction\u002FObservation）手写 ReAct 循环。与原生 tool_calls 版对照阅读，彻底看清工具调用协议这一层的本质，以及 agent 心跳是如何用一个 while 循环转起来的。","2026-06-15",[12,13],"Agent开发","人工智能",{"type":15,"children":16,"toc":35},"root",[17,25],{"type":18,"tag":19,"props":20,"children":22},"element","h3",{"id":21},"直接看代码",[23],{"type":24,"value":21},"text",{"type":18,"tag":26,"props":27,"children":29},"pre",{"code":28},"\"\"\"练习 3（对照版·经典文本协议）：从零手写 ReAct 循环 —— Agent 的“心跳”\n\n【这是对照版本】配套文件 04-react-from-scratch.py 是对齐官方仓库的“原生\ntool_calls”写法；本文件保留的是 ReAct 论文最原始的“文本协议”写法。\n两者都是 from-scratch 的手动 while 循环，区别只在“工具调用怎么传”：\n    · 本文件：模型输出 Thought\u002FAction 文本，我们自己用正则解析；\n    · from-scratch 版：用 API 原生的 tool_calls \u002F role=tool \u002F finish_reason。\n建议两个一起读，能彻底看清“工具调用协议”这层到底是什么。\n\nReAct = Reasoning（推理）+ Acting（行动）。它的循环是：\n    Thought（想）→ Action（做）→ Observation（看结果）→ Thought（再想）→ …\n    直到模型认为任务完成，输出 Final Answer。\n\n【为什么不用 function-calling，而是自己解析文本？】\n    练习 1\u002F2\u002F6 用的是模型的“原生工具调用”，循环其实被 SDK 藏起来了。\n    本练习刻意退回到最朴素的方式：让模型用纯文本按固定格式回复，\n    我们自己解析、自己执行、自己把结果拼回去——这样你能亲眼看到\n    “循环”是我们用一个 while 转起来的，agent 的心跳就是这段代码。\n\n整个循环（run 函数）核心约 50 行，不依赖任何 agent 框架。\n\"\"\"\n\nimport json\nimport os\nimport re\n\nfrom dotenv import load_dotenv\nfrom openai import OpenAI\n\nload_dotenv()\n\nclient = OpenAI(\n    api_key=os.environ[\"DEEPSEEK_API_KEY\"],\n    base_url=os.environ[\"DEEPSEEK_BASE_URL\"],\n)\nMODEL = os.environ[\"MODEL\"]\n\n\n# ── 工具：我们的“手”。注意这里没有用任何 schema，纯 Python 字典登记 ──────\ndef get_weather(city: str) -> str:\n    fake = {\"北京\": 26, \"上海\": 24, \"广州\": 30}\n    temp = fake.get(city)\n    return f\"{city}当前气温 {temp} 度\" if temp is not None else f\"{city}气温未知\"\n\n\ndef calculator(expression: str) -> str:\n    # 只允许数字和基本运算符，避免 eval 执行危险代码（安全意识，见本阶段“致命三角”警示）\n    if not re.fullmatch(r\"[\\d\\s+\\-*\u002F().]+\", expression):\n        return \"错误：表达式含非法字符\"\n    return str(eval(expression))\n\n\nTOOLS = {\"get_weather\": get_weather, \"calculator\": calculator}\n\n# ── 系统提示：教模型按固定格式说话，这样我们才能解析 ──────────────────────\n# 这份 prompt 就是 ReAct 的“协议”：规定模型要么给 Action，要么给 Final Answer。\nSYSTEM_PROMPT = \"\"\"你是一个会使用工具的助手。请严格按以下格式逐步思考：\n\nThought: 你的推理\nAction: 要调用的工具名（只能是 get_weather 或 calculator）\nAction Input: 传给工具的参数（JSON，如 {\"city\": \"北京\"} 或 {\"expression\": \"30-26\"}）\n\n我会把工具执行结果作为 Observation 返回给你。\n你可以重复 Thought\u002FAction\u002FAction Input 多轮。\n当你已经能回答用户时，改为输出：\n\nThought: 你的推理\nFinal Answer: 给用户的最终回答\n\n一次只输出一个 Action，不要自己编造 Observation。\"\"\"\n\n\ndef run(question: str, max_steps: int = 6) -> str:\n    \"\"\"ReAct 主循环。这就是 agent 的心跳。\"\"\"\n    messages = [\n        {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n        {\"role\": \"user\", \"content\": question},\n    ]\n\n    # max_steps 是“熔断”：万一模型陷入循环，也不会无限调用下去（重的要安全护栏）\n    for step in range(1, max_steps + 1):\n        # ① 想 + 决定做什么：让模型产出 Thought\u002FAction 或 Final Answer\n        reply = client.chat.completions.create(\n            model=MODEL, messages=messages, temperature=0\n        ).choices[0].message.content\n        print(f\"\\n──── 第 {step} 步 ────\\n{reply}\")\n\n        # ② 终止条件：模型给出 Final Answer，循环结束（agent 自己决定何时停）\n        if \"Final Answer:\" in reply:\n            return reply.split(\"Final Answer:\", 1)[1].strip()\n\n        # ③ 解析模型选的工具和参数\n        action = re.search(r\"Action:\\s*(\\w+)\", reply)\n        action_input = re.search(r\"Action Input:\\s*(\\{.*\\})\", reply, re.DOTALL)\n        if not action or not action_input:\n            # 模型没按格式来，把要求重申一遍，让它纠正（轻量纠错）\n            messages.append({\"role\": \"assistant\", \"content\": reply})\n            messages.append({\"role\": \"user\", \"content\": \"请严格按 Action \u002F Action Input 格式输出。\"})\n            continue\n\n        tool_name = action.group(1)\n        args = json.loads(action_input.group(1))\n\n        # ④ 做：真正执行的是我们的代码，不是模型\n        if tool_name in TOOLS:\n            observation = TOOLS[tool_name](**args)\n        else:\n            observation = f\"错误：没有名为 {tool_name} 的工具\"\n        print(f\"Observation: {observation}\")\n\n        # ⑤ 看结果 → 拼回对话，进入下一轮“想”。循环就这样转起来了。\n        messages.append({\"role\": \"assistant\", \"content\": reply})\n        messages.append({\"role\": \"user\", \"content\": f\"Observation: {observation}\"})\n\n    return \"（达到最大步数仍未得出最终答案）\"\n\n\nif __name__ == \"__main__\":\n    # 这个任务需要循环转好几圈：查北京气温 → 查广州气温 → 相减 → 给答案\n    task = \"北京和广州现在的气温相差多少度？\"\n    print(f\"【任务】{task}\")\n    answer = run(task)\n    print(f\"\\n【最终答案】{answer}\")\n",[30],{"type":18,"tag":31,"props":32,"children":33},"code",{"__ignoreMap":7},[34],{"type":24,"value":28},{"title":7,"searchDepth":36,"depth":36,"links":37},2,[38],{"id":21,"depth":39,"text":21},3,"markdown","content:articles:agent:04-react-text-protocol.md","content","articles\u002Fagent\u002F04-react-text-protocol.md","articles\u002Fagent\u002F04-react-text-protocol","md",1781678620538]