欢迎来到本系列的第一章。很多人认为“Agent”不过是在LLM外面套一层循环调用工具。但真正理解Agent的本质——如何感知环境、规划行动、执行反馈并循环迭代——才是从“调用API”走向“构建自主系统”的关键。本章将从零开始,定义一个可运行的ReAct Agent,并引出后续所有章节将要解决的痛点。
1.1 为什么需要Agent?从单一问答到任务闭环
传统的大模型调用是“无状态、单回合”的:用户输入 → LLM → 输出。这种模式适用于翻译、摘要、简单的知识问答。但当你需要模型执行一个多步骤的任务,比如“帮我查一下北京未来三天的天气,如果下雨就提醒我带伞,否则推荐一个公园”,模型就必须:
调用天气API获取数据
根据结果做出判断
再次生成最终回答
这就是Agent的核心价值:让LLM成为“大脑”,能够自主调用外部工具、规划步骤、处理异常,最终完成一个闭环任务。
1.1.1 Agent 的“感知-规划-行动”循环
一个典型的Agent循环包含三个步骤:
while not done:
1. 感知(Perceive):将当前环境状态(用户输入、历史消息、工具返回结果)组装成上下文
2. 规划(Plan):LLM决定下一步要做什么(是调用某个工具,还是直接输出最终答案)
3. 行动(Act):执行LLM选择的行为(调用工具函数,或者返回结果给用户)
下面这张图展示了最简单的ReAct Agent循环流程(使用Mermaid语法):

