📅 2026-07-18 | 🏷️ Python · AI Agent 方向 | ⏱️ 建议 5h | 🎯 Agent 面试的灵魂——ReAct 手写 + Function Calling 设计


📌 今日知识地图

Agent 面试全景
│
├── 模块一:Prompt Engineering
│   ├── Zero-shot / Few-shot / CoT / ToT / Self-Consistency
│   └── 为什么 ReAct 是 Agent 的基石?
│
├── 模块二:Function Calling 深度拆解
│   ├── OpenAI Function Calling 协议
│   ├── JSON Schema 工具定义(用 Pydantic 生成)
│   ├── 并行调用、错误处理、强制调用
│   └── Tool Choice 策略
│
├── 模块三:Agent 核心架构(四要素)
│   ├── Planning(规划):任务分解 / 反思 / 自适应
│   ├── Memory(记忆):短期 + 长期 + 工作记忆
│   ├── Tools(工具):注册 / 校验 / 重试 / Fallback
│   └── Action(行动):执行 / 解析 / 循环终止
│
├── 模块四:ReAct 框架完整手写实现
│   ├── Thought → Action → Observation 循环
│   ├── 状态管理 & 循环终止条件
│   └── 完整可运行代码
│
└── 模块五:Agent 决策策略对比
    └── ReAct vs Plan-and-Execute vs Reflexion vs LATS

模块一:Prompt Engineering — 从入门到 Agent 基石

1.1 一张图看懂提示工程的进化

Zero-shot              "翻译成英文:你好"
  ↓ 效果不够好?给示例
Few-shot               "Q: 苹果 → A: Apple   Q: 你好 → A:"
  ↓ 推理题还是错?让模型展示推理过程
Chain-of-Thought (CoT) "一步一步思考:小明有 5 个苹果..."
  ↓ 一条思路可能不对?多条思路投票
Self-Consistency       "用 5 种不同的思路解这道题,取多数答案"
  ↓ 复杂问题需要回溯?用树形结构
Tree-of-Thought (ToT)  生成多个思路 → 评估 → 选最优分支 → 继续
  ↓ 需要跟外部环境交互?
ReAct                  思考(Thought) → 行动(Action) → 观察(Observation) → 循环

1.2 各策略详解

Zero-shot:不给示例,直接问。

Prompt: 判断这条评论的情感是正面还是负面:"等了两个小时才上菜,服务员态度也很差。"
Output: 负面

Few-shot:给几个示例,让模型学会格式。本质是 In-Context Learning——不更新模型参数,通过示例激活模型的模式匹配能力。

评论:"环境很好,菜品精致" → 情感:正面
评论:"价格太贵了,不值这个价" → 情感:负面
评论:"无功无过,随便吃吃还行" → 情感:中性
评论:"等了两小时,态度还差" → 情感:
Output: 负面

面试话术:“Few-shot 的关键技巧:示例覆盖目标场景多样性(正/负/中都要有),格式高度一致(模型对格式极其敏感),示例顺序有影响(近因效应,重要示例放最后)。”

Chain-of-Thought (CoT):让模型"出声思考"。对于复杂推理任务,仅靠"请一步一步思考"就能大幅提升准确率。原理是自回归生成中前面的 token 影响后面——推理过程本身充当了"工作记忆",把隐含推理变成了显式文字。

Prompt: 小明有 5 个苹果,小红给了他 3 个,他又吃了 2 个,现在有几个?请一步一步推理。

Output:
第一步:小明初始有 5 个苹果
第二步:小红给了他 3 个 → 5 + 3 = 8 个
第三步:他吃了 2 个 → 8 - 2 = 6 个
答案:6 个

Self-Consistency:一个思路不够,多个思路投票。对同一问题采样多条 CoT 推理路径(调高 temperature),多数投票选答案。核心洞见:模型不是一直错的——正确答案在不同采样中出现最频繁。代价是计算量翻 N 倍(N 通常 5-10)。

Tree-of-Thought (ToT):把推理建模为树搜索。每步生成多个候选思路 → 评估每个分支 → 选最有希望的分支深入 → 可以回溯。比 CoT 更系统,但延迟也更高。

1.3 ReAct:为什么它是所有 Agent 的基石?

