Agent 不只是套个循环,而是模型、工具与环境之间的动态博弈
项目目标
在完成模型训练、推理优化和 RAG 之后,LLM 仍然是一个相对孤立的系统。它具备强大的文本处理能力,但无法直接与外部世界交互。项目 30 的目标是打破这个边界。
我们将构建一个 Agent 循环(Agent Loop),使模型能够根据用户的需求,自主决定调用哪些工具,观察执行结果,并在失败时尝试修正。这个过程将涵盖从工具定义、Prompt 工程到状态管理的全栈细节。
任务 1:工具定义与 Schema 抽象
模型无法直接运行 Python 函数或访问 API,它只能生成表示调用意图的文本。因此,Agent 的第一步是把本地能力翻译成模型能理解的结构化描述(Schema)。
一个健壮的工具定义应包含:
- 唯一标识符(Name):简洁且具描述性。
- 详尽的功能描述(Description):这是模型判断“何时调用”的关键,类似于函数的文档字符串。
- 参数定义(Parameters):通常采用 JSON Schema 格式,明确类型、默认值和约束条件。
def calculate_real_estate_tax(price: float, city: str):
"""计算特定城市的房产税。"""
# 模拟业务逻辑
rates = {"shanghai": 0.01, "beijing": 0.012}
rate = rates.get(city.lower(), 0.008)
return price * rate
# 转换为模型可消费的格式
tools_schema = [
{
"type": "function",
"function": {
"name": "calculate_real_estate_tax",
"description": "根据房价和城市名计算房产税额度",
"parameters": {
"type": "object",
"properties": {
"price": {"type": "number", "description": "房屋总价"},
"city": {"type": "string", "description": "城市名称,如上海、北京"}
},
"required": ["price", "city"]
}
}
}
]
任务 2:实现 ReAct 模式(Thought-Action-Observation)
目前主流的 Agent 模式是 ReAct(Reasoning and Acting)。它要求模型在采取行动前先进行“思考”(Thought),然后生成“行动”(Action),执行后获取“观察”(Observation)。
我们需要设计一个 System Prompt 来约束模型输出特定的格式。这种格式必须足够稳健,便于正则表达式或 JSON 解析器提取。
System Prompt 示例:
你是一个具备工具调用能力的助手。
对于每个用户请求,你必须遵循以下严格的循环格式:
Thought: 思考当前需要做什么,分析还缺少哪些信息。
Action: 调用工具,格式必须为 {"name": "tool_name", "args": {"key": "value"}}。
Observation: 这里会填入系统反馈的工具执行结果。
... (重复上述过程,直到获得最终答案)
Final Answer: 输出给用户的最终结论。
禁止在 Final Answer 之前直接给出结论。
任务 3:构建 Agent 状态机循环
这是项目的核心代码实现。一个完整的 Agent 循环需要处理三个核心逻辑:上下文拼接、异常反馈与终止条件。
class MinimalAgent:
def __init__(self, llm_client, tools_map, max_steps=5):
self.llm = llm_client
self.tools = tools_map # dict: {name: func}
self.max_steps = max_steps
def run(self, query):
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.append({"role": "user", "content": query})
for step in range(self.max_steps):
# 1. 产生 Thought + Action
response = self.llm.generate(messages)
messages.append({"role": "assistant", "content": response})
# 2. 解析并执行 Action
action = self.extract_action(response)
if not action:
# 检查是否包含 Final Answer
if "Final Answer:" in response:
return response.split("Final Answer:")[-1]
continue
# 3. 执行本地工具并捕获异常
try:
result = self.tools[action['name']](**action['args'])
obs_content = f"Observation: {result}"
except Exception as e:
# 将错误反馈给模型,让其修正参数
obs_content = f"Observation: Error executing tool: {str(e)}"
messages.append({"role": "user", "content": obs_content})
return "Failed to complete task within max steps."
任务 4:观测与分析(Plotting)
Agent 系统具有高度的不确定性,因此必须进行定量观测。我们需要记录以下指标并绘制分析图表:
- 任务成功率与步数分布:记录不同复杂度的任务平均需要多少步完成。如果大部分任务都在第 5 步强制终止,说明模型陷入了死循环。
- 工具调用准确率:统计模型生成的 JSON 格式错误率。
- 推理开销分析:统计每一轮循环中 Thought 占用的 token 比例。过长的思考可能意味着模型在徘徊。
- Token 消耗增长曲线:由于每一步都要携带历史上下文,Agent 的成本是随步数平方级(或至少是累进地)增长的。
故意破坏实验
实验是理解 Agent 脆弱性的最好方式:
- 实验 A:描述冲突。定义两个工具,一个是
get_weather,另一个是get_forecast,且描述非常接近。观察模型是否会在选择工具时产生震荡。 - 实验 B:强制错误反馈。在 Observation 中手动输入
Permission Denied或Invalid Argument。观察模型是会选择放弃,还是尝试变换参数重新调用,或是直接向用户撒谎说“执行成功”。 - 实验 C:长序列压力测试。设计一个需要调用 10 次工具的任务。观察模型是否在第 7 或第 8 步开始遗忘最初的 User Prompt 目标。
为什么 Agent 会失败(Explain)
通过实验,我们需要总结出 Agent 失败的三大核心模式:
- 推理环路(Reasoning Loop):模型认为需要调用 A 才能解决问题,但 A 返回的结果不符合预期,模型在下一轮 Thought 中仍然认为需要调用 A,从而形成死循环。
- 幻觉调用(Hallucinated Tools):模型尝试调用一个在 Schema 中不存在,但其预训练知识中存在的函数(如猜测系统中应该有
search_google)。 - 格式坍塌(Format Collapse):在长对话中,由于 Observation 的干扰,助手角色开始模仿 User 的语气,或者不再输出
Thought:前缀,导致解析器失效。
本项目交付物
- 一个支持多步 ReAct 循环、具备基本错误恢复能力的 Python Agent 脚本。
- 一组针对特定任务(如:查询三座城市的税率并计算总额)的完整 Trace 日志。
- 针对不同底座模型(如 Qwen2.5-7B 与 Llama-3.1-8B)在工具调用准确率上的对比图表。
- 一份关于“如何通过 Prompt 抑制 Agent 幻觉”的实验报告。
延伸阅读
- ReAct: Synergizing Reasoning and Acting in Language Models
- MRKL Systems: A modular, neuro-symbolic architecture
- OpenAI Function Calling Official Documentation
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:项目 30:构建 Tool Use 与 Agent Loop
本文链接:https://www.sshipanoo.com/blog/ai/llm-roadmap/lab-30-agent-loop/
本文最后一次更新为 天前,文章中的某些内容可能已过时!