一个最简化的 AI Agent 核心实现 —— ReAct 循环 + 工具调用 + 可观测 + 持久化

70 阅读5分钟

依赖:openai / 模块:AgentTrace(可观测)、Compaction(上下文压缩)、Persistence(持久化)


一、整体架构

agent.py (~300 行)
  ├── trace.py    ── 可观测性:Span 树 + Trace 日志
  ├── compaction.py ── 上下文压缩:超长时自动摘要
  ├── persistence.py ── 持久化:会话保存/恢复 (Store → jsonl)
  └── openai          ── LLM 调用 (兼容接口)

一条线串起 4 个层次 —— 最简 Agent → 可观测 → 上下文管理 → 持久化 —— 每一层只引入一个类/文件,不搞抽象。


二、工具系统

2.1 工具即函数

两个工具,直白到不需要解释:

def search_web(query: str) -> str:
    """模拟搜索,实际可接 Google/Bing API"""
    fake_db = {"北京": "北京今天晴天,25°C~32°C", ...}
    return fake_db.get(query, f"未找到'{query}'的相关结果")

def calculate(expression: str) -> str:
    """安全的数学计算"""
    ...

2.2 JSON Schema 注册表

遵循 OpenAI Function Calling 规范,每个工具的类型、名称、描述、参数 schema 是一个 dict:

TOOLS = [
    {"type": "function", "function": {"name": "search_web", "description": "...", "parameters": {...}}},
    {"type": "function", "function": {"name": "calculate",   "description": "...", "parameters": {...}}},
]

TOOL_MAP = {"search_web": search_web, "calculate": calculate}

设计要点:LLM 只读 TOOLS 的 JSON Schema 做推理,运行时由 TOOL_MAP 分发执行——模型无法直接跑代码,安全隔离。


三、核心循环:run_agent()

3.1 流程图

用户输入
  │
  ▼
┌──────────────────────────────────────┐
│  messages: [system, user_input, ...]  │
└──────────────────┬───────────────────┘
                   ▼
          ┌────────────────┐
          │  LLM 思考      │  ← tools=TOOLS, tool_choice="auto"
          │  (任意模型)    │
          └───────┬────────┘
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
   有 tool_calls?      无 tool_calls?
        │                   │
        ▼                   ▼
 ┌──────────────┐    ┌──────────────┐
 │ 执行工具      │    │ 返回最终答案  │
 │ 结果→messages │    └──────────────┘
 └──────┬───────┘
        │
        └──→ 循环 (max_steps=5)

3.2 核心代码(~50 行)

def run_agent(user_input: str, max_steps: int = 5) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_input},
    ]

    for step in range(max_steps):
        response = client.chat.completions.create(
            model="your-model-name",
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",   # LLM 自己决定要不要调工具
        )
        msg = response.choices[0].message

        if not msg.tool_calls:           # 情况 A:直接回答
            return msg.content

        messages.append(msg)             # 情况 B:执行工具

        for tool_call in msg.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            result = TOOL_MAP[name](**args)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })
        # 循环回去,LLM 看到工具结果后继续思考

    return "⚠️ 达到最大步数限制"

3.3 设计要点

要点说明
tool_choice="auto"LLM 自主决策,不强制调用——简单的问候直接答,需要信息才调工具
对话历史 = 状态messages 数组是 Agent 的全部「记忆」,无需额外状态管理
最大步数 5硬上限防无限循环,适合简单查询,复杂任务可调高
纯文本交互无 UI 依赖,stdio 打印每步的执行过程

四、增强版:run_agent_with_trace()

在基础循环上叠加了三层能力:

4.1 可观测 —— AgentTracer

# 每次 LLM 调用:记录延迟 + 输入/输出
llm_span = tracer.log_llm_call(messages, response, elapsed)

# 每次工具调用:记录参数 + 结果 + 耗时
tool_span = tracer.log_tool_call(name, args, result, elapsed)

# 构建 Span 树
run = tracer.start_run(run_id, user_input)
run.children.append(llm_span)
run.children.append(tool_span)

4.2 上下文压缩 —— ContextManager

# 每轮结束后检查是否需要压缩
messages = ctx.maybe_compact(messages, client)
# 内部逻辑:调用 LLM 生成历史摘要,替换旧的 messages

4.3 持久化 —— PersistenceManager

# 创建新会话
session_id = pm.new_session_id()          # 时间戳 + 随机哈希

# 每轮自动保存(防崩溃)
pm.save_messages(session_id, messages)

# 会话完成时保存完整状态
pm.save_session(session_id, messages, ctx.summary, tracer.to_dicts(), user_input)

# 恢复历史会话
state = pm.load_session(session_id)      # → {messages, summary, traces}

4.4 会话恢复

def resume_session(session_id, new_input, *, client, ...):
    """恢复历史会话并继续对话"""
    state = pm.load_session(session_id)
    messages = state["messages"]
    ctx.restore(state["summary"])
    # 然后走常规 run_agent 循环