ReAct = Reasoning + Acting = 将推理(CoT)和行动(工具调用)交织在一起。

Q: 北京今天的气温比上海高多少?

传统 CoT(只会想,不会查):
  A: 我需要知道北京和上海的气温...但我不知道,让我猜一下...

ReAct(边想边查):
  Thought: 我需要查北京和上海当前的气温
  Action: search_weather("北京")
  Observation: 北京当前 35°C
  Thought: 北京 35°C,还需要上海的气温
  Action: search_weather("上海")
  Observation: 上海当前 32°C
  Thought: 北京 35°C, 上海 32°C, 北京比上海高 3°C
  Final Answer: 北京比上海高 3°C

ReAct 论文(Yao et al., 2022)的核心发现:单独用 CoT 会胡编乱造(因为不知道事实),单独用 Action 会盲目试错(因为没有推理指引方向)。两者结合产生 1+1>2 的效果。 消融实验证明,去掉 Reasoning 后退化为盲目试错,去掉 Acting 后退化为胡编乱造。

面试加分点:“这正是所有现代 Agent 框架——LangChain AgentExecutor、OpenAI Assistants、Anthropic Tool Use、字节 Coze——的底层范式。”


模块二:Function Calling — LLM 长出双手

2.1 最关键的认知纠正

Function Calling ≠ LLM 在调用函数

LLM 只负责输出结构化 JSON(选哪个工具、传什么参数)
→ 你的代码解析 JSON
→ 你的代码执行真正的函数
→ 结果以 tool role 消息喂回 LLM
→ LLM 继续生成

LLM 的角色 = 路由器 + 参数提取器(不是执行器!)

2.2 完整调用流程(五步)

第一步:发送请求,tools 参数告诉 LLM 有哪些工具可用。

POST /chat/completions
{
  "model": "gpt-4",
  "messages": [{"role": "user", "content": "北京天气怎么样?"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {"type": "string", "description": "城市名"},
          "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
        },
        "required": ["city"]
      }
    }
  }],
  "tool_choice": "auto"
}

第二步:LLM 返回 tool_calls(JSON 格式,finish_reason=“tool_calls”)。

{
  "choices": [{
    "finish_reason": "tool_calls",
    "message": {
      "role": "assistant",
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
        }
      }]
    }
  }]
}

第三步:你的代码执行 get_weather("北京", "celsius")

第四步:把执行结果以 tool role 消息喂回 LLM。

messages += [
  assistant_message_with_tool_calls,
  {"role": "tool", "tool_call_id": "call_abc123",
   "content": "北京今天晴,35°C,湿度 60%"}
]

第五步:LLM 基于结果生成最终回复:“北京今天晴天,气温 35°C,湿度 60%,注意防暑降温。”

2.3 用 Pydantic 自动生成 JSON Schema(Agent 最佳实践)

from pydantic import BaseModel, Field
from typing import Literal

class WeatherParams(BaseModel):
    """天气查询参数"""
    city: str = Field(..., description="城市名", min_length=1, max_length=100)
    unit: Literal["celsius", "fahrenheit"] = Field(default="celsius")

class SearchParams(BaseModel):
    """搜索参数"""
    query: str = Field(..., description="搜索关键词", min_length=1, max_length=500)
    max_results: int = Field(default=5, ge=1, le=20)

class ToolRegistry:
    """工具注册器 —— Agent 开发的核心组件"""

    def __init__(self):
        self._tools = {}

    def register(self, name: str, description: str,
                 params_model: type[BaseModel], handler: callable):
        self._tools[name] = {
            "openai_spec": {
                "type": "function",
                "function": {
                    "name": name,
                    "description": description,
                    "parameters": params_model.model_json_schema(),
                }
            },
            "handler": handler,
            "params_model": params_model,
        }

    def get_openai_tools(self) -> list[dict]:
        """返回 OpenAI API 格式的 tools 列表"""
        return [t["openai_spec"] for t in self._tools.values()]

    def execute(self, name: str, arguments: dict):
        """执行工具:Pydantic 校验 → 调用 handler → 返回结果"""
        if name not in self._tools:
            return f"错误:未知工具 {name}"
        tool = self._tools[name]
        params = tool["params_model"](**arguments)  # Pydantic 自动校验!
        return tool["handler"](**params.model_dump())

