AI Agent 不是复读机:一个“计划-观察-执行“闭环,把大模型从聊天框变成能闭环干活的实习生

一、那个让你想把显示器关掉的场景

凌晨两点,你让 AI Agent "给订单系统写一个接口测试脚本,覆盖查询和退款两个接口"。三分钟后,它洋洋洒洒回了 200 行 Python,看着挺像那么回事。你一跑,满屏 NameErrorKeyError。你把报错贴回去,它道歉、改了一版变量名,错误原封不动。又贴回去,它开始给你讲测试金字塔。

三个小时过去,Agent 在聊天框里演了一场独角戏,你的 Jira 进度还是 0。

这不是模型不够聪明,是你把它当成了一只"会说话的鹦鹉",却指望它自己伸手开柜子、拿文件、用工具、改错误。2026 年的 AI 落地早已过了"接个 API 就能 demo"的阶段,现在拼的是:怎么把大模型从"一次性回答"变成"能闭环干活的 Agent"。

这篇文章不聊玄学,直接给你一个在生产环境验证过的模式——ReAct(Reasoning + Acting)循环,再加一个可插拔的工具注册表。读完你能用 200 行 Python 搭一个骨架,让国产大模型(DeepSeek / 通义 / 文心 / GLM / Kimi / 豆包都能接)真的会查数据库、调接口、看报错、再重试。

二、问题出在哪:不是模型笨,是你没给它"手"和"脑子"

先做一个基本判断:大模型本质上是一个"文本预测机"。你给一段 prompt,它预测下一段最可能像答案的文本。这个能力在写诗、总结、翻译上强得离谱,但在"完成一件多步骤任务"上有个致命短板——它没有状态、没有工具、没有试错循环

看一张对比表,左边是多数团队现在的用法,右边是 ReAct Agent 落地后的用法:

维度

裸用 LLM(Before)

ReAct Agent(After)

任务理解

一次性 prompt,上下文有限

每轮根据观察重新思考,动态调整计划

外部工具

没有,只能瞎编

显式注册工具,模型按需调用

错误处理

出了错继续胡扯

把报错喂回模型,让它自己改

多步骤任务

一步错,步步错

每步有观察、有校验、可回退

可控性

黑盒输出

中间过程可观测、可审计、可干预

核心差别就一句话:裸用 LLM 是在"问答案",Agent 是在"跑任务"

再用一张图把架构差异画清楚:

三、ReAct 到底是什么:把"推理"和"行动"串成一个环

ReAct 不是某个框架的名字,是一种思想:把大模型的 "Reasoning(推理)" 和 "Acting(行动)" 交替执行,形成一个循环。

一次循环只有三步:

  1. Thought(思考):模型根据当前目标和已有观察,决定下一步要做什么。
  1. Action(行动):模型输出一个工具调用指令,比如 query_order(order_id="A1001")
  1. Observation(观察):程序执行这个工具调用,把真实结果返回给模型。

如果任务没完成,就再来一轮:基于新的观察继续思考、继续行动。直到模型认为可以给出最终答案,循环结束。

用图看更直观:

关键点:模型不直接操作外部世界,它只输出"我想调用某个工具";真正执行工具的是你的代码。这样你既保留了大模型的推理能力,又把所有外部调用收拢在可控的代码路径里。

四、200 行 Python 骨架:能直接抄的 Agent

下面这段代码不依赖 LangChain、不依赖 LlamaIndex,纯标准库 + requests。目标只有一个:让你看清楚 ReAct 循环的每一根骨头。

4.1 工具注册表

用装饰器把普通函数注册成 Agent 可调用的工具,同时自动生成 JSON Schema:

import inspect
import json
from typing import Callable, Any

