AI Agent 架构设计:从单次推理到多轮规划的工程演进
AI Agent 架构设计:从单次推理到多轮规划的工程演进

一、当 LLM 不再"一问一答":Agent 的工程挑战
一个客服系统,用户问"帮我退掉昨天的订单",LLM 直接生成了退款操作。但用户昨天有 3 笔订单,退哪一笔?LLM 不知道,因为它没有"先查询再确认"的规划能力。这不是模型能力不足,而是架构设计缺失——缺少让模型"思考后再行动"的框架。
AI Agent 的核心,是让 LLM 从"单次推理器"进化为"多轮规划执行器"。单次推理是"问什么答什么",Agent 是"理解意图、拆解任务、调用工具、观察结果、调整策略"。这五步循环,就是 Agent 的基本骨架。
Agent 的设计,像极了易经中的"变"——每一次工具调用都是一次"变",观察结果是"变"的反馈,调整策略是"应变"。不变的,是目标导向的规划逻辑。本文将从架构层面拆解 Agent 的设计模式,给出生产级的实现方案。
二、从 ReAct 到 Plan-and-Execute:Agent 架构的演进脉络
Agent 架构的核心问题:如何平衡"思考深度"与"执行效率"。
graph TB
subgraph ReAct 循环
R1[观察 Observation] --> R2[思考 Thought]
R2 --> R3[行动 Action]
R3 --> R4[观察结果]
R4 --> R2
end
subgraph Plan-and-Execute
P1[用户目标] --> P2[规划器: 生成任务列表]
P2 --> P3[执行器: 逐步执行]
P3 --> P4{任务完成?}
P4 -->|否| P5[重新规划]
P5 --> P3
P4 -->|是| P6[输出结果]
end
subgraph 多 Agent 协作
M1[编排 Agent] --> M2[搜索 Agent]
M1 --> M3[代码 Agent]
M1 --> M4[分析 Agent]
M2 --> M5[结果汇总]
M3 --> M5
M4 --> M5
end
style R2 fill:#fff9c4
style P2 fill:#fff9c4
style M1 fill:#fff9c4
1. ReAct:思考-行动-观察的循环
ReAct(Reasoning + Acting)是最经典的 Agent 模式。每一步,LLM 先"思考"当前状态和下一步策略,再选择一个"行动"(工具调用),然后"观察"行动结果,进入下一轮循环。优点是灵活,每一步都可以根据观察调整策略。缺点是效率低——简单任务也需要多轮 LLM 调用。
2. Plan-and-Execute:先规划后执行
Plan-and-Execute 将规划与执行分离。规划器(通常是更强的 LLM)一次性生成任务列表,执行器逐个执行。如果执行过程中发现计划不合理,触发重新规划。优点是减少了 LLM 调用次数,缺点是规划质量依赖 LLM 的推理能力,复杂任务容易规划失败。
3. 多 Agent 协作:分而治之
将不同能力分配给专门的 Agent:搜索 Agent 负责信息检索,代码 Agent 负责编程,分析 Agent 负责数据处理。编排 Agent 负责任务分配和结果汇总。优点是每个 Agent 的 system prompt 更聚焦,缺点是 Agent 间通信成本高,编排逻辑复杂。
三、生产级 Agent 框架:模块化设计与工具管理
import json
import re
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Optional
from enum import Enum
logger = logging.getLogger(__name__)
class ToolCallStatus(Enum):
"""工具调用状态"""
SUCCESS = "success"
ERROR = "error"
TIMEOUT = "timeout"
@dataclass
class ToolCallResult:
"""工具调用结果"""
status: ToolCallStatus
output: Any
error: Optional[str] = None
execution_time_ms: float = 0.0
class BaseTool(ABC):
"""工具基类:所有 Agent 工具必须实现此接口"""
@property
@abstractmethod
def name(self) -> str:
"""工具名称,用于 LLM 调用"""
pass
@property
@abstractmethod
def description(self) -> str:
"""工具描述,LLM 据此判断何时调用"""
pass
@abstractmethod
def parameters_schema(self) -> dict:
"""工具参数的 JSON Schema"""
pass
@abstractmethod
def execute(self, **kwargs) -> ToolCallResult:
"""执行工具逻辑"""
pass
def to_openai_format(self) -> dict:
"""转换为 OpenAI function calling 格式"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters_schema(),
},
}
class DatabaseQueryTool(BaseTool):
"""数据库查询工具示例"""
@property
def name(self) -> str:
return "query_orders"
@property
def description(self) -> str:
return (
"查询用户订单信息。当用户提到订单、退款、物流等需要查数据库时使用。"
"返回订单列表,包含订单号、金额、状态、创建时间。"
)
def parameters_schema(self) -> dict:
return {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "用户ID",
},
"date_range": {
"type": "string",
"description": "日期范围,格式: YYYY-MM-DD~YYYY-MM-DD",
},
"status": {
"type": "string",
"enum": ["all", "pending", "completed", "refunded"],
"description": "订单状态筛选,默认 all",
},
},
"required": ["user_id"],
}
def execute(self, **kwargs) -> ToolCallResult:
"""执行数据库查询"""
try:
user_id = kwargs.get("user_id")
date_range = kwargs.get("date_range", "")
status = kwargs.get("status", "all")
# 生产环境中这里连接真实数据库
# 此处模拟查询结果
orders = [
{"order_id": "ORD-001", "amount": 299.0, "status": "completed"},
{"order_id": "ORD-002", "amount": 158.0, "status": "pending"},
{"order_id": "ORD-003", "amount": 89.0, "status": "completed"},
]
if status != "all":
orders = [o for o in orders if o["status"] == status]
return ToolCallResult(
status=ToolCallStatus.SUCCESS,
output=orders,
)
except Exception as e:
logger.error(f"数据库查询失败: {e}")
return ToolCallResult(
status=ToolCallStatus.ERROR,
output=None,
error=str(e),
)
@dataclass
class AgentState:
"""Agent 状态:记录对话历史和执行上下文"""
messages: list = field(default_factory=list)
tool_results: list = field(default_factory=list)
iteration: int = 0
max_iterations: int = 10
task_complete: bool = False
def add_observation(self, content: str):
"""添加观察结果"""
self.messages.append({"role": "user", "content": content})
def add_thought(self, content: str):
"""添加思考过程"""
self.messages.append({"role": "assistant", "content": f"[思考] {content}"})
def check_iteration_limit(self) -> bool:
"""检查是否超过最大迭代次数"""
self.iteration += 1
if self.iteration >= self.max_iterations:
logger.warning(
f"Agent 已达最大迭代次数 {self.max_iterations},强制终止"
)
self.task_complete = True
return True
return False
class ReActAgent:
"""
ReAct 模式 Agent:思考-行动-观察循环
核心流程:
1. LLM 根据当前状态生成思考 + 工具调用
2. 执行工具调用,获取结果
3. 将结果作为观察反馈给 LLM
4. 重复直到任务完成或达到迭代上限
"""
def __init__(
self,
llm_client, # OpenAI 兼容的 LLM 客户端
tools: list[BaseTool],
system_prompt: str = "",
max_iterations: int = 10,
):
self.llm = llm_client
self.tools = {tool.name: tool for tool in tools}
self.system_prompt = system_prompt or self._default_system_prompt()
self.max_iterations = max_iterations
def _default_system_prompt(self) -> str:
return (
"你是一个智能助手,能够通过调用工具完成用户任务。\n"
"请按以下格式回复:\n"
"1. 先思考当前情况和下一步策略\n"
"2. 如果需要调用工具,使用 function calling\n"
"3. 如果已有足够信息,直接给出最终答案\n"
"注意:每次只调用一个工具,观察结果后再决定下一步。"
)
def run(self, user_input: str) -> str:
"""执行 Agent 循环"""
state = AgentState(max_iterations=self.max_iterations)
state.messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_input},
]
while not state.task_complete:
if state.check_iteration_limit():
break
# 调用 LLM
try:
response = self.llm.chat.completions.create(
model="gpt-4",
messages=state.messages,
tools=[t.to_openai_format() for t in self.tools.values()],
tool_choice="auto",
)
except Exception as e:
logger.error(f"LLM 调用失败: {e}")
return f"抱歉,服务暂时不可用: {str(e)}"
message = response.choices[0].message
# 检查是否有工具调用
if message.tool_calls:
for tool_call in message.tool_calls:
result = self._execute_tool(tool_call)
# 将工具结果加入对话历史
state.messages.append(message)
state.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
result.output if result.status == ToolCallStatus.SUCCESS
else {"error": result.error},
ensure_ascii=False,
),
})
else:
# 没有工具调用,说明 LLM 认为任务完成
state.task_complete = True
return message.content or ""
return "任务执行超时,请稍后重试。"
def _execute_tool(self, tool_call) -> ToolCallResult:
"""执行单个工具调用"""
tool_name = tool_call.function.name
tool = self.tools.get(tool_name)
if tool is None:
logger.error(f"未知工具: {tool_name}")
return ToolCallResult(
status=ToolCallStatus.ERROR,
output=None,
error=f"未知工具: {tool_name}",
)
try:
arguments = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
logger.error(f"工具参数解析失败: {e}")
return ToolCallResult(
status=ToolCallStatus.ERROR,
output=None,
error=f"参数格式错误: {str(e)}",
)
logger.info(f"调用工具: {tool_name}, 参数: {arguments}")
return tool.execute(**arguments)
四、Agent 架构的权衡:灵活性、成本与可靠性的三角博弈
1. LLM 调用成本与延迟
ReAct 模式每一步都需要 LLM 推理,一个复杂任务可能需要 5-10 轮调用。以 GPT-4 的价格计算,单次 Agent 执行成本可能在 $0.5-2 之间。Plan-and-Execute 减少了调用次数,但规划失败时需要重新规划,成本不可预测。生产环境中,必须设置调用次数上限和单次成本上限。
2. 工具调用的可靠性问题
LLM 生成的工具参数可能不符合 schema,比如日期格式错误、枚举值越界。解决方案:在工具执行前做参数校验,校验失败时将错误信息反馈给 LLM 重试。但这增加了调用轮次。更根本的方案是优化工具的 description 和 schema,让 LLM 更容易生成正确参数。
3. 多 Agent 协作的通信开销
Agent 间通过自然语言通信,信息压缩损失大。搜索 Agent 返回的 10 条结果,传递给分析 Agent 时可能只保留摘要。解决方案:定义结构化的 Agent 间通信协议,用 JSON 而非自然语言传递中间结果。
4. 安全边界:Agent 的权限控制
Agent 可以调用工具,就意味着可以执行操作。退款、发邮件、删除数据——这些操作一旦被 LLM 错误触发,后果严重。生产环境中,所有写操作必须经过人工确认,或设置操作白名单和频率限制。
五、总结
AI Agent 的架构设计,核心是在灵活性、成本和可靠性之间找平衡。ReAct 适合需要动态决策的复杂任务,Plan-and-Execute 适合步骤明确的流程性任务,多 Agent 协作适合能力异构的综合性任务。
落地路线建议:第一,从 ReAct 模式起步,它最简单也最灵活。第二,工具设计遵循"单一职责",每个工具的 description 要精确描述触发条件和参数含义。第三,所有写操作必须加确认环节,禁止 Agent 直接执行不可逆操作。第四,设置调用次数上限和超时机制,防止 Agent 陷入死循环。第五,生产环境必须记录完整的 Agent 执行轨迹,用于调试和审计。Agent 不是万能的,但架构得当,它是 LLM 从"对话工具"走向"工作伙伴"的关键一步。
更多推荐
所有评论(0)