# 使用示例
registry = ToolRegistry()
registry.register("get_weather", "查询指定城市的天气", WeatherParams,
                  lambda city, unit: f"{city}天气:晴,35°C")
registry.register("search", "搜索互联网", SearchParams,
                  lambda query, max_results: f"搜索'{query}'返回{max_results}条")

# 一行生成所有工具的 OpenAI 格式定义
print(registry.get_openai_tools())

2.4 Tool Choice 四种策略

tool_choice 行为 适用场景
"auto"(默认) LLM 自己决定是否调工具 大多数场景
"none" 强制不调工具,只生成文本 不需要工具的闲聊
"required" 必须调一个工具 Agent 中间步骤(必须获取外部信息)
{"type":"function","function":{"name":"xxx"}} 强制调指定工具 已知需要用哪个工具

2.5 并行工具调用 & 错误处理

import asyncio, json

async def agent_loop(messages, tools, max_iterations=10):
    """Agent 主循环 —— 支持并行工具调用"""
    for i in range(max_iterations):
        response = await llm_call(messages, tools)
        msg = response.choices[0].message

        # LLM 给出最终回复(不再调工具)
        if not msg.tool_calls:
            return msg.content

        messages.append(msg)

        # 并行执行所有工具调用(这是 Agent 提速的关键!)
        async def exec_one(tc):
            func_name = tc.function.name
            arguments = json.loads(tc.function.arguments)
            try:
                return await registry.execute(func_name, arguments)
            except Exception as e:
                return f"工具执行失败: {e}"

        results = await asyncio.gather(
            *[exec_one(tc) for tc in msg.tool_calls],
            return_exceptions=True  # 某个失败不拖累其他
        )

        # 把所有结果喂回 LLM
        for tc, result in zip(msg.tool_calls, results):
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": str(result),
            })

    return "达到最大迭代次数"

模块三:Agent 核心架构 — 四要素深度拆解

3.1 架构总览

                    ┌──────────────┐
  用户输入 ────────→│   Planning   │←──────┐
                    │  任务分解    │       │
                    └──────┬───────┘       │
                           │               │
              ┌────────────┼───────────────┤
              ↓            ↓               │
        ┌──────────┐ ┌──────────┐   ┌─────┴─────┐
        │  Memory  │ │  Tools   │   │  Action   │
        │ 短期+长期 │ │ 工具集   │   │ 执行+解析  │
        └──────────┘ └──────────┘   └───────────┘
              │            │               │
              └────────────┴───────────────┘
                           │
                    回到 Planning (反思调整)

3.2 Planning(规划)— Agent 的大脑皮层

层次 做什么 代表方法 例子
任务分解 把大任务拆成子任务 CoT, ReAct, Plan-and-Solve “写市场报告” → 搜索数据 → 分析竞品 → 撰写
反思 评估之前行动,修正错误 Self-Refine, Reflexion “第一步搜索结果不相关,换关键词重搜”
自适应 根据进度调整后续计划 Re-prompting, Re-planning “已花3步在搜索上,先基于现有信息写初稿”

3.3 Memory(记忆)— Agent 不健忘的关键

三层记忆架构:

┌─────────────────────────────────────┐
│         短期记忆(上下文窗口)        │
│   当前对话的最近 N 条消息             │
│   容量:~128K tokens(GPT-4o)       │
│   特点:精确,但容量有限              │
├─────────────────────────────────────┤
│         工作记忆(Scratchpad)        │
│   当前任务的中间状态(Redis)         │
│   特点:任务隔离,快速读写             │
├─────────────────────────────────────┤
│         长期记忆(向量数据库)         │
│   历史对话摘要、用户偏好、知识库       │
│   容量:近乎无限                      │
│   特点:语义检索,可能有噪声           │
└─────────────────────────────────────┘
class AgentMemory:
    def __init__(self, vector_db, redis_client):
        self.vector_db = vector_db       # ChromaDB / Milvus
        self.redis = redis_client        # 工作记忆
        self.short_term: list[dict] = [] # 短期记忆

    def add_message(self, role: str, content: str):
        self.short_term.append({"role": role, "content": content})
        # 短期记忆超过阈值 → 压缩到长期记忆
        if len(self.short_term) > 40:
            self._compress_to_long_term()

    def _compress_to_long_term(self):
        """将旧消息压缩为摘要,存入长期记忆"""
        old_messages = self.short_term[:25]
        summary = llm_summarize(old_messages)  # LLM 生成摘要
        self.vector_db.insert(
            text=summary,
            metadata={"timestamp": time.time(), "type": "conversation_summary"}
        )
        self.short_term = self.short_term[25:]  # 保留最近的

    def retrieve_context(self, query: str, top_k: int = 5) -> str:
        """从长期记忆中检索与当前查询相关的内容"""
        results = self.vector_db.search(query, top_k=top_k)
        return "\n".join(r["text"] for r in results)