class ToolRegistry:
    def __init__(self):
        self.tools: dict[str, Callable] = {}
        self.specs: list[dict] = []

    def register(self, description: str):
        """装饰器:把函数注册为 Agent 可调用的工具。"""
        def wrapper(fn: Callable) -> Callable:
            name = fn.__name__
            self.tools[name] = fn
            self.specs.append({
                "type": "function",
                "function": {
                    "name": name,
                    "description": description,
                    "parameters": self._build_schema(fn),
                },
            })
            return fn
        return wrapper

    @staticmethod
    def _build_schema(fn: Callable) -> dict:
        sig = inspect.signature(fn)
        props: dict[str, dict] = {}
        required: list[str] = []
        type_map = {int: "integer", float: "number", bool: "boolean", str: "string"}
        for pname, param in sig.parameters.items():
            t = param.annotation if param.annotation is not inspect.Parameter.empty else str
            props[pname] = {"type": type_map.get(t, "string")}
            if param.default is inspect.Parameter.empty:
                required.append(pname)
        return {"type": "object", "properties": props, "required": required}

    def call(self, name: str, arguments: dict) -> str:
        if name not in self.tools:
            return json.dumps({"error": f"工具 {name} 不存在"}, ensure_ascii=False)
        try:
            result = self.tools[name](**arguments)
            return json.dumps(result, ensure_ascii=False)
        except Exception as e:
            # 关键:把报错结构化地返回,让模型下一轮能自己修
            return json.dumps({"error": str(e)}, ensure_ascii=False)

registry = ToolRegistry()

@registry.register(description="查询指定城市的当前天气,参数 city 为城市名")
def get_weather(city: str) -> dict:
    # 生产环境这里接真实天气 API;示例用 mock
    return {"city": city, "weather": "晴", "temperature": 26}

@registry.register(description="根据订单号查询订单状态,参数 order_id 为订单编号")
def query_order(order_id: str) -> dict:
    mock_db = {"A1001": "已发货", "A1002": "待付款", "A1003": "已退款"}
    return {"order_id": order_id, "status": mock_db.get(order_id, "未找到")}

这里有两个设计意图:

  • Schema 从类型注解自动生成:少写一堆 JSON,新增工具只要写普通 Python 函数。
  • 工具报错必须返回给模型:如果执行失败,Agent 下一轮能基于错误信息改参数或换工具,而不是直接崩溃。

4.2 大模型调用层

requests 直接调 DeepSeek,兼容任意支持 OpenAI 接口格式的国产模型:

import os
import requests

DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
BASE_URL = "https://api.deepseek.com/v1"


def chat(messages: list[dict], tools: list[dict] | None = None) -> dict:
    headers = {
        "Authorization": f"Bearer {DEEPSEEK_API_KEY}",
        "Content-Type": "application/json",
    }
    body = {
        "model": "deepseek-chat",
        "messages": messages,
        "temperature": 0.3,  # Agent 需要稳定,别让它太发散
    }
    if tools:
        body["tools"] = tools

    resp = requests.post(
        f"{BASE_URL}/chat/completions",
        headers=headers,
        json=body,
        timeout=60,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]

注意 temperature 别设太高。Agent 的每一轮思考都要稳定,创意可以留给最终文案,推理步骤不能飘。

4.3 ReAct 循环

核心逻辑:解析模型输出里的 TOOL_CALL 标记,执行工具,把结果塞回上下文,继续下一轮。

import re

SYSTEM_PROMPT = """你是一个智能助手,解决问题时必须遵循 ReAct 模式:
1. Thought:先分析当前情况,说明你要做什么。
2. Action:如果需要工具,按格式输出 TOOL_CALL: {"name": "工具名", "arguments": {...}}。
3. Observation:工具返回会自动提供给你,用于下一轮思考。
如果已经得到足够信息,直接输出 FINAL_ANSWER: 你的最终答案。
"""


def run_agent(query: str, max_turns: int = 10) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": query},
    ]

    for turn in range(max_turns):
        msg = chat(messages, registry.specs)
        content = msg.get("content", "") or ""
        messages.append({"role": "assistant", "content": content})

        # 终止条件 1:模型直接给最终答案
        if "FINAL_ANSWER:" in content:
            return content.split("FINAL_ANSWER:", 1)[1].strip()

        # 终止条件 2:模型没调用工具也没给答案,提醒它
        match = re.search(r'TOOL_CALL:\s*(\{.*?\})\s*$', content, re.DOTALL)
        if not match:
            messages.append({
                "role": "user",
                "content": "请按格式调用工具,或直接给出 FINAL_ANSWER。",
            })
            continue

        # 执行工具调用
        call = json.loads(match.group(1))
        name = call.get("name")
        args = call.get("arguments", {})
        observation = registry.call(name, args)

        # 把观察结果写回上下文
        messages.append({
            "role": "user",
            "content": f"Observation: {observation}",
        })

    return "达到最大轮次,任务未完成。"


if __name__ == "__main__":
    answer = run_agent("帮我查一下订单 A1001 的状态,再看看南京今天天气怎么样")
    print(answer)

跑起来之后,一次典型的交互长这样:

Turn 1  Thought: 用户问了两个事,先查订单 A1001。
        Action: TOOL_CALL: {"name":"query_order","arguments":{"order_id":"A1001"}}
        Observation: {"order_id": "A1001", "status": "已发货"}

Turn 2  Thought: 订单已发货,再查南京天气。
        Action: TOOL_CALL: {"name":"get_weather","arguments":{"city":"南京"}}
        Observation: {"city": "南京", "weather": "晴", "temperature": 26}

Turn 3  Thought: 两个信息都有了,给出最终答案。
        FINAL_ANSWER: 订单 A1001 已发货;南京今天晴,26℃。

每一轮你都能打印出来,问题卡在哪一眼就能看见。这比"黑盒生成一个答案"好调试十倍。

五、三个工程化细节,决定 Agent 能不能上生产

代码能跑只是 20%。把 Agent 搬进生产环境,下面这三个坑我踩过不止一次。

5.1 工具描述就是 prompt,写不好模型会乱调

装饰器里的 description 不是给人类看的注释,它会原封不动塞进模型上下文。如果你写:

@registry.register(description="查订单")
def query_order(order_id: str) -> dict:
    ...

模型根本搞不清 order_id 是什么格式。正确写法要包含:工具是干什么的、参数格式、返回什么、什么时候不该用

@registry.register(
    description="根据订单号查询订单状态。参数 order_id 为大写字母+数字组合,"
                "例如 A1001。仅用于回答与订单状态相关的问题。"
)
def query_order(order_id: str) -> dict:
    ...

这个描述会占用上下文 token,太啰嗦费钱,太简单误调。需要在真实问题上反复测。

5.2 观察结果要截断、要结构化

真实 API 返回可能是几百 KB 的 JSON。如果你把整个 JSON 原样塞回 prompt,几轮之后上下文就爆炸了。生产上要做两件事:

  • 截断:只把模型需要的字段抽出来返回。
  • 结构化:统一用 JSON,别让模型去猜自然语言里的关键信息。

例如天气接口返回了 30 个字段,Agent 其实只需要 temperatureweather

5.3 循环必须有上限和兜底

max_turns 不是摆设。模型可能在某个循环里反复调用同一个工具,陷入死循环。上线前必须配:

  • 最大轮次(例如 10)。
  • 单工具重复调用检测(同一工具同一参数连续 3 次,强制退出)。
  • 兜底答案:循环耗尽后返回一句"系统无法完成,请人工介入"。

六、Before / After:效果到底差多少?

回到开篇那个"写接口测试脚本"的场景。用 ReAct Agent 之后,流程会变成:

  1. Agent 先读项目目录,识别测试框架(pytest)。
  1. 调用文件读取工具看已有接口封装。
  1. 生成脚本。
  1. 调用本地 shell 工具执行 pytest。
  1. 观察报错,把报错塞回上下文,改脚本。
  1. 直到 pytest 通过,给出最终脚本。

对比效果:

指标

裸用 LLM

ReAct Agent

多步骤任务完成率

30%~40%

80%~90%

遇到报错能否自修复

不能,越改越偏

能,把报错当观察喂回去

工具调用可控性

白名单 + 审计日志

新增业务能力成本

改 prompt,提示词工程

写一个 Python 函数

可观测性

黑盒

每轮 Thought/Action/Observation 都可打印

七、结语

2026 年的 AI 竞争,已经从"谁的模型参数更大"转向了"谁能把模型更安全、更稳定、更可维护地接到真实业务里"。ReAct 不是银弹,但它解决了一个最基础的问题:让大模型从"说答案"变成"做事情"

这个 200 行的骨架可以扩展成很多形态:接 MCP Server、接 Playwright 做 UI 测试、接数据库查业务数据、接代码仓库做自动化重构。核心套路不变——思考、行动、观察、再思考

拿去跑一遍,把 get_weather 换成你的真实业务接口,你就知道 Agent 和聊天机器人的区别了。


参考与备注

  1. ReAct 论文:Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023.
  1. 工具调用(Function Calling)基础概念:OpenAI / DeepSeek / 智谱 / 阿里云官方 API 文档。
  1. MCP(Model Context Protocol)协议:可作为 ReAct Agent 工具层的进一步标准化方向,参考 https://modelcontextprotocol.io/specification/。
  1. 本文示例基于 Python 3.12 + DeepSeek API 编写,替换 BASE_URLmodel 即可接入其他兼容 OpenAI 接口的国产大模型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值