1.2 经典Agent架构:ReAct、Plan-and-Execute、Reflexion
1.2.1 ReAct(Reason + Act)
ReAct是最经典的范式:LLM在每一步交替输出“思考”(Reason)和“行动”(Act)。例如:
Thought: 我需要知道北京今天的天气才能回答用户。
Action: get_weather(city="北京")
Observation: 晴天,25°C
Thought: 天气晴朗,不需要提醒带伞,可以推荐公园。
Action: recommend_park(city="北京")
Observation: 朝阳公园、奥林匹克森林公园
Thought: 我现在可以给出最终答案了。
Answer: 北京今天晴天,25°C,推荐去朝阳公园或奥林匹克森林公园。
优点:简单直观,易于调试。
缺点:对于复杂任务,思维链可能过长,容易陷入循环。
1.2.2 Plan-and-Execute(先规划后执行)
该模式先让LLM生成一个完整计划(步骤列表),然后逐步执行每个步骤,必要时动态调整。
Plan:
1. 调用天气API获取北京未来三天天气
2. 如果包含雨天,则生成提醒
3. 搜索北京公园推荐
4. 合并结果输出
优点:对长时间任务更稳定,减少重复推理。
缺点:计划可能与实际结果不符,需要纠错机制。
1.2.3 Reflexion(反思)
Reflexion在Agent执行任务后,增加一个“反思”步骤:评估结果是否满意,如果不满意则生成改进建议,并重试。常用于编程、数学等有明确反馈的任务。
本系列将主要基于ReAct展开,因为它是理解Agent最小闭环的最佳起点,后续章节会融入Plan-and-Execute和Reflexion的元素。
1.3 工具(Tools)的概念与设计
Agent的能力取决于它能使用的工具。工具可以是任何函数:查询数据库、调用API、运行代码、发送邮件等。关键在于如何让LLM理解工具的存在,并选择正确的工具。
1.3.1 Function Calling 接口
OpenAI、Anthropic、Google等主流模型都支持Function Calling(或Tool Use)。你向模型提供一组工具的描述(名称、参数Schema),模型返回一个结构化的tool_calls对象,而不是纯文本。
一个工具描述的例子(OpenAI格式):
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名,例如 '北京'"
}
},
"required": ["city"]
}
}
}
模型调用时返回:
{
"tool_calls": [{
"id": "call_abc123",
"function": {
"name": "get_weather",
"arguments": "{\\"city\\":\\"北京\\"}"
}
}]
}
1.3.2 本地模型与ReAct提示词
如果你使用本地模型(如Llama 3、Qwen),它们不一定支持Function Calling。此时可以退化为提示词引导,例如:
你是一个助手,可以使用以下工具:
– get_weather(city): 获取城市天气,返回字符串。
– calculate(expression): 计算数学表达式。
当你需要使用工具时,请输出以下格式:
Action: 工具名称
Action Input: 参数JSON
当你已经获得足够信息,输出最终答案:
Final Answer: …
我们在本章的代码实例中将同时演示OpenAI官方Function Calling和本地模型提示词两种方式。
1.4 实战:搭建一个最小ReAct Agent
我们将构建一个支持天气查询和计算器的Agent。用户输入混合任务,Agent自主决定调用哪个工具,并最终给出答案。
1.4.1 环境准备
安装依赖:
pip install openai python-dotenv httpx
# 如果使用本地模型,需要安装 ollama 或 llama.cpp,此处以OpenAI为例
创建.env文件:
OPENAI_API_KEY=sk-…
1.4.2 定义工具函数
# tools.py
import httpx
import json
def get_weather(city: str) -> str:
"""模拟天气API,实际可换成真实API"""
# 这里使用 wttr.in 的简单接口
try:
response = httpx.get(f"https://wttr.in/{city}?format=%C+%t", timeout=5)
if response.status_code == 200:
return response.text.strip()
else:
return f"无法获取{city}天气"
except Exception as e:
return f"天气服务异常: {str(e)}"
def calculate(expression: str) -> str:
"""安全地计算数学表达式"""
# 注意:eval有安全风险,生产环境应使用受限的解析器如numexpr或ast.literal_eval
# 本演示仅用于教学,只允许简单四则运算
allowed_chars = set("0123456789+-*/(). ")
if not all(c in allowed_chars for c in expression):
return "表达式包含非法字符"
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"计算错误: {str(e)}"
# 工具注册表
TOOLS = {
"get_weather": get_weather,
"calculate": calculate,
}
# 提供给OpenAI Function Calling 的工具描述
TOOLS_DEFINITION = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气情况",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如'北京'"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算,支持加减乘除和括号",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式,如'3*(4+5)'"}
},
"required": ["expression"]
}
}
}
]
1.4.3 Agent 核心循环(OpenAI Function Calling版)
# agent_openai.py
import os
from dotenv import load_dotenv
from openai import OpenAI
import json
from tools import TOOLS, TOOLS_DEFINITION
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
class SimpleAgent:
def __init__(self, model="gpt-4o-mini", max_iterations=5):
self.model = model
self.max_iterations = max_iterations
self.tools = TOOLS
self.tools_def = TOOLS_DEFINITION
def run(self, user_input: str) -> str:
messages = [{"role": "user", "content": user_input}]
for step in range(self.max_iterations):
# 调用LLM,附带工具定义
response = client.chat.completions.create(
model=self.model,
messages=messages,
tools=self.tools_def,
tool_choice="auto", # 让模型自己决定是否调用工具
)
msg = response.choices[0].message
messages.append(msg)
# 检查是否有工具调用
if msg.tool_calls:
# 处理所有工具调用(可并行,这里串行演示)
for tool_call in msg.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
if tool_name in self.tools:
tool_result = self.tools[tool_name](**tool_args)
# 将工具结果追加到对话中
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
})
else:
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": f"未知工具: {tool_name}"
})
else:
# 没有工具调用,说明Agent已准备好最终答案
return msg.content
return "达到最大迭代次数,无法完成请求"
if __name__ == "__main__":
agent = SimpleAgent()
queries = [
"北京今天天气怎么样?",
"计算 (25 + 35) * 2 等于多少?",
"上海现在天气如何?如果温度低于10度,告诉我多穿衣服,否则告诉我适合户外运动。"
]
for q in queries:
print(f"\\n用户: {q}")
answer = agent.run(q)
print(f"Agent: {answer}")
运行结果示例:
用户: 北京今天天气怎么样?
Agent: 北京今天的天气是晴天,温度约为25°C。
用户: 计算 (25 + 35) * 2 等于多少?
Agent: (25 + 35) * 2 = 120。
用户: 上海现在天气如何?如果温度低于10度,告诉我多穿衣服,否则告诉我适合户外运动。
Agent: 上海当前天气为多云,温度15°C,适合户外运动。
1.4.4 本地模型版本(使用Ollama + 提示词驱动)
如果你没有OpenAI API,可以使用本地模型(如Llama 3.1 8B)通过Ollama运行。安装Ollama后,拉取模型:
ollama pull llama3.1
然后实现一个基于提示词的Agent(不依赖Function Calling):
# agent_local.py
import json
import ollama
from tools import TOOLS
class LocalAgent:
def __init__(self, model="llama3.1", max_iterations=5):
self.model = model
self.max_iterations = max_iterations
self.tools = TOOLS
def _build_prompt(self, messages):
"""将消息列表转换为提示词格式"""
prompt = ""
for m in messages:
role = m["role"]
content = m["content"]
if role == "system":
prompt += f"System: {content}\\n"
elif role == "user":
prompt += f"User: {content}\\n"
elif role == "assistant":
prompt += f"Assistant: {content}\\n"
elif role == "tool":
prompt += f"Observation: {content}\\n"
prompt += "Assistant: "
return prompt
def run(self, user_input: str) -> str:
system_prompt = f"""你是一个能使用工具的助手。你可以调用以下函数:
{', '.join(self.tools.keys())}。
当需要调用工具时,严格按以下格式输出:
Action: 函数名
Action Input: JSON字符串,例如 {{"city": "北京"}}
当你准备好最终答案时,输出:
Final Answer: 你的回答
请一步一步思考。"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input}
]
for step in range(self.max_iterations):
prompt = self._build_prompt(messages)
response = ollama.generate(model=self.model, prompt=prompt, options={"temperature": 0})
output = response["response"].strip()
messages.append({"role": "assistant", "content": output})
if output.startswith("Final Answer:"):
return output[len("Final Answer:"):].strip()
elif output.startswith("Action:"):
# 解析工具名和参数
lines = output.split("\\n")
action = None
action_input = None
for line in lines:
if line.startswith("Action:"):
action = line.split(":", 1)[1].strip()
elif line.startswith("Action Input:"):
raw = line.split(":", 1)[1].strip()
try:
action_input = json.loads(raw)
except:
action_input = {}
if action and action in self.tools:
result = self.tools[action](**action_input)
messages.append({"role": "tool", "content": str(result)})
else:
messages.append({"role": "tool", "content": f"Error: 未知工具 {action}"})
else:
# 模型未按格式输出,尝试强制要求
messages.append({"role": "user", "content": "请按格式输出 Action 或 Final Answer"})
return "达到最大迭代次数"
if __name__ == "__main__":
agent = LocalAgent()
print(agent.run("北京天气如何?"))
这个版本虽然不如Function Calling稳定,但展示了ReAct提示词的本质,适合理解原理。
1.5 当前Agent的局限与本章总结
通过上述代码,你已经拥有一个能自动调用工具的多步Agent。但是,它存在诸多问题,正是后续章节要解决的:
| 输出格式不稳定 | 本地模型可能不按JSON输出,导致解析失败 | 第2章(Spec驱动开发) |
| 无限循环 | Agent可能重复调用同一个工具 | 第3章(状态机与终止检测) |
| 上下文爆炸 | 多轮对话后token超限 | 第4章(上下文窗口管理) |
| 知识不足 | 无法回答私有领域问题 | 第5章(RAG) |
| 工具安全风险 | eval执行任意代码 | 第6章(沙箱执行) |
| 单点故障 | 无监督、无降级 | 第9章(可观测性与降级) |
| 难以部署 | 同步阻塞,不满足高并发 | 第10章(生产部署) |
本章我们奠定了Agent的基本概念和最小实现。在下一章,我们将引入Spec驱动开发——用结构化约束(JSON Schema、Pydantic)彻底解决输出格式不稳定的问题,让Agent的行为可预测、可测试。
1.6 思考与练习
尝试修改SimpleAgent,支持并行调用多个工具(提示:OpenAI支持一次返回多个tool_calls)。
如果某个工具调用失败(例如天气API超时),如何让Agent自动重试或改用其他策略?
分析LocalAgent中,为什么temperature=0很重要?
(挑战)实现一个最简单的缓存:如果用户两次问同一个城市的天气,直接返回上次结果而不调用API。
答案提示:下一章将提供部分参考实现。
下章预告:第2章「Spec驱动开发(上):用结构化约束驯服LLM」——我们将学习如何确保LLM的输出百分之百符合预期Schema,并引入instructor库自动重试与校验。