面试话术:“记忆系统的三个关键设计决策:① 压缩策略——何时触发压缩、摘要保留哪些信息;② 检索时机——每次对话前检索还是按需检索;③ 记忆衰减——旧信息权重降低(时间衰减或重要性加权)。”

3.4 Tools(工具)— 设计原则

原则 说明 反面案例
单一职责 一个工具只做一件事 “万能助手” — LLM 不知道该不该用它
幂等性 同参数多次调用结果一致 "发送邮件"调两次发两封
明确 Schema JSON Schema 描述精确到每个字段 参数: data: string — 太模糊
有意义错误 失败时返回 LLM 能理解的信息 Error: 500 vs 搜索API配额耗尽,请改用本地搜索
超时保护 长时间无响应要超时 一直等 → Agent 卡死
人工确认 敏感操作(付费、删除)需确认 直接删库不确认

3.5 Action(行动)— 闭环的最后一公里

Action 模块的职责链:

接收 tool_calls → 解析函数名+参数 → ToolManager.execute()
→ 解析结果(成功/失败/部分成功)
→ 判断循环终止条件:
    ① finish_reason="stop"(LLM 给出最终文本回复)
    ② 达到最大迭代次数(兜底)
    ③ 人工介入终止(Human-in-the-loop)
    ④ 连续 N 次相同 Action+Input(死循环检测)
→ 格式化为 Observation → 喂回 Planning

面试题精选(8 道)

Q1. ReAct 框架的原理?跟纯 CoT 有什么区别?(字节/阿里/腾讯 高频)

标准回答

ReAct = Reasoning + Acting,将推理和行动在同一个循环中交替进行。Thought 分析状况并决定行动,Action 调用工具获取信息,Observation 接收结果进入下一轮思考。

跟 CoT 的核心区别:CoT 只推理不行动,信息来源于模型训练时的知识,无法获取实时信息,容易幻觉。ReAct 通过 Action 与外部交互获取真实信息,Reasoning 让行动有方向(不乱试),Acting 让推理有依据(不乱编)。两者互补产生 1+1>2 的效果。

所有现代 Agent 框架(LangChain AgentExecutor、OpenAI Function Calling、Anthropic Tool Use)本质上都是 ReAct 范式的工程化实现。

Q2. Function Calling 中 LLM 真的调用了函数吗?(字节/腾讯)

标准回答

不。LLM 只负责输出结构化 JSON(选哪个工具、传什么参数),真正的函数执行由开发者的代码完成。流程:开发者把工具定义放在 tools 参数里 → LLM 返回 tool_calls(JSON 格式的函数名+参数) → 代码解析并执行 → 结果以 tool role 消息喂回 LLM → LLM 继续生成。

LLM 在流程中的角色是"路由器 + 参数提取器",不是"执行器"。这是面试中最重要的认知纠正。

Q3. Agent 的四个核心组件?(阿里/百度)

标准回答

Planning(规划):任务分解、反思和自适应调整。没有规划 = 布朗运动。
Memory(记忆):短期(上下文窗口)+ 工作(Redis)+ 长期(向量数据库)三层。没有记忆 = 每次都是"初次见面"。
Tools(工具):扩展 LLM 能力边界。没有工具 = “只会聊天的脑子”。
Action(行动):执行工具、解析结果、判断终止条件。没有 Action = “光想不做”。

四者形成闭环:Planning 决定做什么 → Tools 提供能力 → Action 执行 → Memory 保存结果 → Planning 基于记忆反思调整。

Q4. 如何设计 Agent 的记忆系统?(腾讯/字节)

标准回答