4.5 完整版本

def run_agent_with_trace(
    user_input: str,
    *,
    tracer: AgentTracer,
    client: OpenAI,
    ctx: ContextManager,
    pm: PersistenceManager,
    session_id: str | None = None,
    max_steps: int = 5,
) -> str:
    """
    带 可观测 + 压缩 + 持久化 的 Agent 循环。
    在原来 run_agent_with_trace 上直接加持久化能力。
    """

    # ── 创建或恢复会话 ─────────────────────────────
    if session_id:
        state = pm.load_session(session_id)
        messages = state["messages"]
        ctx.restore(state["summary"])
        print(f"📂 恢复会话 {session_id},已有 {len(messages)} 条消息")
    else:
        session_id = pm.new_session_id()
        messages = []

    # 追加用户输入 + 确保 system prompt 在第一位
    messages.append({"role": "user", "content": user_input})
    if not messages or messages[0].get("role") != "system":
        messages.insert(0, {"role": "system", "content": SYSTEM_PROMPT})

    # ── Trace 开始 ─────────────────────────────────
    run_id = f"run_{os.urandom(6).hex()}"
    run = tracer.start_run(run_id, user_input)

    # ── 主循环 ─────────────────────────────────────
    for step in range(max_steps):
        print(ctx.stats(messages))

        t0 = time.time()
        try:
            response = client.chat.completions.create(
                model="deepseek-v4-pro",
                messages=messages,
                tools=TOOLS,
                tool_choice="auto",
            )
        except Exception as e:
            llm_err = Span(
                span_id=f"llm_{os.urandom(4).hex()}",
                type="llm_call",
                start_time=t0,
                end_time=time.time(),
                error=str(e),
            )
            run.children.append(llm_err)
            raise

        llm_span = tracer.log_llm_call(messages, response, time.time() - t0)
        run.children.append(llm_span)

        msg = response.choices[0].message

        # ✅ 新增:统一转 dict 再 append
        messages.append(to_dict(msg))

        # 无需工具 → 输出答案
        if not msg.tool_calls:
            run.end_time = time.time()
            # ✅ 新增:最终保存
            pm.save_session(
                session_id, messages, ctx.summary,
                tracer.to_dicts(), user_input,
            )
            print(tracer.summary(run))
            print(f"💾 会话已保存: {session_id}")
            return msg.content or ""

        # 执行工具
        for tool_call in msg.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)

            t0 = time.time()
            try:
                result = TOOL_MAP[name](**args)
            except Exception as e:
                result = f"工具执行错误: {e}"

            tool_span = tracer.log_tool_call(name, args, result, time.time() - t0)
            run.children.append(tool_span)

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

        # ✅ 新增:压缩检查
        messages = ctx.maybe_compact(messages, client)

        # ✅ 新增:每轮自动保存(防崩溃)
        pm.save_messages(session_id, messages)

    # 达到最大步数
    run.end_time = time.time()
    pm.save_session(session_id, messages, ctx.summary, tracer.to_dicts(), user_input)
    print(tracer.summary(run))
    print(f"💾 会话已保存: {session_id}")
    return "⚠️ 达到最大步数限制"

五、整体设计评价

优点

  1. 极简:核心 Agent 循环 ~50 行,可运行、可调试、可教学
  2. 渐进增强:基础 → trace → compaction → persistence,每层独立模块化
  3. 安全:工具执行在 host 端,LLM 只输出 tool_call JSON,无法直接执行代码
  4. 可恢复:full session 持久化 + 单轮自动保存,崩溃后可续跑
  5. OpenAI 兼容base_url 可替换为任何兼容 API(火山引擎 / DeepSeek / 本地 vLLM)

局限

  1. 无流式输出:所有调用都是非流式的,用户需等完整响应
  2. 同步阻塞:工具调用串行,不能并发执行多个独立工具
  3. 工具少:仅 2 个 mock 工具,生产需扩展
  4. 无工具错误恢复:工具执行失败只返回错误字符串,无重试/降级

适用场景

  • 学习 Agent 核心概念(ReAct 循环、工具调用、Function Calling)
  • 快速原型验证(有新想法时 5 分钟跑起来)
  • 教学演示(每一步可 print 观察)

扩展方向

方向改动点
流式输出client.chat.completions 加 stream=True
并行工具执行asyncio.gather 并发跑 tool_calls
工具重试错误时追加 "工具执行错误,请重试" 到 messages
多 Agent多个 run_agent 实例共享 messages + 角色路由
MCP 集成TOOLS 动态拉取 MCP server 的 tools/list

六、模块依赖图

agent.py
  ├─ openai               (外部)
  ├─ trace.py
  │    └─ dataclass Span   (标准库)
  ├─ compaction.py
  │    └─ + openai         (调用 LLM 做摘要)
  └─ persistence.py
       └─ Store (jsonl)    (标准库)

全部 4 个文件、< 800 行代码、0 个第三方框架依赖(仅 openai 一个 pip 包)。