三层架构:① 短期记忆 = 上下文窗口,保留最近 N 轮对话,精确但容量有限(~128K tokens);② 工作记忆 = Redis,存储当前任务中间状态(“已搜索哪些关键词”),任务间隔离;③ 长期记忆 = 向量数据库(ChromaDB/Milvus),存储历史对话摘要和用户偏好,语义检索按需召回。

关键设计决策:压缩策略(窗口达80%触发LLM摘要→存入长期记忆)、检索时机(按需 vs 每轮检索)、记忆衰减(时间衰减或重要性加权,旧信息权重降低)。

Q5. 如何处理 Agent 工具调用的错误?(字节/快手)

标准回答

三层容错:① 参数校验(Pydantic 调用前校验类型和范围,非法参数直接拒绝并返回明确错误信息);② 重试机制(指数退避,最多3次,超时和临时错误可重试,业务错误不重试);③ Fallback 降级(主工具失败 → 备选工具 → 告知 LLM 当前能力受限)。

最关键的原则:错误信息必须对 LLM 友好。返回 “搜索 API 配额耗尽,建议使用本地搜索或告知用户稍后重试” 而不是 “Error: 500”。LLM 看到明确、可操作的错误信息后才能做出合理的下一步决策。

Q6. Plan-and-Execute 和 ReAct 怎么选?(阿里/腾讯)

标准回答

任务明确、环境稳定、步骤间依赖清晰 → Plan-and-Execute(如"写一份市场分析报告",先规划子任务再逐步执行)。环境不确定、需根据中间结果调整策略 → ReAct(如"帮我 Debug 这段代码",调试过程充满意外)。实际产品常用混合方案:高层用 Plan-and-Execute 做任务分解,底层子任务用 ReAct 灵活执行。

Q7. 如何防止 Agent 陷入死循环?(字节)

标准回答

三种检测机制:① 最大迭代次数(简单粗暴,兜底保护);② 重复动作检测(连续3次相同 Action + 相同 Input,说明卡在同一个操作里);③ 无进展检测(连续N步后没有向目标靠近,如搜索仍无果)。

检测到死循环后:终止当前任务 → 告知用户当前进度 → 请求人工介入或重新规划。生产环境通常 max_iterations=15,重复检测阈值=3。

Q8. 提高 Function Calling 准确率的方法?(字节/腾讯)

标准回答

五个方法:① JSON Schema 越精确越好(字段类型、枚举值、取值范围、自然语言描述缺一不可);② 工具 description 写清楚"什么时候用"而非"做什么";③ Few-shot 示例(在 system prompt 中给1-2个正确的工具调用示例);④ 简单工具优于复杂工具(一个工具只做一件事,LLM 更容易选对);⑤ 错误反馈闭环(工具调用失败时让 LLM 看到具体错误信息并尝试修正)。


今日代码实战(3 道)

# ═══════════════════════════════════════════════
# 练习 1:精简版 ReAct Agent(可直接运行理解核心循环)
# ═══════════════════════════════════════════════
import re, json

class SimpleReAct:
    """最精简的 ReAct Agent —— 帮助理解 Thought → Action → Observation 循环"""

    def __init__(self, tools: dict, max_steps: int = 5):
        self.tools = tools          # {"tool_name": handler_function}
        self.max_steps = max_steps
        self.history = []           # 记录完整执行轨迹

    def _build_prompt(self, query: str) -> str:
        tool_desc = "\n".join(
            f"- {name}: {fn.__doc__}" for name, fn in self.tools.items()
        )
        return f"""你是 ReAct Agent。可用工具:
{tool_desc}

回复格式(严格遵守):
Thought: 你的推理
Action: 工具名
Action Input: JSON参数

或者:

Thought: 你的推理
Final Answer: 最终回答

用户问题:{query}"""

    def _parse(self, text: str) -> dict:
        """解析 LLM 的 ReAct 格式输出"""
        result = {"thought": "", "action": "", "input": {}, "is_final": False}

        # 提取 Thought
        tm = re.search(r"Thought:\s*(.+?)(?=\n(?:Action|Final)|$)", text, re.DOTALL)
        if tm:
            result["thought"] = tm.group(1).strip()

        # 检查是否为最终回答
        fm = re.search(r"Final Answer:\s*(.+)", text, re.DOTALL)
        if fm:
            result["is_final"] = True
            result["final_answer"] = fm.group(1).strip()
            return result

        # 提取 Action 和 Action Input
        am = re.search(r"Action:\s*(.+?)$", text, re.MULTILINE)
        im = re.search(r"Action Input:\s*(\{.+?\})", text, re.DOTALL)
        if am:
            result["action"] = am.group(1).strip()
        if im:
            try:
                result["input"] = json.loads(im.group(1))
            except json.JSONDecodeError:
                result["input"] = {}

        return result

    def run(self, query: str, llm) -> str:
        """Agent 主循环 —— llm 参数是调用方传入的 LLM 函数"""
        prompt = self._build_prompt(query)
        msgs = [{"role": "user", "content": prompt}]

        for step in range(self.max_steps):
            response = llm(msgs)  # 调用 LLM(OpenAI API / 本地模型等)
            parsed = self._parse(response)

            if parsed["is_final"]:
                return parsed["final_answer"]

            action = parsed["action"]
            if action not in self.tools:
                msgs.append({"role": "user",
                    "content": f"错误:未知工具 {action},请使用可用工具重试。"})
                continue

            # 执行工具
            try:
                result = self.tools[action](**parsed["input"])
            except Exception as e:
                result = f"工具执行失败: {e}"

            # 记录历史
            self.history.append({
                "step": step + 1, "thought": parsed["thought"],
                "action": action, "input": parsed["input"],
                "result": str(result),
            })

            # 喂回结果
            msgs.append({"role": "user", "content": f"Observation: {result}"})

        return f"超过最大步数 {self.max_steps}"


# ═══════════════════════════════════════════════
# 练习 2:Pydantic 工具注册器 —— Agent 工具管理的标准方案
# ═══════════════════════════════════════════════
from pydantic import BaseModel, Field
from typing import Callable, Any

class FunctionTool:
    """单个工具的完整封装 —— 定义 + 校验 + 执行"""
    def __init__(self, name: str, description: str,
                 params_model: type[BaseModel], handler: Callable):
        self.name = name
        self.handler = handler
        self.params_model = params_model
        # 自动生成 OpenAI Function Calling 格式
        self.openai_spec = {
            "type": "function",
            "function": {
                "name": name,
                "description": description,
                "parameters": params_model.model_json_schema(),
            }
        }

    def execute(self, arguments: dict) -> Any:
        """先 Pydantic 校验参数 → 再调用 handler"""
        params = self.params_model(**arguments)
        return self.handler(**params.model_dump())


# ═══════════════════════════════════════════════
# 练习 3:记忆管理器 —— Agent 不健忘的秘密
# ═══════════════════════════════════════════════
class MemoryManager:
    """Agent 记忆管理器精简版"""
    def __init__(self, max_short_term: int = 40, summary_trigger: int = 30):
        self.short_term: list[dict] = []
        self.long_term: list[str] = []     # 生产环境用向量数据库替代 list
        self.max_short = max_short_term
        self.trigger = summary_trigger

    def add(self, msg: dict):
        """添加消息,超过阈值自动压缩到长期记忆"""
        self.short_term.append(msg)
        if len(self.short_term) >= self.trigger:
            self._compress()

    def _compress(self):
        """压缩旧消息为摘要"""
        old = self.short_term[:20]
        # 生产环境:调用 LLM 生成摘要而非简单拼接
        summary = f"对话摘要(共{len(old)}条): " + \
                  "; ".join([m.get("content", "")[:80] for m in old])
        self.long_term.append(summary)
        self.short_term = self.short_term[20:]

    def get_context(self, query: str) -> list[dict]:
        """组装给 LLM 的完整上下文"""
        # 从长期记忆中取相关摘要(生产环境用向量搜索)
        relevant = [{"role": "system", "content": f"[历史记忆] {m}"}
                    for m in self.long_term[-3:]]
        return relevant + self.short_term

模块四:ReAct 框架完整手写实现(面试高分手写题!)

import json, asyncio, re
from dataclasses import dataclass, field

@dataclass
class AgentStep:
    """单次 ReAct 循环的完整记录"""
    thought: str
    action: str
    action_input: dict
    observation: str

@dataclass
class ReActAgent:
    """ReAct Agent 完整实现 —— 建议手写三遍形成肌肉记忆"""

    llm: callable                       # LLM 调用函数
    tool_manager: 'ToolManager'         # 工具管理器
    system_prompt: str = ""             # 用户自定义系统提示词
    max_iterations: int = 10            # 最大迭代次数
    verbose: bool = True                # 是否打印详细日志

    # 运行时状态
    messages: list[dict] = field(default_factory=list)
    steps: list[AgentStep] = field(default_factory=list)

    def _build_react_prompt(self) -> str:
        """构建 ReAct 格式的系统提示词 —— 这是整个 Agent 的灵魂"""
        return f"""你是一个 ReAct Agent,通过"思考-行动-观察"的循环来解决问题。

## 回复格式(严格遵守!)

需要调用工具时:
Thought: <你的推理过程——分析当前状况,决定下一步做什么>
Action: <工具名称>
Action Input: <JSON 格式的工具参数>

已经可以回答用户时:
Thought: 我现在已经掌握了所有必要信息
Final Answer: <你的最终回答>

## 重要规则
1. 每次只能调用一个工具
2. 工具参数必须是合法的 JSON
3. 如果工具返回错误,分析错误原因并尝试修正参数后重试
4. 不要编造信息——所有事实信息必须来自工具调用结果
5. 不确定用哪个工具时,在 Thought 中分析后再做决定

{self.system_prompt}"""

    async def run(self, user_query: str) -> str:
        """Agent 主循环 —— ReAct 的完整实现"""
        self.messages = [
            {"role": "system", "content": self._build_react_prompt()},
            {"role": "user", "content": user_query},
        ]

        for i in range(self.max_iterations):
            if self.verbose:
                print(f"\n{'='*50}")
                print(f"第 {i+1}/{self.max_iterations} 轮")
                print(f"{'='*50}")

            # ① 调用 LLM
            response = await self.llm(self.messages)

            if self.verbose:
                print(f"\nLLM 回复:\n{response[:300]}...")

            # ② 解析 LLM 输出
            thought, action, action_input = self._parse_react_output(response)

            if self.verbose:
                print(f"\nThought: {thought}")

            # ③ 检查是否是最终回答
            if action == "FINAL_ANSWER":
                if self.verbose:
                    print(f"Final Answer: {action_input}")
                return action_input

            # ④ 执行工具
            if self.verbose:
                print(f"Action: {action}({action_input})")

            result = await self.tool_manager.execute(action, action_input)
            observation = result.data if result.success else f"错误: {result.error}"

            if self.verbose:
                print(f"Observation: {str(observation)[:200]}")

            # ⑤ 记录步骤
            step = AgentStep(
                thought=thought, action=action,
                action_input=action_input, observation=observation,
            )
            self.steps.append(step)

            # ⑥ 将 Assistant 回复和工具结果都加入对话历史
            self.messages.append({"role": "assistant", "content": response})
            self.messages.append({"role": "user", "content": f"Observation: {observation}"})

            # ⑦ 死循环检测
            if self._detect_loop():
                return "检测到死循环,Agent 停止。请检查工具定义和提示词。"

        return (f"Agent 在 {self.max_iterations} 轮迭代后仍未完成任务。"
                f"已执行步骤:{len(self.steps)} 步")

    def _parse_react_output(self, text: str) -> tuple:
        """解析 LLM 的 ReAct 格式输出 —— 核心解析器"""
        # 提取 Thought
        thought_m = re.search(r"Thought:\s*(.+?)(?=\n(?:Action|Final)|$)", text, re.DOTALL)
        thought = thought_m.group(1).strip() if thought_m else ""

        # 检查是否为最终回答
        final_m = re.search(r"Final Answer:\s*(.+)", text, re.DOTALL)
        if final_m:
            return thought, "FINAL_ANSWER", final_m.group(1).strip()

        # 提取 Action
        action_m = re.search(r"Action:\s*(.+?)(?=\n|$)", text)
        action = action_m.group(1).strip() if action_m else ""

        # 提取 Action Input(JSON)
        input_m = re.search(r"Action Input:\s*(\{.+?\})", text, re.DOTALL)
        action_input = {}
        if input_m:
            try:
                action_input = json.loads(input_m.group(1))
            except json.JSONDecodeError:
                action_input = {"raw": input_m.group(1)}

        return thought, action, action_input

    def _detect_loop(self) -> bool:
        """检测死循环:连续 3 次相同 Action + 相同 Input"""
        if len(self.steps) < 3:
            return False
        last_three = self.steps[-3:]
        return all(
            s.action == last_three[0].action and
            s.action_input == last_three[0].action_input
            for s in last_three
        )

模块五:Agent 决策策略对比

策略 流程 优点 缺点 适用场景
ReAct Thought→Action→Observation 串行循环 简单可靠,社区最成熟 串行效率低(一次一个工具) 大多数通用 Agent
Plan-and-Execute 先完整规划→再逐步执行 全局最优,可并行无依赖步骤 计划跟不上变化 任务明确、环境稳定
Reflexion ReAct + 长期失败记忆 能从错误中学习、持续改进 需额外存储和检索系统 需要持续优化的 Agent
LATS ToT + ReAct + MCTS 理论上最优(搜索+推理+行动) 极其昂贵(每次多条路径) 需要极高可靠性的关键任务
LLMCompiler 分析依赖→并行执行无依赖工具 速度极快 依赖分析可能出错 多工具并行查询场景

面试话术:“实际项目中我倾向于混合策略:高层用 Plan-and-Execute 做任务分解和依赖分析,底层每个子任务用 ReAct 灵活执行。这样既有全局规划的效率,又有单步执行的灵活性。LangGraph 的 StateGraph + 条件边就是这种混合策略的最佳实现。”


📊 今日知识图谱

Agent 核心速通 DAY 4
│
├── Prompt Engineering
│   ├── Zero-shot → Few-shot(ICL) → CoT(出声思考)
│   ├── Self-Consistency(多路径投票) → ToT(树搜索)
│   └── ReAct = Reasoning + Acting(所有 Agent 的根基)
│
├── Function Calling
│   ├── 原理:LLM输出JSON → 代码执行 → 结果喂回(LLM 不是执行器!)
│   ├── Pydantic → JSON Schema(一行生成工具定义)
│   ├── tool_choice 四种策略(auto/none/required/指定)
│   └── 并行调用 + 错误处理 + 重试机制
│
├── Agent 四要素
│   ├── Planning:分解 → 反思 → 自适应
│   ├── Memory:短期(上下文) + 工作(Redis) + 长期(向量库)
│   ├── Tools:单一职责/明确Schema/有意义错误/超时/人工确认
│   └── Action:执行/解析/终止条件/死循环检测
│
├── ReAct 手写实现(重点!)
│   ├── Thought → Action → Observation 完整循环
│   ├── _parse_react_output 正则解析器
│   ├── _detect_loop 死循环检测
│   └── AgentStep 完整轨迹记录
│
└── 决策策略
    ├── ReAct: 灵活但串行(通用首选)
    ├── Plan-and-Execute: 高效但僵硬(明确任务用)
    ├── Reflexion: 从错误中学习(持续优化用)
    └── LATS: 最优但昂贵(关键任务用)

🔜 明日预告

Day 5 — RAG 检索增强生成全体系

  • RAG 原理 & vs 微调/Long Context 全面对比
  • Chunking 五策略:固定/递归/语义/父子文档/Small-to-Big
  • Embedding 模型选型(BGE/GTE/Jina/OpenAI)& 向量数据库对比
  • 混合检索(Dense + Sparse + BM25)+ Rerank + HyDE
  • Lost in the Middle & 上下文压缩(LongLLMLingua)
  • Agentic RAG:Self-RAG / Corrective RAG / 多跳检索
  • 从零搭建完整 RAG 系统的完整代码
  • 8 道 RAG 面试真题(含公司标签)

💡 速通心法:Day 4 的核心是把"Agent 到底怎么工作"刻进肌肉记忆。面试官问"你用过 Agent 吗",不是在问你有没有调过 LangChain 的 agent.run(),而是在问你能不能讲清楚:LLM 怎么选工具?参数怎么校验?失败怎么兜底?记忆怎么跨对话保留?死循环怎么检测?把今天 ReAct 手写代码敲一遍,对着代码把 Thought→Action→Observation 循环讲三遍,这五个问题你就能一边画架构图一边回答了。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