大模型应用开发--Agent笔记1(定义,ReAct、Plan-and-execute范式,Coze、Dify低代码平台,LangChain、LlamaIndex、AutoGen框架,评估,含代码)
写在前面
本文是我的agent开发学习笔记,学习的内容是datawhale的Hello-Agents开源项目(项目链接),以及xhs上搜集到的一些大模型应用开发相关面经的gpt答案。感谢所有内容来源并且尊重datawhale团队的成果。该笔记中的所有Demo都是我自己跑通并略微思考和修改过的。
我的章节和Hello-Agents有些不同,具体来说:
- 第一章:智能体基础概念
- 第二章:各种方式的智能体构建与范式(基础)
- 第三章:性能评估方式
- 第四章:不同的拓展功能(持续更新)
- 第五章:常见面试问题(持续更新)
由于内容过多,因此分成了两篇博客,本篇博客包括我的第一、二、三章。
Demo没有采用hello-agent打包的框架,而是做成了本地可以运行的最小Demo。后续有时间也会随着不断学习不断更新。
目录
同一个agent需要同时很多function-call,该怎么做?
1.1.什么是智能体(Agent)
在没有学习过智能体开发之前,可能会回答说agent就是“通过让大模型调用一系列api来完成一套完整任务的东西/功能”,但是他的具体定义应该更深奥更完整一些:“任何能够通过传感器(Sensors)感知其所处环境(Environment),并自主地通过执行器(Actuators)采取行动(Action)以达成特定目标的具有自主性(Autonomy)的实体”。

传统智能体有:
- 反射智能体(Simple Reflex Agent):结构最简单,决策核心由工程师明确设计的“条件-动作”规则构成,完全依赖于当前的感知输入,不具备记忆或预测能力。
- 基于模型的反射智能体(Model-Based Reflex Agent):为了改善反射智能体“无法应对需要理解上下文的复杂任务”的局限,研究者们引入了“状态”的概念,让智能体拥有了初级的“记忆”,使其决策不再仅仅依赖于瞬时感知,而是基于一个更连贯、更完整的世界状态理解。
- 基于目标的智能体(Goal-Based Agent):仅仅理解世界还不够,智能体需要有明确的目标。它的行为不再是被动地对环境做出反应,而是主动地、有预见性地选择能够导向某个特定未来状态的行动。这类智能体的核心能力体现在了对未来的考量与规划上。
- 基于效用的智能体(Utility-Based Agent):核心目标不再是简单地达成某个特定状态,而是最大化期望效用。它需要回答一个更复杂的问题:“哪种行为能为我带来最满意的结果?”。这种架构让智能体学会在相互冲突的目标之间进行权衡,使其决策更接近人类的理性选择。
由大语言模型驱动的 LLM 智能体,其核心决策机制与传统智能体存在本质区别,从而赋予了其一系列全新的特性。通过在海量数据上的预训练,获得了隐式的世界模型与强大的涌现能力,使其能够以更灵活、更通用的方式应对复杂任务。LLM 智能体可以直接处理高层级、模糊且充满上下文信息的自然语言指令。目前Agent的核心目标是引导一个通用的“大脑”去规划、行动和学习。
一个简洁的定义:智能体 = 在循环中自主调用工具的 LLM。随着底层模型能力增强,智能体的自治水平便可提升:更能独立探索复杂问题空间,并从错误中恢复。
1.2.智能体运行机制

图为智能体循环 (Agent Loop)。该循环描述了智能体与环境之间的动态交互过程,构成了其自主行为的基础:
- 感知 (Perception):这是循环的起点。智能体通过其传感器(例如,API 的监听端口、用户输入接口)接收来自环境的输入信息。这些信息,即观察 (Observation),既可以是用户的初始指令,也可以是上一步行动所导致的环境状态变化反馈。
- 思考 (Thought):接收到观察信息后,智能体进入其核心决策阶段。对于 LLM 智能体而言,这通常是由大语言模型驱动的内部推理过程。“思考”阶段可进一步细分为两个关键环节:
- 规划 (Planning):智能体基于当前的观察和其内部记忆,更新对任务和环境的理解,并制定或调整一个行动计划。这可能涉及将复杂目标分解为一系列更具体的子任务。
- 工具选择 (Tool Selection):根据当前计划,智能体从其可用的工具库中,选择最适合执行下一步骤的工具,并确定调用该工具所需的具体参数。
- 行动 (Action):决策完成后,智能体通过其执行器(Actuators)执行具体的行动。这通常表现为调用一个选定的工具(如代码解释器、搜索引擎 API),从而对环境施加影响,意图改变环境的状态。
行动并非循环的终点。智能体的行动会引起环境 (Environment) 的状态变化 (State Change),环境随即会产生一个新的观察 (Observation) 作为结果反馈。这个新的观察又会在下一轮循环中被智能体的感知系统捕获,形成一个持续的“感知-思考-行动-观察”的闭环。而思考和行动是核心部分。
Demo:"感知-思考-行动-观察"闭环机制
# ==================== 第一部分:Agent系统提示词 ====================
# 作用:定义Agent的核心行为准则和工作流程
# 在Agent中:作为系统级指令,指导LLM如何思考、行动和输出
AGENT_SYSTEM_PROMPT = """
你是一个智能旅行助手。你的任务是分析用户的请求,并使用可用工具一步步地解决问题。
# 可用工具:
- `get_weather(city: str)`: 查询指定城市的实时天气。
- `get_attraction(city: str, weather: str)`: 根据城市和天气搜索推荐的旅游景点。
# 输出格式要求:
你的每次回复必须严格遵循以下格式,包含一对Thought和Action:
Thought: [你的思考过程和下一步计划]
Action: [你要执行的具体行动]
Action的格式必须是以下之一:
1. 调用工具:function_name(arg_name="arg_value")
2. 结束任务:Finish[最终答案]
# 重要提示:
- 每次只输出一对Thought-Action
- Action必须在同一行,不要换行
- 当收集到足够信息可以回答用户问题时,必须使用 Action: Finish[最终答案] 格式结束
请开始吧!
"""
# ==================== 第二部分:工具函数定义 ====================
# 作用:提供Agent可调用的具体能力(工具)
# 在Agent中:每个工具函数对应一个具体操作,Agent通过调用它们来获取信息或执行任务
import requests
def get_weather(city: str) -> str:
"""
作用:查询真实天气信息
在Agent中:当Agent需要获取某个城市的天气时调用此函数
输入:城市名称
输出:格式化的天气描述字符串
"""
# 调用第三方天气API获取实时数据
url = f"https://wttr.in/{city}?format=j1"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
# 从返回的JSON中提取温度、天气描述等信息
current_condition = data['current_condition'][0]
weather_desc = current_condition['weatherDesc'][0]['value']
temp_c = current_condition['temp_C']
return f"{city}当前天气:{weather_desc},气温{temp_c}摄氏度"
except requests.exceptions.RequestException as e:
return f"错误:查询天气时遇到网络问题 - {e}"
except (KeyError, IndexError) as e:
return f"错误:解析天气数据失败,可能是城市名称无效 - {e}"
import os
from tavily import TavilyClient
def get_attraction(city: str, weather: str) -> str:
"""
作用:根据天气和城市推荐景点
在Agent中:当Agent获取天气后,根据天气情况推荐合适的景点
输入:城市名称、天气描述
输出:景点推荐信息字符串
"""
# 使用Tavily搜索API进行智能搜索
api_key = os.environ.get("TAVILY_API_KEY")
if not api_key:
return "错误:未配置TAVILY_API_KEY环境变量。"
tavily = TavilyClient(api_key=api_key)
# 构造精确的搜索查询,将天气条件纳入考虑
query = f"'{city}' 在'{weather}'天气下最值得去的旅游景点推荐及理由"
try:
response = tavily.search(query=query, search_depth="basic", include_answer=True)
# 优先返回Tavily生成的综合回答
if response.get("answer"):
return response["answer"]
# 否则返回搜索结果摘要
formatted_results = []
for result in response.get("results", []):
formatted_results.append(f"- {result['title']}: {result['content']}")
if not formatted_results:
return "抱歉,没有找到相关的旅游景点推荐。"
return "根据搜索,为您找到以下信息:\n" + "\n".join(formatted_results)
except Exception as e:
return f"错误:执行Tavily搜索时出现问题 - {e}"
# 将工具函数注册到字典中,便于Agent动态调用
# 作用:建立工具名称到函数对象的映射
# 在Agent中:当Agent解析出要调用的工具名称后,从此字典查找对应的函数
available_tools = {
"get_weather": get_weather,
"get_attraction": get_attraction,
}
# ==================== 第三部分:LLM客户端封装 ====================
# 作用:封装与大语言模型的通信逻辑
# 在Agent中:负责将Agent的思考过程(prompt)发送给LLM,获取下一步行动计划
from openai import OpenAI
class OpenAICompatibleClient:
"""
作用:统一接口调用各种兼容OpenAI API的LLM服务
在Agent中:作为Agent的"大脑",每次循环调用此客户端获取Thought-Action
"""
def __init__(self, model: str, api_key: str, base_url: str):
self.model = model
self.client = OpenAI(api_key=api_key, base_url=base_url)
def generate(self, prompt: str, system_prompt: str) -> str:
"""调用LLM API来生成回应。"""
print("正在调用大语言模型...")
try:
messages = [
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': prompt}
]
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
stream=False
)
answer = response.choices[0].message.content
print("大语言模型响应成功。")
return answer
except Exception as e:
print(f"调用LLM API时发生错误: {e}")
return "错误:调用语言模型服务时出错。"
# ==================== 第四部分:Agent主循环 ====================
# 作用:实现ReAct(Reasoning + Acting)模式的Agent核心逻辑
# 在Agent中:协调思考(Reasoning)和行动(Acting)的完整流程
import re
# --- 4.1 配置LLM客户端和初始化环境 ---
# 作用:设置Agent运行所需的API密钥和模型参数
# 在Agent中:决定使用哪个LLM作为推理引擎
API_KEY = "YOUR_API_KEY"
BASE_URL = "YOUR_BASE_URL"
MODEL_ID = "YOUR_MODEL_ID"
TAVILY_API_KEY="YOUR_Tavily_KEY"
os.environ['TAVILY_API_KEY'] = "YOUR_TAVILY_API_KEY"
llm = OpenAICompatibleClient(
model=MODEL_ID,
api_key=API_KEY,
base_url=BASE_URL
)
# --- 4.2 初始化用户请求和对话历史 ---
# 作用:记录完整的交互过程
# 在Agent中:维护Thought-Action-Observation的循环历史,保持上下文连贯性
user_prompt = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
prompt_history = [f"用户请求: {user_prompt}"]
print(f"用户输入: {user_prompt}\n" + "="*40)
# --- 4.3 Agent主循环(最多5次)---
# 作用:执行ReAct循环直到任务完成或达到最大次数
# 在Agent中:每个循环包含 思考(Thought)->行动(Action)->观察(Observation) 三步骤
for i in range(5):
print(f"--- 循环 {i+1} ---\n")
# 步骤1:构建完整prompt(包含历史)
# 作用:为LLM提供完整的上下文
full_prompt = "\n".join(prompt_history)
# 步骤2:调用LLM进行思考,获取下一步行动
# 作用:Agent的"思考"环节
llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
# 清理输出:确保只保留第一个Thought-Action对
# 作用:防止LLM一次性生成多个动作,保持Agent的单步执行特性
match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
if match:
truncated = match.group(1).strip()
if truncated != llm_output.strip():
llm_output = truncated
print("已截断多余的 Thought-Action 对")
print(f"模型输出:\n{llm_output}\n")
prompt_history.append(llm_output)
# 步骤3:解析Action字段
# 作用:从LLM输出中提取具体要执行的操作
action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
if not action_match:
observation = "错误: 未能解析到 Action 字段。请确保你的回复严格遵循 'Thought: ... Action: ...' 的格式。"
observation_str = f"Observation: {observation}"
print(f"{observation_str}\n" + "="*40)
prompt_history.append(observation_str)
continue
action_str = action_match.group(1).strip()
# 步骤4:判断是否完成任务
# 作用:检查Agent是否准备好输出最终答案
if action_str.startswith("Finish"):
final_answer = re.match(r"Finish\[(.*)\]", action_str).group(1)
print(f"任务完成,最终答案: {final_answer}")
break
# 步骤5:解析工具调用
# 作用:从Action字符串中提取工具名称和参数
tool_name = re.search(r"(\w+)\(", action_str).group(1)
args_str = re.search(r"\((.*)\)", action_str).group(1)
kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))
# 步骤6:执行工具函数(Agent的"行动"环节)
# 作用:根据解析出的工具名称调用对应的函数
if tool_name in available_tools:
observation = available_tools[tool_name](**kwargs)
else:
observation = f"错误:未定义的工具 '{tool_name}'"
# 步骤7:记录观察结果(Observation)
# 作用:将工具执行结果反馈给Agent,用于下一轮思考
observation_str = f"Observation: {observation}"
print(f"{observation_str}\n" + "="*40)
prompt_history.append(observation_str)
"感知-思考-行动-观察"闭环是:
用户输入:"查询北京天气,然后根据天气推荐景点"
↓
┌─────────────────────────────────────┐
│ 循环1:感知+思考 │
│ Thought: 需要先获取北京天气 │
│ Action: get_weather(city="北京") │
└─────────────────────────────────────┘
↓
【工具执行:调用天气API】
↓
┌─────────────────────────────────────┐
│ 观察(Observation): │
│ 北京当前天气:晴,气温25摄氏度 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 循环2:基于观察结果再次思考 │
│ Thought: 已获得天气信息(晴天25°C), │
│ 现在需要推荐景点 │
│ Action: get_attraction(city="北京", │
│ weather="晴") │
└─────────────────────────────────────┘
↓
【工具执行:搜索景点推荐】
↓
┌─────────────────────────────────────┐
│ 观察(Observation): │
│ 推荐颐和园、故宫、圆明园... │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 循环3:最终思考 │
│ Thought: 信息已完整,可以回答用户 │
│ Action: Finish[北京今天晴天25°C, │
│ 推荐去颐和园...] │
└─────────────────────────────────────┘
1.3.智能体应用的协作模式
- 作为开发者工具(辅助)的智能体
- Copilot、Claude Code......
- 作为自主协作者的智能体:它标志着我们与 AI 的关系从“命令-执行”演变为“目标-委托”。智能体不再是被动的工具,而是主动的目标追求者。这也是当今(至少2025-2026)“大模型应用开发”岗位所招聘的agent的重点。
- 单智能体自主循环:这是早期的典型范式,如 AgentGPT 所代表的模式。其核心是一个通用智能体通过“思考-规划-执行-反思”的闭环,不断进行自我提示和迭代,以完成一个开放式的高层级目标。
- 多智能体协作:这是当前(写于2026年年中)最主流的探索方向,旨在通过模拟人类团队的协作模式来解决复杂问题。它又可细分为不同模式: 角色扮演式对话:如 CAMEL 框架,通过为两个智能体(例如,“程序员”和“产品经理”)设定明确的角色和沟通协议,让它们在一个结构化的对话中协同完成任务。 组织化工作流:如 MetaGPT 和 CrewAI,它们模拟一个分工明确的“虚拟团队”(如软件公司或咨询小组)。每个智能体都有预设的职责和工作流程(SOP),通过层级化或顺序化的方式协作,产出高质量的复杂成果(如完整的代码库或研究报告)。AutoGen 和 AgentScope 则提供了更灵活的对话模式,允许开发者自定义智能体间的复杂交互网络。
- 高级控制流架构:诸如 LangGraph 等框架,则更侧重于为智能体提供更强大的底层工程基础。它将智能体的执行过程建模为状态图(State Graph),从而能更灵活、更可靠地实现循环、分支、回溯以及人工介入等复杂流程。
为什么要多智能体协作?
多智能体的优点:
| 维度 | 单智能体 | 多智能体协作 |
|---|---|---|
| 上下文管理 | 单个上下文,易超限(prompt history) | 每个Agent独立上下文 |
| 执行效率 | 串行执行 | 并行执行 |
| 专业化 | 一个模型做所有事(有可能多个目标的prompt风格互相干扰,且领域微调会牺牲其他领域的性能) | 每个模型专注一个领域 |
| 容错性 | 一步错,全盘崩 | 单个Agent失败可替换 |
| 可扩展性 | 修改影响全局 | 增减Agent不影响其他 |
多智能体的缺点:
- 通信开销(智能体之间的"沟通成本")与协调复杂度(可能循环等待出现“死锁”)
- 一致性问题:不同Agent可能给出矛盾建议,需要额外的仲裁机制来判断谁对谁错(如天气agent查询到下雨,建议室内活动;而旅游agent建议室外活动)
- 资源消耗增加:n个LLM调用,token成本增加
- 调试困难
多智能体不等于多模型。多智能体的核心是多个具有独立角色、目标、记忆和决策能力的实体之间的协作。它们可以共享同一个大模型后端,但通过不同的system prompt、工具集和记忆状态来实现角色分化。反之,简单地调用多个不同的模型处理一个流水线任务,如果没有智能体之间的自主交互和协商,那只是'多模型调用',不是真正的多智能体系统。
结合xhs上的一些贴主的实际开发经验,多智能体绝非仅仅是简单的设计一堆“岗位”,然后让他们在聊天里互相转述,这样很容易带来上下文损耗变形、错误面扩大偏移、“聊天作为协作方式”并不可靠的问题,并且最终导致token不够用。
实际的多agent应该解决的是真正的“任务分解”(并行处理、权限隔离、结果验证、独立工具与目标),它们之间传递的应该是结构化的结果,最终可以“弥补上下文管理不足和扩充上下文”的目的。多智能体大概率是并行的,这样可以加快速度;同时,并行时他们之间的记忆都是不共享的,终中通过一个整合agent整合回答,若共享记忆,那可能会有安全和幻觉隐患。串行时可以传递结果,这其实和每一轮对话一轮一轮差不多(可以见2.3.4.AutoGen的Demo)。
同一个agent需要同时很多function-call,该怎么做?
实际工程中,并发只是手段,调度才是灵魂。原因有三:
- 资源受限:GPU显存、API的QPS(每秒查询率)、数据库连接池都是有限的,不能无限并发。
- 依赖约束:Function B 必须等 Function A 的结果才能运行(即数据依赖)。
- 成本控制:调度的目标是在满足依赖的前提下,最小化总耗时(makespan) 或 最小化成本。
很多个fuction同时调用:核心在于调度而非并发,先实现一个规划agent实现调度DAG(即“做什么”),而调度器(Scheduler)负责执行DAG(即“何时做、在哪做”)。两者通常是分离的。
- DAG:有向无环图(Directed Acyclic Graph),它是描述任务依赖关系的数学结构。无环是因为绝对不能有循环依赖(即 A依赖B,B又依赖A),否则会死锁。
Workflow和Agent的差异

简单来说,Workflow 是让 AI 按部就班地执行指令,而 Agent 则是赋予 AI 自由度去自主达成目标。
- 工作流是一种传统的自动化范式,其核心是对一系列任务或步骤进行预先定义的、结构化的编排。它本质上是一个精确的、静态的流程图,规定了在何种条件下、以何种顺序执行哪些操作。
- 基于大型语言模型的智能体是一个具备自主性的、以目标为导向的系统。它不仅仅是执行预设指令,而是能够在一定程度上理解环境、进行推理、制定计划,并动态地采取行动以达成最终目标。LLM 在其中扮演着“大脑”的角色,具有充分的自主性,没有任何写死的规则。这种基于实时信息进行动态推理和决策的能力,正是 Agent 的核心价值所在。
| 维度 | Workflow | Agent |
|---|---|---|
| 决策方式 | 预定义路径,if-else分支 | 动态推理,LLM决策 |
| 确定性 | 高,每一步都明确 | 低,根据情况调整 |
| 可预测性 | 强,输出可预期 | 弱,可能"发散" |
| 成本 | 低(规则+少量LLM) | 高(多次LLM调用) |
| 延迟 | 低 | 高 |
| 容错性 | 差,规则外就失败 | 好,可以自我纠错 |
| 维护成本 | 规则变化需改代码 | 改prompt即可 |
| 适用场景 | 流程固定,逻辑清晰 | 开放、探索性任务 |
Workflow适合"做已知的事"(所有的可能和步骤都能预先枚举),Agent适合"探索未知的事"(路径需要根据中间结果动态决定)。最佳实践是Workflow控制流程(固定步骤),Agent处理决策点(结果判断)。
2.1.智能体经典范式
一个现代的智能体,其核心能力在于能将大语言模型的推理能力与外部世界联通。它能够自主地理解用户意图、拆解复杂任务,并通过调用代码解释器、搜索引擎、API等一系列“工具”,来获取信息、执行操作,最终达成目标。 然而,智能体并非万能,它同样面临着来自大模型本身的“幻觉”问题、在复杂任务中可能陷入推理循环、以及对工具的错误使用等挑战,这些也构成了智能体的能力边界。
Hello-Agent教程专注最具代表性的三种:
- ReAct (Reasoning and Acting): 一种将“思考”和“行动”紧密结合的范式,让智能体边想边做,动态调整。
- Plan-and-Solve: 一种“三思而后行”的范式,智能体首先生成一个完整的行动计划,然后严格执行。
- Reflection: 一种赋予智能体“反思”能力的范式,通过自我批判和修正来优化结果。
2.1.1.ReAct (Reason + Act)
ReAct的核心思想是模仿人类解决问题的方式,将推理 (Reasoning) 与行动 (Acting) 显式地结合起来,形成一个“思考-行动-观察”的循环。(也就是1.2节中demo所演示的)
ReAct工作流程
在ReAct诞生之前,主流的方法可以分为两类:一类是“纯思考”型,如思维链 (Chain-of-Thought),它能引导模型进行复杂的逻辑推理,但无法与外部世界交互,容易产生事实幻觉;另一类是“纯行动”型,模型直接输出要执行的动作,但缺乏规划和纠错能力。
ReAct的巧妙之处在于,它认识到思考与行动是相辅相成的。思考指导行动,而行动的结果又反过来修正思考。为此,ReAct范式通过一种特殊的提示工程来引导模型,使其每一步的输出都遵循一个固定的轨迹:
- Thought (思考): 这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。
- Action (行动): 这是智能体决定采取的具体动作,通常是调用一个外部工具,例如
Search[“华为最新款手机”]。 - Observation (观察): 这是执行
Action后从外部工具返回的结果,例如搜索结果的摘要或API的返回值。
智能体将不断重复这个 Thought -> Action -> Observation 的循环,将新的观察结果追加到历史记录中,形成一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。这个过程形成了一个强大的协同效应:推理使得行动更具目的性,而行动则为推理提供了事实依据。

这种机制特别适用于以下场景:
- 需要外部知识的任务:如查询实时信息(天气、新闻、股价)、搜索专业领域的知识等。
- 需要精确计算的任务:将数学问题交给计算器工具,避免LLM的计算错误。
- 需要与API交互的任务:如操作数据库、调用某个服务的API来完成特定功能。
Demo:一个具备使用外部工具能力的ReAct智能体
如果说大语言模型是智能体的大脑,那么工具 (Tools) 就是其与外部世界交互的“手和脚”。
一个良好定义的工具应包含以下三个核心要素:
- 名称 (Name): 一个简洁、唯一的标识符,供智能体在
Action中调用,例如Search。 - 描述 (Description): 一段清晰的自然语言描述,说明这个工具的用途。这是整个机制中最关键的部分,因为大语言模型会依赖这段描述来判断何时使用哪个工具。
- 执行逻辑 (Execution Logic): 真正执行任务的函数或方法。
当智能体需要使用多种工具时(例如,除了搜索,还可能需要计算、查询数据库等),我们需要一个统一的管理器来注册和调度这些工具,通常包含:
- 注册新工具的函数(def registerTool):检查name是否重复,并注册对应的description和function(函数名)。
- 调用工具的函数(def getTool):根据name获取function。
- 获取所有可用工具名的函数(def getAvailableTools):输出所有name和description组合。
系统提示词是整个 ReAct 机制的基石,它为大语言模型提供了行动的操作指令。我们需要精心设计一个模板,它将动态地插入可用工具、用户问题以及中间步骤的交互历史。
还需要设定max_steps 参数作为一个重要的安全阀,防止智能体陷入无限循环而耗尽资源。
最后一步,也是形成闭环的关键,是将Action本身和工具执行后的Observation添加回历史记录中,为下一轮循环提供新的上下文。
针对本demo——回答关于“苹果最新发布的手机是什么”的问题,我们需要为智能体提供一个网页搜索工具。(相较于Hello-agent的官方代码仓库,由于是单智能体demo,因此在SYSTEM_PROMPT中设置了一次只能输出一对“思考-执行”,并且对Action的异常格式输出做了一些额外匹配)
"""
ReAct Agent 实现
功能:创建一个能够调用外部工具(如搜索引擎)的智能体,通过思考-行动-观察循环回答问题
"""
import os
import re
from typing import List, Dict, Any
from openai import OpenAI
from dotenv import load_dotenv
from serpapi import SerpApiClient
# ==================== 1. 配置加载模块 ====================
# 作用:加载 .env 文件中的环境变量(API密钥、模型配置等)
load_dotenv()
# ==================== 2. LLM客户端封装模块 ====================
class HelloAgentsLLM:
"""
作用:封装大语言模型(LLM)客户端
功能:
- 初始化OpenAI兼容的API连接
- 提供 think() 方法调用模型进行思考(流式响应)
"""
def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
"""
初始化LLM客户端
优先使用传入参数,否则从环境变量读取
"""
self.model = model or os.getenv("LLM_MODEL_ID")
apiKey = apiKey or os.getenv("LLM_API_KEY")
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
# 验证必要参数
if not all([self.model, apiKey, baseUrl]):
raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
# 创建OpenAI客户端
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
"""
调用LLM进行思考
参数:
- messages: 对话消息列表
- temperature: 温度参数(控制随机性,默认0表示确定性输出)
返回:模型响应的文本内容
"""
print(f"🧠 正在调用 {self.model} 模型...")
try:
# 发起流式请求
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
)
# 处理流式响应
print("✅ 大语言模型响应成功:")
collected_content = []
for chunk in response:
if not chunk.choices:
continue
content = chunk.choices[0].delta.content or ""
print(content, end="", flush=True) # 实时打印
collected_content.append(content)
print() # 换行
return "".join(collected_content)
except Exception as e:
print(f"❌ 调用LLM API时发生错误: {e}")
return None
# ==================== 3. 工具定义模块 ====================
def search(query: str) -> str:
"""
作用:搜索引擎工具(基于SerpApi)
功能:
- 执行Google搜索
- 智能解析结果:优先返回答案框/知识图谱,否则返回前3个搜索摘要
"""
print(f"🔍 正在执行 [SerpApi] 网页搜索: {query}")
try:
api_key = os.getenv("SERPAPI_API_KEY")
if not api_key:
return "错误:SERPAPI_API_KEY 未在 .env 文件中配置。"
# 配置搜索参数
params = {
"engine": "google",
"q": query,
"api_key": api_key,
"gl": "cn", # 地区:中国
"hl": "zh-cn", # 语言:简体中文
}
# 执行搜索
client = SerpApiClient(params)
results = client.get_dict()
# 智能解析结果(优先级从高到低)
# 1. 答案列表
if "answer_box_list" in results:
return "\n".join(results["answer_box_list"])
# 2. 答案框
if "answer_box" in results and "answer" in results["answer_box"]:
return results["answer_box"]["answer"]
# 3. 知识图谱描述
if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
return results["knowledge_graph"]["description"]
# 4. 前3个有机搜索结果摘要
if "organic_results" in results and results["organic_results"]:
snippets = [
f"[{i+1}] {res.get('title', '')}\n{res.get('snippet', '')}"
for i, res in enumerate(results["organic_results"][:3])
]
return "\n\n".join(snippets)
return f"对不起,没有找到关于 '{query}' 的信息。"
except Exception as e:
return f"搜索时发生错误: {e}"
# ==================== 4. 工具执行器模块 ====================
class ToolExecutor:
"""
作用:管理和执行工具的注册器
功能:
- registerTool(): 注册新工具
- getTool(): 根据名称获取工具函数
- getAvailableTools(): 获取所有可用工具的描述
"""
def __init__(self):
self.tools: Dict[str, Dict[str, Any]] = {} # 存储工具:{名称: {描述, 函数}}
def registerTool(self, name: str, description: str, func: callable):
"""注册一个新工具"""
if name in self.tools:
print(f"警告:工具 '{name}' 已存在,将被覆盖。")
self.tools[name] = {"description": description, "func": func}
print(f"工具 '{name}' 已注册。")
def getTool(self, name: str) -> callable:
"""根据名称获取工具函数"""
return self.tools.get(name, {}).get("func")
def getAvailableTools(self) -> str:
"""获取所有可用工具的格式化描述(用于提示词)"""
return "\n".join([
f"- {name}: {info['description']}"
for name, info in self.tools.items()
])
//ToDO:还可以进一步完善,输出tool所需的参数(数量及格式等),以便LLM执行。
# ==================== 5. ReAct提示词模板 ====================
# 作用:定义LLM的响应格式规范
# 强制LLM输出:Thought(思考)+ Action(行动)的结构
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。
可用工具如下:
{tools}
请严格按照以下格式进行回应:
Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
**重要**:Finish 中的最终答案应该简洁明了,不要包含换行符。如果需要多行内容,请用空格或标点符号分隔。
**注意**:每次只能输出【一对】Thought 和 Action,完成当前 Action 并获得观察结果后,才能进行下一轮思考。
现在,请开始解决以下问题:
Question: {question}
History: {history}
"""
# ==================== 6. ReAct智能体核心模块 ====================
class ReActAgent:
"""
作用:实现ReAct(Reasoning + Acting)范式的智能体
工作流程:
1. 思考(Thought):分析问题并决定下一步
2. 行动(Action):调用工具或输出最终答案
3. 观察(Observation):获取工具执行结果
4. 循环直至获得答案或达到最大步数
"""
def __init__(self, llm_client: HelloAgentsLLM, tool_executor: ToolExecutor, max_steps: int = 6):
"""
初始化ReAct智能体
参数:
- llm_client: LLM客户端
- tool_executor: 工具执行器
- max_steps: 最大循环步数(防止无限循环)
"""
self.llm_client = llm_client
self.tool_executor = tool_executor
self.max_steps = max_steps
self.history = [] # 记录行动-观察历史
def run(self, question: str):
"""
运行ReAct智能体解决问题
这是主要入口函数,包含核心的思考-行动循环
"""
self.history = [] # 重置历史
current_step = 0
while current_step < self.max_steps:
current_step += 1
print(f"\n--- 第 {current_step} 步 ---")
# 步骤1:格式化提示词
tools_desc = self.tool_executor.getAvailableTools()
history_str = "\n".join(self.history)
prompt = REACT_PROMPT_TEMPLATE.format(
tools=tools_desc,
question=question,
history=history_str
)
# 步骤2:调用LLM进行思考
messages = [{"role": "user", "content": prompt}]
response_text = self.llm_client.think(messages=messages)
if not response_text:
print("错误:LLM未能返回有效响应。")
break
# 步骤3:解析LLM的输出(提取Thought和Action)
thought, action = self._parse_output(response_text)
if thought:
print(f"💭 思考: {thought}")
if not action:
print("警告:未能解析出有效的Action,流程终止。")
break
# 清理 action 字符串(移除多余的换行和空格)
action = action.strip()
# 步骤4:执行Action
# 情况A:输出最终答案(修复正则表达式以支持多行内容)
if action.startswith("Finish"):
# 使用更灵活的正则表达式匹配 Finish[...],支持跨行匹配
finish_match = re.match(r"Finish\[(.*?)\]", action, re.DOTALL)
if finish_match:
final_answer = finish_match.group(1).strip()
print(f"🎉 最终答案: {final_answer}")
return final_answer
else:
# 如果无法匹配标准格式,尝试提取 Finish 之后的所有内容
print(f"警告:无法解析 Finish 格式: {action}")
# 降级处理:移除 "Finish" 前缀后作为答案
final_answer = action.replace("Finish", "").strip("[] \n")
if final_answer:
print(f"🎉 最终答案(降级解析): {final_answer}")
return final_answer
else:
print("错误:Finish 指令格式错误,无法提取答案。")
break
# 情况B:调用工具
tool_name, tool_input = self._parse_action(action)
if not tool_name or not tool_input:
print(f"警告:无法解析Action格式: {action}")
# 记录无效action到历史,避免重复
self.history.append(f"Action: {action}")
self.history.append(f"Observation: 错误:Action格式无效,请使用正确格式 ToolName[input] 或 Finish[答案]")
continue
print(f"🎬 行动: {tool_name}[{tool_input}]")
# 执行工具
tool_function = self.tool_executor.getTool(tool_name)
if not tool_function:
observation = f"错误:未找到名为 '{tool_name}' 的工具。"
else:
observation = tool_function(tool_input)
print(f"👀 观察: {observation}")
# 记录历史(供下一轮LLM参考)
self.history.append(f"Action: {action}")
self.history.append(f"Observation: {observation}")
# 超出最大步数
print("⚠️ 已达到最大步数,流程终止。")
return None
def _parse_output(self, text: str):
"""
作用:从LLM响应中提取Thought和Action
使用正则表达式匹配格式:
Thought: ...
Action: ...
修复:支持 Action 内容跨越多行的情况
"""
# 提取Thought(到Action或文本末尾)
thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
# 提取Action(改进版:支持多行内容,直到遇到下一个Thought或文本末尾)
# 使用非贪婪匹配,在遇到下一个 "Thought:" 或字符串结束时停止
action_match = re.search(r"Action:\s*(.*?)(?=\nThought:|$)", text, re.DOTALL)
# 如果没有匹配到,尝试匹配到字符串末尾
if not action_match:
action_match = re.search(r"Action:\s*(.*)$", text, re.DOTALL)
thought = thought_match.group(1).strip() if thought_match else None
action = action_match.group(1).strip() if action_match else None
return thought, action
def _parse_action(self, action_text: str):
"""
作用:解析Action字符串,提取工具名称和输入参数
格式:ToolName[input]
修复:支持 input 中包含各种字符(包括中文标点、括号等)
"""
# 改进正则表达式:支持工具名称后跟方括号,匹配到最后一个闭合方括号
# 使用贪婪匹配确保获取完整的 input 内容
match = re.match(r"(\w+)\[(.*)\]$", action_text, re.DOTALL)
if match:
return match.group(1), match.group(2).strip()
# 如果上述匹配失败,尝试更宽松的匹配(允许输入中包含未转义的括号)
match = re.match(r"(\w+)\[(.*)", action_text, re.DOTALL)
if match:
tool_name = match.group(1)
tool_input = match.group(2).rstrip("]").strip()
return tool_name, tool_input
return None, None
# ==================== 7. 主程序入口 ====================
if __name__ == '__main__':
# 1. 初始化LLM客户端
llm = HelloAgentsLLM()
# 2. 创建工具执行器并注册搜索引擎工具
tool_executor = ToolExecutor()
search_desc = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
tool_executor.registerTool("Search", search_desc, search)
# 3. 创建ReAct智能体
agent = ReActAgent(llm_client=llm, tool_executor=tool_executor, max_steps=6)
# 4. 定义问题并运行
question = "苹果最新发布的手机是什么(截至今天,2026年5月)?它的主要卖点是什么?"
agent.run(question)
PS D:\Microsoft VS Code\vs work\codeworkvs\other\Agent\ReactDemo> python .\demo.py
工具 'Search' 已注册。
--- 第 1 步 ---
🧠 正在调用 xiaomi-mimo-v2.5-pro 模型...
✅ 大语言模型响应成功:
Thought: 用户想知道截至2026年5月苹果最新发布的手机型号及其主要卖点。由于我的知识库截止于2025年7月,我需要通过网络搜索获取最新信息。首先,我需要搜索苹果在2026年发布的新款手机信息,然后提取其主要卖点。
Action: Search[苹果 2026年 最新发布 手机 型号]
💭 思考: 用户想知道截至2026年5月苹果最新发布的手机型号及其主要卖点。由于我的知识库截止于2025年7月,我需要通过网络搜索获取最新信息。首先,我需要搜索苹果在2026年发布的新款手机信息,然后提取其主要卖点。
🎬 行动: Search[苹果 2026年 最新发布 手机 型号]
🔍 正在执行 [SerpApi] 网页搜索: 苹果 2026年 最新发布 手机 型号
👀 观察: [1] 苹果2026年新品前瞻:折叠屏iPhone在列,超20款产品将登场
... 发布Mac或是iPad。 iPhone 18 Pro系列,相比于往年,今年苹果将改变手机发布策略,秋季发布会上将只有两款Pro机型,标准版则会放到第二年的春季。这代 ...
[2] 2026年苹果手机推荐:五款旗舰机型深度解析与选购指南
五款机型——iPhone 13 Pro、iPhone Air、iPhone 17、iPhone 15和iPhone 14 Pro,每一款都展现了不同的特色与风格。 🕴️ 商务场合首选 续航表现卓越…
[3] 7 款新品齊發! 2026年的蘋果太瘋狂了! | 日更第43天| iPhone18 ...
7 款新品齊發! 2026年的蘋果太瘋狂了! | 日更第43天| iPhone18 / iPhone折疊機/ Siri 2.0 / 20週年Macbook / HomePod | 大耳朵TV.
--- 第 2 步 ---
🧠 正在调用 xiaomi-mimo-v2.5-pro 模型...
✅ 大语言模型响应成功:
Thought: 用户询问截至2026年5月,苹果最新发布的手机是什么及其主要卖点。根据提供的历史搜索结果,之前的搜索似乎包含了对2026年的预测或展望(例如提到iPhone 18 Pro系列和折叠屏iPhone),但这些可能并非“最新发布”的确切信息,特别是用户指定了“截至今天,2026年5月”。历史结果中的第二条似乎列出了一些旧型号(如iPhone 13 Pro、iPhone 17),这不太可能是2026年“最新”的。因此,我需要更精确地搜索2026年5月之前苹果实际发布的新款手机型号。我将使用搜索工具,查询关键词聚焦于2026年已发布或近期发布的iPhone型号。
Action: Search[苹果 2026年 已发布 手机 主要卖点]
💭 思考: 用户询问苹果截至2026年5月最新发布的手机及其主要卖点。由于我的知识截止到2025年,且当前日期是2026年5月,我需要搜索最新信息来回答这个问题。我将使用搜索引擎查找相关信息。
🎬 行动: Search[苹果 2026年 最新发布 手机 主要卖点]
🔍 正在执行 [SerpApi] 网页搜索: 苹果 2026年 已发布 手机 主要卖点
👀 观察: [1] 苹果首款折叠屏手机计划今年秋季发布!这些A股公司或受益
如无意外,苹果公司首款折叠屏手机将于今年秋季与苹果新款iPhone一起发布,目前各界对其出货量持乐观态度,预计A股苹果公司供应链企业业绩将获提振。
[2] 接下来,苹果的iPhone 和小米手机一样多
接下来,苹果的iPhone 和小米手机一样多 · iPhone 一年7 台,价位全覆盖 · 不过,我们都知道今年真正的主角,将会是苹果首款折叠屏iPhone Fold,真正的定名可能是「iPhone Ultra」 ...
[3] 2026年苹果iPhone手机推荐哪一款性价比最高?(8000字选 ...
iPhone 17系列的更新重点主要集中在A19系列处理器、高刷下放、摄影系统提升以及VC散热技术等方面,使得性能、摄影能力、显示技术和散热表现等方面都有了显著的提升。特别 ...
--- 第 3 步 ---
🧠 正在调用 xiaomi-mimo-v2.5-pro 模型...
✅ 大语言模型响应成功:
Thought: 我需要根据用户提供的搜索结果和当前日期(2026年5月)来回答。搜索结果显示苹果首款折叠屏手机计划于2026年秋季发布,因此尚未发布。而搜索结果[3]提到了iPhone 17系列及其更新重点,这暗示iPhone 17系列可能是截至2026年5月最新发布的手机。主要卖点从搜索结果[3]中提取,包括A19系列处理器、高刷下放、摄影系统提升和VC散热技术。
Action: Finish[苹果最新发布的手机是iPhone 17系列,主要卖点是搭载A19系列处理器、高刷新率显示、摄影系统提升和VC散热技术。]
💭 思考: 我需要根据用户提供的搜索结果和当前日期(2026年5月)来回答。搜索结果显示苹果首款折叠屏手机计划于2026年秋季发布,因此尚未发布。而搜索结果[3]提到了iPhone 17系列及其更新重点,这暗示iPhone 17系列可能是截至2026年5月最新发布的手机。主要卖点从搜索结果[3]中提取,包括A19系列处理器、高刷下放、摄影系统提升和VC散热技术。
🎉 最终答案: 苹果最新发布的手机是iPhone 17系列,主要卖点是搭载A19系列处理器、高刷新率显示、摄影系统提升和VC散热技术。
.create()和.generate()个人解析补充
调用api模型生成时.create()和.generate()的使用场景区别:一个是 OpenAI 风格,一个是Hugging Face / Transformers 风格。
# OpenAI SDK 风格
from openai import OpenAI
client = OpenAI()
response = self.client.chat.completions.create(
# 统一接口,直接传递参数即可
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}], # 需要区分角色
temperature=0.7,
max_tokens=100,
stream=True
)
# Transformers 库风格
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("gpt2")
tokenizer = AutoTokenizer.from_pretrained("gpt2") # 需要先手动分词
inputs = tokenizer("Hello", return_tensors="pt")
streamer = TextStreamer(tokenizer, skip_prompt=True) # 流式需要先行定义类
outputs = model.generate(**inputs, max_length=50, streamer=streamer, temperature=0.7)
ReAct的优缺点和调试技巧
主要优点:
- 高可解释性:ReAct 最大的优点之一就是透明。通过
Thought链,我们可以清晰地看到智能体每一步的“心路历程”——它为什么会选择这个工具,下一步又打算做什么。这对于理解、信任和调试智能体的行为至关重要。 - 动态规划与纠错能力:与一次性生成完整计划的范式不同,ReAct 是“走一步,看一步”。它根据每一步从外部世界获得的
Observation来动态调整后续的Thought和Action。如果上一步的搜索结果不理想,它可以在下一步中修正搜索词,重新尝试(如第一步到第二步的调整)。 - 工具协同能力:ReAct 范式天然地将大语言模型的推理能力与外部工具的执行能力结合起来。LLM 负责运筹帷幄(规划和推理),工具负责解决具体问题(搜索、计算),二者协同工作,突破了单一 LLM 在知识时效性、计算准确性等方面的固有局限。
固有局限性:
- 对LLM自身能力的强依赖:ReAct 流程的成功与否,高度依赖于底层 LLM 的综合能力。如果 LLM 的逻辑推理能力、指令遵循能力或格式化输出能力不足,就很容易在
Thought环节产生错误的规划,或者在Action环节生成不符合格式的指令,导致整个流程中断。 - 执行效率问题:由于其循序渐进的特性,完成一个任务通常需要多次调用 LLM。每一次调用都伴随着网络延迟和计算成本。对于需要很多步骤的复杂任务,这种串行的“思考-行动”循环可能会导致较高的总耗时和费用(这个demo任务就大致需要10k tokens)。
- 提示词的脆弱性:整个机制的稳定运行建立在一个精心设计的提示词模板之上。模板中的任何微小变动,甚至是用词的差异,都可能影响 LLM 的行为。此外,并非所有模型都能持续稳定地遵循预设的格式,这增加了在实际应用中的不确定性。
- 可能陷入局部最优:步进式的决策模式意味着智能体缺乏一个全局的、长远的规划。它可能会因为眼前的
Observation而选择一个看似正确但长远来看并非最优的路径,甚至在某些情况下陷入“原地打转”的循环中(如有可能一直在搜索,直到maxstep了都没有搜到一个LLM自认为满意的答案)。
调试技巧:
- 检查完整的提示词:在每次调用 LLM 之前,将最终格式化好的、包含所有历史记录的完整提示词打印出来。这是追溯 LLM 决策源头的最直接方式。
- 分析原始输出:当输出解析失败时(例如,正则表达式没有匹配到
Action),务必将 LLM 返回的原始、未经处理的文本打印出来。这能帮助你判断是 LLM 没有遵循格式,还是你的解析逻辑有误。 - 验证工具的输入与输出:检查智能体生成的
tool_input是否是工具函数所期望的格式,同时也要确保工具返回的observation格式是智能体可以理解和处理的。 - 调整提示词中的示例 (Few-shot Prompting):如果模型频繁出错,可以在提示词中加入一两个完整的“Thought-Action-Observation”成功案例,通过示例来引导模型更好地遵循你的指令(但是few-shot可能会影响模型的泛化能力,尤其是小模型,会不着调的往few-shot的样例靠拢)。
- 尝试不同的模型或参数:更换一个能力更强的模型,或者调整
temperature参数(通常设为0以保证输出的确定性),有时能直接解决问题。
2.1.2.Plan-and-Solve
这种范式将任务处理明确地分为两个阶段:先规划 (Plan),后执行 (Solve)。
如果说 ReAct 像一个经验丰富的侦探,根据现场的蛛丝马迹(Observation)一步步推理,随时调整自己的调查方向;那么 Plan-and-Solve 则更像一位建筑师,在动工之前必须先绘制出完整的蓝图(Plan),然后严格按照蓝图来施工(Solve)。事实上我们现在用的很多大模型工具的Agent模式都融入了这种设计模式。
P-&-S工作原理

其核心动机是为了解决思维链在处理多步骤、复杂问题时容易“偏离轨道”的问题。与 ReAct 将思考和行动融合在每一步不同,Plan-and-Solve 将整个流程解耦为两个核心阶段:
- 规划阶段 (Planning Phase): 首先,智能体会接收用户的完整问题。它的第一个任务不是直接去解决问题或调用工具,而是将问题分解,并制定出一个清晰、分步骤的行动计划。这个计划本身就是一次大语言模型的调用产物。
- 执行阶段 (Solving Phase): 在获得完整的计划后,智能体进入执行阶段。它会严格按照计划中的步骤,逐一执行。每一步的执行都可能是一次独立的 LLM 调用,或者是对上一步结果的加工处理,直到计划中的所有步骤都完成,最终得出答案。
这种“先谋后动”的策略,使得智能体在处理需要长远规划的复杂任务时,能够保持更高的目标一致性,避免在中间步骤中迷失方向。在执行阶段,执行模型 πsolve 会逐一完成计划中的步骤。对于第 i 个步骤,其解决方案 si 的生成会同时依赖于原始问题 q、完整计划 P 以及之前所有步骤的执行结果(s1,…,si−1)。
Plan-and-Solve 尤其适用于那些结构性强、可以被清晰分解的复杂任务,例如:
- 多步数学应用题:需要先列出计算步骤,再逐一求解。
- 需要整合多个信息源的报告撰写:需要先规划好报告结构(引言、数据来源A、数据来源B、总结),再逐一填充内容。
- 代码生成任务:需要先构思好函数、类和模块的结构,再逐一实现。
这类任务的特点是,答案无法通过单次查询或计算得出,必须先将问题分解为一系列逻辑连贯的子步骤,然后按顺序求解。这恰好能发挥 Plan-and-Solve “先规划,后执行”的核心能力。
Demo:一个提示词约束的规划+执行器
规划阶段的目标是让大语言模型接收原始问题,并输出一个清晰、分步骤的行动计划。这个计划必须是结构化的,以便我们的代码可以轻松解析并逐一执行。因此,我们设计的提示词需要明确地告诉模型它的角色和任务,并给出一个输出格式的范例。
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。
问题: {question}
请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
```
"""
在规划器 (Planner) 生成了清晰的行动蓝图后,我们就需要一个执行器 (Executor) 来逐一完成计划中的任务。执行器不仅负责调用大语言模型来解决每个子问题,还承担着一个至关重要的角色:状态管理。它必须记录每一步的执行结果,并将其作为上下文提供给后续步骤,确保信息在整个任务链条中顺畅流动。
执行器的提示词与规划器不同。它的目标不是分解问题,而是在已有上下文的基础上,专注解决当前这一个步骤。因此,提示词需要包含以下关键信息:
- 原始问题: 确保模型始终了解最终目标。
- 完整计划: 让模型了解当前步骤在整个任务中的位置。
- 历史步骤与结果: 提供至今为止已经完成的工作,作为当前步骤的直接输入。
- 当前步骤: 明确指示模型现在需要解决哪一个具体任务。
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决“当前步骤”,并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。
# 原始问题:
{question}
# 完整计划:
{plan}
# 历史步骤与结果:
{history}
# 当前步骤:
{current_step}
请仅输出针对“当前步骤”的回答:
"""
import os
import re
from typing import List, Dict, Any
from openai import OpenAI
from dotenv import load_dotenv
from serpapi import SerpApiClient
import ast
from typing import Any
load_dotenv()
class HelloAgentsLLM:
"""
作用:封装大语言模型(LLM)客户端
功能:
- 初始化OpenAI兼容的API连接
- 提供 think() 方法调用模型进行思考(流式响应)
"""
def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
"""
初始化LLM客户端
优先使用传入参数,否则从环境变量读取
"""
self.model = model or os.getenv("LLM_MODEL_ID")
apiKey = apiKey or os.getenv("LLM_API_KEY")
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
# 验证必要参数
if not all([self.model, apiKey, baseUrl]):
raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
# 创建OpenAI客户端
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
"""
调用LLM进行思考
参数:
- messages: 对话消息列表
- temperature: 温度参数(控制随机性,默认0表示确定性输出)
返回:模型响应的文本内容
"""
print(f"🧠 正在调用 {self.model} 模型...")
try:
# 发起流式请求
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
)
# 处理流式响应
print("✅ 大语言模型响应成功:")
collected_content = []
for chunk in response:
if not chunk.choices:
continue
content = chunk.choices[0].delta.content or ""
print(content, end="", flush=True) # 实时打印
collected_content.append(content)
print() # 换行
return "".join(collected_content)
except Exception as e:
print(f"❌ 调用LLM API时发生错误: {e}")
return None
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的 AI 规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个 Python 列表,其中每个元素都是一个描述子任务的字符串。
问题:
{question}
请严格按照以下格式输出计划。```python 与 ``` 作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
```
"""
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的 AI 执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整计划,以及到目前为止已经完成的步骤和结果。
请专注于解决“当前步骤”,并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。
原始问题:
{question}
完整计划:
{plan}
历史步骤与结果:
{history}
当前步骤:
{current_step}
请仅输出针对“当前步骤”的回答:
"""
class Planner:
"""负责将复杂问题拆分为多个可执行步骤。"""
def __init__(self, llm_client: Any) -> None:
self.llm_client = llm_client
def plan(self, question: str) -> list[str]:
"""
根据用户问题生成行动计划。
Args:
question: 用户提出的问题。
Returns:
由多个步骤组成的字符串列表。解析失败时返回空列表。
"""
prompt = PLANNER_PROMPT_TEMPLATE.format(question=question)
messages = [{"role": "user", "content": prompt}]
print("\n--- 正在生成计划 ---")
response_text = self.llm_client.think(messages=messages) or ""
print(f"✅ 计划已生成:\n{response_text}")
return self._parse_plan(response_text)
@staticmethod
def _parse_plan(response_text: str) -> list[str]:
"""
从大语言模型的回复中提取 Python 列表。
Args:
response_text: 大语言模型返回的原始文本。
Returns:
合法的步骤列表。解析失败时返回空列表。
"""
try:
# 提取 ```python 和 ``` 之间的内容
plan_str = response_text.split("```python", maxsplit=1)[1]
plan_str = plan_str.split("```", maxsplit=1)[0].strip()
# 安全解析 Python 字面量
plan = ast.literal_eval(plan_str)
# 检查列表中的每个元素是否都是非空字符串
if not isinstance(plan, list):
raise TypeError("计划必须是 Python 列表。")
if not all(isinstance(step, str) and step.strip() for step in plan):
raise TypeError("计划中的每个步骤都必须是非空字符串。")
return plan
except (ValueError, SyntaxError, IndexError, TypeError) as error:
print(f"❌ 解析计划时出错:{error}")
print(f"原始响应:\n{response_text}")
return []
class Executor:
"""负责按照规划器生成的计划逐步执行任务。"""
def __init__(self, llm_client: Any) -> None:
self.llm_client = llm_client
def execute(self, question: str, plan: list[str]) -> str:
"""
根据行动计划逐步执行任务。
Args:
question: 用户提出的原始问题。
plan: 规划器生成的行动计划。
Returns:
最后一个步骤的执行结果。
"""
if not plan:
return ""
history = ""
final_answer = ""
print("\n--- 正在执行计划 ---")
for index, current_step in enumerate(plan, start=1):
print(f"\n-> 正在执行步骤 {index}/{len(plan)}:{current_step}")
prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question,
plan=plan,
history=history or "无",
current_step=current_step,
)
messages = [{"role": "user", "content": prompt}]
response_text = self.llm_client.think(messages=messages) or ""
history += (
f"步骤 {index}:{current_step}\n"
f"结果:{response_text}\n\n"
)
final_answer = response_text
print(f"✅ 步骤 {index} 已完成,结果:\n{response_text}")
return final_answer
class PlanAndSolveAgent:
"""先生成计划,再逐步执行计划。"""
def __init__(self, llm_client: Any) -> None:
self.planner = Planner(llm_client)
self.executor = Executor(llm_client)
def run(self, question: str) -> str:
"""
运行完整的 Plan-and-Solve 流程。
Args:
question: 用户提出的问题。
Returns:
最终答案。规划失败时返回空字符串。
"""
print(f"\n--- 开始处理问题 ---\n问题:{question}")
# 1. 生成计划
plan = self.planner.plan(question)
if not plan:
print("\n--- 任务终止 ---")
print("无法生成有效的行动计划。")
return ""
# 2. 执行计划
final_answer = self.executor.execute(question, plan)
print(f"\n--- 任务完成 ---\n最终答案:\n{final_answer}")
return final_answer
def main() -> None:
"""程序入口。"""
llm_client = HelloAgentsLLM()
agent = PlanAndSolveAgent(llm_client)
question = input("请输入需要解决的问题:").strip()
if not question:
print("❌ 问题不能为空。")
return
agent.run(question)
if __name__ == "__main__":
main()
输出:
PS D:\Microsoft VS Code\vs work\codeworkvs\other\Agent\demo> python .\PaSdemo.py
请输入需要解决的问题:计算1 + (15 + 7) × 3 - 10 的结果
--- 开始处理问题 ---
问题:计算1 + (15 + 7) × 3 - 10 的结果
--- 正在生成计划 ---
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
```python
["先计算括号内的加法:15 + 7", "将括号内的结果乘以 3", "计算 1 加上上一步的乘积", "从上一步的结果中减去 10", "得到最终计算结果"]
```
✅ 计划已生成:
```python
["先计算括号内的加法:15 + 7", "将括号内的结果乘以 3", "计算 1 加上上一步的乘积", "从上一步的结果中减去 10", "得到最终计算结果"]
```
--- 正在执行计划 ---
-> 正在执行步骤 1/5:先计算括号内的加法:15 + 7
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
22
✅ 步骤 1 已完成,结果:
22
-> 正在执行步骤 2/5:将括号内的结果乘以 3
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
66
✅ 步骤 2 已完成,结果:
66
-> 正在执行步骤 3/5:计算 1 加上上一步的乘积
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
67
✅ 步骤 3 已完成,结果:
67
-> 正在执行步骤 4/5:从上一步的结果中减去 10
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
57
✅ 步骤 4 已完成,结果:
57
-> 正在执行步骤 5/5:得到最终计算结果
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
57
✅ 步骤 5 已完成,结果:
57
--- 任务完成 ---
最终答案:
57
如果模型执行到一半发现不符合后续的plan了,怎么办?
- Replan(重新规划):发现偏差后,立即暂停执行,基于当前实际状态重新生成剩余计划。
- 可以重新规划后续所有,也可以只重新规划当前step。
- Plan-Act-Observe-Replan(ReAct 的变体):执行前确认计划是否还适用。
- 冗余路径 + 运行时选择:规划时生成多个备选动作,执行时根据实际情况选择。
- 同时,还需注意计划不需要太细(比如"准备报告"而不是"打开Excel→输入A1→...")。
如何让 Agent "知道" 计划失效了?需要检测机制,常见方法:
| 检测方法 | 说明 | 适用场景 |
|---|---|---|
| 结果验证 | 用 LLM 比较预期 vs 实际输出 | 语义任务 |
| 数值阈值 | 关键指标超出范围 | 量化任务 |
| 时间超时 | 超过预期时间 | 所有任务 |
| 异常捕获 | try-catch 执行错误 | 代码/API调用 |
| 人工确认 | 主动询问用户"还是按原计划吗?" | 高价值任务 |
如何让智能体在执行前就能预料到大概结果?
这核心涉及 Agent 的规划与模拟能力,而不是单纯的“执行-观察”循环。通常有以下几种方法:
- 基于模型的推理 + 规划:不直接执行动作,而是对候选动作序列做 rollout(蒙特卡洛树搜索 MCTS 或 beam search),并根据预期累计收益选择最高分的动作。
- 先验知识或经验回放:执行后在记忆库中存储,以便下次参考历史结果。
- 在 prompt 中强制要求 “先推演,再执行”。
- 主动请求人类或更多信息。
- 世界模型(World Model,不是指 LLM 本身的推理结果,而是指一个独立的环境动力学模型,它回答的是:"如果我做 A,环境会变成什么样?" 而不是 "A 是好是坏")。
- 让 LLM 直接预测执行动作后的状态变化,不需要训练,但LLM 对物理/时空推理不可靠。
- 训练一个小型神经网络(或决策树)作为世界模型,执行前用它快速推演。
2.1.3.Reflection
在已经实现的 ReAct 和 Plan-and-Solve 范式中,智能体一旦完成了任务,其工作流程便告结束。然而,它们生成的初始答案,无论是行动轨迹还是最终结果,都可能存在谬误或有待改进之处。Reflection 机制的核心思想,正是为智能体引入一种事后(post-hoc)的自我校正循环,使其能够像人类一样,审视自己的工作,发现不足,并进行迭代优化。
Reflection核心思想
其核心工作流程可以概括为一个简洁的三步循环:执行 -> 反思 -> 优化:
- 执行 (Execution):首先,智能体使用我们熟悉的方法(如 ReAct 或 Plan-and-Solve)尝试完成任务,生成一个初步的解决方案或行动轨迹。这可以看作是“初稿”。
- 反思 (Reflection):接着,智能体进入反思阶段。它会调用一个独立的、或者带有特殊提示词的大语言模型实例,来扮演一个“评审员”的角色。这个“评审员”会审视第一步生成的“初稿”,并从多个维度进行评估,例如:
- 事实性错误:是否存在与常识或已知事实相悖的内容?
- 逻辑漏洞:推理过程是否存在不连贯或矛盾之处?
- 效率问题:是否有更直接、更简洁的路径来完成任务?
- 遗漏信息:是否忽略了问题的某些关键约束或方面? 根据评估,它会生成一段结构化的反馈 (Feedback),指出具体的问题所在和改进建议。
- 优化 (Refinement):最后,智能体将“初稿”和“反馈”作为新的上下文,再次调用大语言模型,要求它根据反馈内容对初稿进行修正,生成一个更完善的“修订稿”。

这个循环可以重复进行多次,直到反思阶段不再发现新的问题,或者达到预设的迭代次数上限。
与前两种范式相比,Reflection 的价值在于:
- 它为智能体提供了一个内部纠错回路,使其不再完全依赖于外部工具的反馈(ReAct 的 Observation),从而能够修正更高层次的逻辑和策略错误。
- 它将一次性的任务执行,转变为一个持续优化的过程,显著提升了复杂任务的最终成功率和答案质量。
- 它为智能体构建了一个临时的“短期记忆”。整个“执行-反思-优化”的轨迹形成了一个宝贵的经验记录,智能体不仅知道最终答案,还记得自己是如何从有缺陷的初稿迭代到最终版本的。更进一步,这个记忆系统还可以是多模态的,允许智能体反思和修正文本以外的输出(如代码、图像等),为构建更强大的多模态智能体奠定了基础。
Demo:一个具有短期记忆模块的Reflection智能体
因为reflection通常对应着信息的存储和提取,如果上下文足够长的情况,想让“评审员”直接获取所有的信息然后进行反思往往会传入很多冗余信息。
目标任务是:“编写一个Python函数,找出1到n之间所有的素数 (prime numbers)。” 这个任务是检验 Reflection 机制的绝佳场景:
- 存在明确的优化路径:大语言模型初次生成的代码很可能是一个简单但效率低下的递归实现。
- 反思点清晰:可以通过反思发现其“时间复杂度过高”或“存在重复计算”的问题。
- 优化方向明确:可以根据反馈,将其优化为更高效的迭代版本或使用备忘录模式的版本。
Reflection 的核心在于迭代,而迭代的前提是能够记住之前的尝试和获得的反馈。因此,一个“短期记忆”模块是实现该范式的必需品。这个记忆模块将负责存储每一次“执行-反思”循环的完整轨迹。
from typing import List, Dict, Any, Optional
class Memory:
"""
一个简单的短期记忆模块,用于存储智能体的行动与反思轨迹。
"""
def __init__(self):
"""
初始化一个空列表来存储所有记录。
"""
self.records: List[Dict[str, Any]] = []
def add_record(self, record_type: str, content: str):
"""
向记忆中添加一条新记录。
参数:
- record_type (str): 记录的类型 ('execution' 或 'reflection')。
- content (str): 记录的具体内容 (例如,生成的代码或反思的反馈)。
"""
record = {"type": record_type, "content": content}
self.records.append(record)
print(f"📝 记忆已更新,新增一条 '{record_type}' 记录。")
def get_trajectory(self) -> str:
"""
将所有记忆记录格式化为一个连贯的字符串文本,用于构建提示词。
"""
trajectory_parts = []
for record in self.records:
if record['type'] == 'execution':
trajectory_parts.append(f"--- 上一轮尝试 (代码) ---\n{record['content']}")
elif record['type'] == 'reflection':
trajectory_parts.append(f"--- 评审员反馈 ---\n{record['content']}")
return "\n\n".join(trajectory_parts)
def get_last_execution(self) -> Optional[str]:
"""
获取最近一次的执行结果以供反思 (例如,最新生成的代码)。
如果不存在,则返回 None。
"""
for record in reversed(self.records):
if record['type'] == 'execution':
return record['content']
return None
在改demo中,整个智能体的工作流程将围绕我们之前讨论的“执行-反思-优化”循环展开,并通过精心设计的提示词来引导大语言模型扮演不同的角色。与之前的范式不同,Reflection 机制需要多个不同角色的提示词来协同工作。
- 初始执行提示词 (Execution Prompt) :这是智能体首次尝试解决问题的提示词,内容相对直接,只要求模型完成指定任务。
- 反思提示词 (Reflection Prompt) :这个提示词是 Reflection 机制的灵魂。它指示模型扮演“代码评审员”的角色,对上一轮生成的代码进行批判性分析,并提供具体的、可操作的反馈。
- 优化提示词 (Refinement Prompt) :当收到反馈后,这个提示词将引导模型根据反馈内容,对原有代码进行修正和优化。
import os
from typing import List, Dict, Any, Optional
from dotenv import load_dotenv
from openai import OpenAI
# 加载环境变量
load_dotenv()
class Memory:
"""
一个简单的短期记忆模块,用于存储智能体的行动与反思轨迹。
"""
def __init__(self):
"""
初始化一个空列表来存储所有记录。
"""
self.records: List[Dict[str, Any]] = []
def add_record(self, record_type: str, content: str):
"""
向记忆中添加一条新记录。
参数:
- record_type (str): 记录的类型 ('execution' 或 'reflection')。
- content (str): 记录的具体内容 (例如,生成的代码或反思的反馈)。
"""
record = {"type": record_type, "content": content}
self.records.append(record)
print(f"📝 记忆已更新,新增一条 '{record_type}' 记录。")
def get_trajectory(self) -> str:
"""
将所有记忆记录格式化为一个连贯的字符串文本,用于构建提示词。
"""
trajectory_parts = []
for record in self.records:
if record['type'] == 'execution':
trajectory_parts.append(f"--- 上一轮尝试 (代码) ---\n{record['content']}")
elif record['type'] == 'reflection':
trajectory_parts.append(f"--- 评审员反馈 ---\n{record['content']}")
return "\n\n".join(trajectory_parts)
def get_last_execution(self) -> Optional[str]:
"""
获取最近一次的执行结果 (例如,最新生成的代码)。
如果不存在,则返回 None。
"""
for record in reversed(self.records):
if record['type'] == 'execution':
return record['content']
return None
class HelloAgentsLLM:
"""
作用:封装大语言模型(LLM)客户端
功能:
- 初始化OpenAI兼容的API连接
- 提供 think() 方法调用模型进行思考(流式响应)
"""
def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
"""
初始化LLM客户端
优先使用传入参数,否则从环境变量读取
"""
self.model = model or os.getenv("LLM_MODEL_ID")
apiKey = apiKey or os.getenv("LLM_API_KEY")
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
# 验证必要参数
if not all([self.model, apiKey, baseUrl]):
raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
# 创建OpenAI客户端
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
"""
调用LLM进行思考
参数:
- messages: 对话消息列表
- temperature: 温度参数(控制随机性,默认0表示确定性输出)
返回:模型响应的文本内容
"""
print(f"🧠 正在调用 {self.model} 模型...")
try:
# 发起流式请求
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
)
# 处理流式响应
print("✅ 大语言模型响应成功:")
collected_content = []
for chunk in response:
if not chunk.choices:
continue
content = chunk.choices[0].delta.content or ""
print(content, end="", flush=True) # 实时打印
collected_content.append(content)
print() # 换行
return "".join(collected_content)
except Exception as e:
print(f"❌ 调用LLM API时发生错误: {e}")
return None
# 提示词模板
INITIAL_PROMPT_TEMPLATE = """
你是一位资深的Python程序员。请根据以下要求,编写一个Python函数。
你的代码必须包含完整的函数签名、文档字符串,并遵循PEP 8编码规范。
要求: {task}
请直接输出代码,不要包含任何额外的解释。
"""
REFLECT_PROMPT_TEMPLATE = """
你是一位极其严格的代码评审专家和资深算法工程师,对代码的性能有极致的要求。
你的任务是审查以下Python代码,并专注于找出其在<strong>算法效率</strong>上的主要瓶颈。
# 原始任务:
{task}
# 待审查的代码:
```python
{code}
```
请分析该代码的时间复杂度,并思考是否存在一种<strong>算法上更优</strong>的解决方案来显著提升性能。
如果存在,请清晰地指出当前算法的不足,并提出具体的、可行的改进算法建议(例如,使用筛法替代试除法)。
如果代码在算法层面已经达到最优,才能回答“无需改进”。
请直接输出你的反馈,不要包含任何额外的解释。
"""
REFINE_PROMPT_TEMPLATE = """
你是一位资深的Python程序员。你正在根据一位代码评审专家的反馈来优化你的代码。
# 原始任务:
{task}
# 你上一轮尝试的代码:
{last_code_attempt}
评审员的反馈:
{feedback}
请根据评审员的反馈,生成一个优化后的新版本代码。
你的代码必须包含完整的函数签名、文档字符串,并遵循PEP 8编码规范。
请直接输出优化后的代码,不要包含任何额外的解释。
"""
class ReflectionAgent:
def __init__(self, llm_client, max_iterations=3):
self.llm_client = llm_client
self.memory = Memory()
self.max_iterations = max_iterations
def run(self, task: str):
print(f"\n--- 开始处理任务 ---\n任务: {task}")
# --- 1. 初始执行 ---
print("\n--- 正在进行初始尝试 ---")
initial_prompt = INITIAL_PROMPT_TEMPLATE.format(task=task)
initial_code = self._get_llm_response(initial_prompt)
self.memory.add_record("execution", initial_code)
# --- 2. 迭代循环:反思与优化 ---
for i in range(self.max_iterations):
print(f"\n--- 第 {i+1}/{self.max_iterations} 轮迭代 ---")
# a. 反思
print("\n-> 正在进行反思...")
last_code = self.memory.get_last_execution()
reflect_prompt = REFLECT_PROMPT_TEMPLATE.format(task=task, code=last_code)
feedback = self._get_llm_response(reflect_prompt)
self.memory.add_record("reflection", feedback)
# b. 检查是否需要停止
if "无需改进" in feedback:
print("\n✅ 反思认为代码已无需改进,任务完成。")
break
# c. 优化
print("\n-> 正在进行优化...")
refine_prompt = REFINE_PROMPT_TEMPLATE.format(
task=task,
last_code_attempt=last_code,
feedback=feedback
)
refined_code = self._get_llm_response(refine_prompt)
self.memory.add_record("execution", refined_code)
final_code = self.memory.get_last_execution()
print(f"\n--- 任务完成 ---\n最终生成的代码:\n```python\n{final_code}\n```")
return final_code
def _get_llm_response(self, prompt: str) -> str:
"""一个辅助方法,用于调用LLM并获取完整的流式响应。"""
messages = [{"role": "user", "content": prompt}]
response_text = self.llm_client.think(messages=messages) or ""
return response_text
def simple_main():
"""
简化版主函数,适合快速测试
"""
# 初始化LLM客户端
try:
llm_client = HelloAgentsLLM()
print("✅ LLM客户端初始化成功")
except ValueError as e:
print(f"❌ LLM客户端初始化失败: {e}")
print("请检查.env文件配置")
return
# 创建反思代理
agent = ReflectionAgent(llm_client, max_iterations=3)
# 定义任务
task = """
编写一个函数 find_primes(n),返回所有小于等于n的质数列表。
要求算法尽可能高效。
"""
# 运行代理
final_code = agent.run(task)
# # 保存代码
# if final_code:
# with open("generated_code.py", "w", encoding="utf-8") as f:
# f.write(final_code)
# print("\n📁 最终代码已保存到 generated_code.py")
# 选择使用哪个main函数
if __name__ == "__main__":
simple_main()
PS D:\Microsoft VS Code\vs work\codeworkvs\other\Agent\reflectiondemo> python .\demo.py
✅ LLM客户端初始化成功
--- 开始处理任务 ---
任务:
编写一个函数 find_primes(n),返回所有小于等于n的质数列表。
要求算法尽可能高效。
--- 正在进行初始尝试 ---
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
from math import isqrt
def find_primes(n: int) -> list[int]:
"""返回所有小于等于 n 的质数列表。"""
if n < 2:
return []
sieve = bytearray(b"\x01") * (n + 1)
sieve[0:2] = b"\x00\x00"
for number in range(2, isqrt(n) + 1):
if sieve[number]:
start = number * number
count = (n - start) // number + 1
sieve[start:n + 1:number] = b"\x00" * count
return [number for number, is_prime in enumerate(sieve) if is_prime]
📝 记忆已更新,新增一条 'execution' 记录。
--- 第 1/3 轮迭代 ---
-> 正在进行反思...
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
代码当前已经使用埃氏筛,不是试除法。
结论:算法层面无需替换为其他筛法;当前主算法已是实践中非常高效的方案。
关键评审反馈:
1. 时间复杂度
```text
O(n log log n)
```
原因是埃氏筛对每个质数标记其倍数,总标记次数约为:
```text
n/2 + n/3 + n/5 + ... = O(n log log n)
```
最终列表生成:
```python
[number for number, is_prime in enumerate(sieve) if is_prime]
```
需要完整扫描 `sieve`,复杂度为:
```text
O(n)
```
因此总复杂度仍为:
```text
O(n log log n)
```
2. 空间复杂度
```text
O(n)
```
`bytearray` 长度为 `n + 1`。
3. 当前主要瓶颈
主要瓶颈在于:
```python
sieve[start:n + 1:number] = b"\x00" * count
```
这是埃氏筛的核心标记操作,但该写法已经利用了 `bytearray` 切片赋值,底层由 C 层执行,比 Python 层循环逐个标记高效得多。
4. 是否存在算法上显著更优的方案
不存在值得替换的显著更优算法。
线性筛理论复杂度是:
```text
O(n)
```
但在 Python 中通常需要大量 Python 层循环和列表操作,实际性能往往不如当前这种基于 `bytearray` 切片的埃氏筛。
阿特金筛理论上也可能更优,但实现复杂、常数大,在 Python 中通常也不如当前方案稳定高效。
5. 可做的改进
只能做常数级优化,不是算法复杂度级别提升:
- 只筛奇数,减少约一半内存和标记量;
- 对超大 `n` 使用分段筛,降低内存占用并改善缓存局部性;
- 当前实现本身已经是高质量埃氏筛实现。
最终结论:
```text
无需进行算法层面的改进。
当前代码已经采用了适合该任务的高效筛法。
```
📝 记忆已更新,新增一条 'reflection' 记录。
-> 正在进行优化...
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
from math import isqrt
def find_primes(n: int) -> list[int]:
"""Return all prime numbers less than or equal to n."""
if n < 2:
return []
if n == 2:
return [2]
size = (n + 1) // 2
sieve = bytearray(b"\x01") * size
sieve[0] = 0
for number in range(3, isqrt(n) + 1, 2):
index = number // 2
if sieve[index]:
start = number * number // 2
count = (size - 1 - start) // number + 1
sieve[start::number] = b"\x00" * count
return [2] + [
2 * index + 1
for index in range(1, size)
if sieve[index]
]
📝 记忆已更新,新增一条 'execution' 记录。
--- 第 2/3 轮迭代 ---
-> 正在进行反思...
🧠 正在调用 gpt-5.5 模型...
✅ 大语言模型响应成功:
时间复杂度:`O(n log log n)`
空间复杂度:`O(n)`
该代码使用的是**只筛奇数的埃拉托色尼筛法**,不是低效的试除法。核心标记逻辑:
```python
sieve[start::number] = b"\x00" * count
```
整体仍然是标准埃氏筛的复杂度,只是通过跳过偶数将常数约减半。
主要算法瓶颈:
1. 仍然需要对每个质数的倍数进行批量标记,总标记规模为 `O(n log log n)`。
2. 需要维护长度约为 `n / 2` 的筛数组,空间为 `O(n)`。
3. 最后构造结果列表需要线性扫描筛数组,额外 `O(n)`。
是否存在算法上显著更优的方案:
- 对于“返回所有 `<= n` 的质数”这一任务,该实现已经是非常接近最优的通用方案。
- 理论上可以使用**线性筛**将复杂度降到 `O(n)`,但在 Python 中线性筛需要大量 Python 层循环和列表操作,实际性能通常不如当前这种基于 `bytearray` 切片批量赋值的埃氏筛。
- 分段筛可以降低内存占用并改善超大 `n` 时的缓存局部性,但时间复杂度仍然是 `O(n log log n)`,不是算法阶数上的显著提升。
- Atkin 筛理论复杂度更优,但常数大、实现复杂,在 Python 中通常也不如当前实现稳定高效。
结论:
当前代码在算法层面已经使用了高效筛法,并且做了跳过偶数的优化。不存在明显的、在 Python 中能显著提升性能的更优算法替代方案。
无需改进。
📝 记忆已更新,新增一条 'reflection' 记录。
✅ 反思认为代码已无需改进,任务完成。
--- 任务完成 ---
最终生成的代码:
```python
from math import isqrt
def find_primes(n: int) -> list[int]:
"""Return all prime numbers less than or equal to n."""
if n < 2:
return []
if n == 2:
return [2]
size = (n + 1) // 2
sieve = bytearray(b"\x01") * size
sieve[0] = 0
for number in range(3, isqrt(n) + 1, 2):
index = number // 2
if sieve[index]:
start = number * number // 2
count = (size - 1 - start) // number + 1
sieve[start::number] = b"\x00" * count
return [2] + [
2 * index + 1
for index in range(1, size)
if sieve[index]
]
```
Reflection机制的成本收益分析
(1)主要成本
- 模型调用开销增加:这是最直接的成本。每进行一轮迭代,至少需要额外调用两次大语言模型(一次用于反思,一次用于优化)。如果迭代多轮,API 调用成本和计算资源消耗将成倍增加。
- 任务延迟显著提高:Reflection 是一个串行过程,每一轮的优化都必须等待上一轮的反思完成。这使得任务的总耗时显著延长,不适合对实时性要求高的场景。
- 提示工程复杂度上升:如Demo我们的案例所示,Reflection 的成功在很大程度上依赖于高质量、有针对性的提示词。为“执行”、“反思”、“优化”等不同阶段设计和调试有效的提示词,需要投入更多的开发精力。
(2)核心收益
- 解决方案质量的跃迁:最大的收益在于,它能将一个“合格”的初始方案,迭代优化成一个“优秀”的最终方案。这种从功能正确到性能高效、从逻辑粗糙到逻辑严谨的提升,在很多关键任务中是至关重要的。
- 鲁棒性与可靠性增强:通过内部的自我纠错循环,智能体能够发现并修复初始方案中可能存在的逻辑漏洞、事实性错误或边界情况处理不当等问题,从而大大提高了最终结果的可靠性。
综上所述,Reflection 机制是一种典型的“以成本换质量”的策略。它非常适合那些对最终结果的质量、准确性和可靠性有极高要求,且对任务完成的实时性要求相对宽松的场景。反之,如果应用场景需要快速响应,或者一个“大致正确”的答案就已经足够,那么使用更轻量的 ReAct 或 Plan-and-Solve 范式可能会是更具性价比的选择。
2.2.基于低代码平台的Agent搭建
2.2.1.什么是低代码平台
对于一个快速发展的领域而言,纯代码的开发模式并非总是最高效的选择,尤其是在需要快速验证想法、或者非专业开发者希望参与构建的场景中。在追求工程效率和创新的实战中,我们往往需要站在巨人的肩膀上。
其核心价值主要体现在以下几个方面:
- 降低技术门槛:低代码平台将复杂的技术细节(如 API 调用、状态管理、并发控制)封装成一个个易于理解的“节点”或“模块”。用户无需精通编程,只需通过拖拽、连接这些节点,就能构建出功能强大的工作流。这使得产品经理、设计师、业务专家等非技术人员也能参与到智能体的设计与创造中来,极大地拓宽了创新的边界。
- 提升开发效率:对于专业开发者而言,平台同样能带来巨大的效率提升。在项目初期,当需要快速验证一个想法或搭建一个原型 (Prototype) 时,使用低代码平台可以在数小时甚至数分钟内完成原本需要数天编码的工作。开发者可以将精力更多地投入到业务逻辑梳理和提示工程优化上,而非底层的工程实现。
- 提供更优的可视化与可观测性:相比于在终端中打印日志,图形化的平台天然提供了对智能体运行轨迹的端到端可视化。你可以清晰地看到数据在每一个节点之间如何流动,哪一个环节耗时最长,哪一个工具调用失败。这种直观的调试体验,是纯代码开发难以比拟的。
- 标准化与最佳实践沉淀:优秀的低代码平台通常会内置许多行业内的最佳实践。例如,它会提供预设的 ReAct 模板、优化的知识库检索引擎、标准化的工具接入规范等。这不仅避免了开发者“踩坑”,也使得团队协作更加顺畅,因为所有人都基于同一套标准和组件进行开发。
简而言之,低代码平台并非要取代代码,而是提供了一种更高层次的抽象。它让我们可以从繁琐的底层实现中解放出来,更专注于智能体“思考”与“行动”的逻辑本身,从而更快、更好地将创意变为现实。
Hello-Agent教程中主要介绍四个低代码平台:
- Coze
- 核心定位:由字节跳动推出,主打零代码/低代码的 Agent 的构建体验,让不具备编程背景的用户也能轻松创造。
- 特点分析:Coze 拥有极其友好的可视化界面,用户可以像搭建乐高积木一样,通过拖拽插件、配置知识库和设定工作流来创建智能体。其内置了极为丰富的插件库,并支持一键发布到抖音、飞书、微信公众号等多个主流平台,极大地简化了分发流程。
- 适用人群:AI 应用的入门用户、产品经理、运营人员,以及希望快速将创意变为可交互产品的个人创作者。
- Dify
- 核心定位:Dify 是一个开源的、功能全面的 LLM 应用开发与运营平台,旨在为开发者提供从原型构建到生产部署的一站式解决方案。
- 特点分析:它融合了后端服务和模型运营的理念,支持 Agent 工作流、RAG Pipeline、数据标注与微调等多种能力。对于追求专业、稳定、可扩展的企业级应用而言,Dify 提供了坚实的基础。
- 适用人群:有一定技术背景的开发者、需要构建可扩展的企业级 AI 应用的团队。
- FastGPT
- 核心定位:FastGPT 是一个开源的、基于 LLM 大语言模型的知识库问答平台与 Agent 构建工具,专注于提供简单易用的 RAG(检索增强生成)解决方案和可视化工作流编排能力。
- 特点分析:FastGPT 最核心的优势在于其对知识库问答场景的极致优化。它提供了从数据导入、自动文本分块、向量化存储到智能检索的完整 RAG 链路,并支持通过直观的可视化界面(Flow 模块)编排复杂的对话流程和 Agent 工作流。平台采用模型中立设计,可灵活对接 OpenAI、Claude、通义千问等多种国内外主流大模型,同时提供了完善的 API 接口和插件市场,便于与企业微信、钉钉、飞书等现有系统快速集成。
- 适用人群:希望基于私有知识库快速搭建智能客服、企业内部知识助手、文档问答机器人的开发者和中小企业团队,以及对 RAG 技术感兴趣但希望降低实现门槛的技术爱好者。
- n8n
- 核心定位:n8n 本质上是一个开源工作流自动化工具,而非纯粹的 LLM 平台。近年来,它积极集成了 AI 能力。
- 特点分析:n8n 的强项在于“连接”。它拥有数百个预置的节点,可以轻松地将各类 SaaS 服务、数据库、API 连接成复杂的自动化业务流程。你可以在这个流程中嵌入 LLM 节点,使其成为整个自动化链路中的一环。虽然在 LLM 功能的专一度上不如前两者,但其通用自动化能力是独一无二的。不过,其学习曲线也相对陡峭。
- 适用人群:需要将 AI 能力深度整合进现有业务流程、实现高度定制化自动化的开发者和企业。
Workflow和Chatflow
个人感觉后者就是在前者的自动化编排的基础上用到了LLM处理模糊语义和对话的能力。
2.2.2.Coze(扣子)
coze编程官网;coze官网;coze agent在coze编程-智能体开发中。
Demo:“每日新闻”助手
- 智能体开发-创建项目-Agent
- 添加工作流
- 建立工作流之间的数据流
- 给大模型系统提示和用户提示
- 运行工作流
- 仔细检查简报的内容准确性、格式完整性以及语言风格。如果发现不符合预期的部分,需返回提示词或插件配置环节进行细致调整。例如,若内容不够精炼,可修改提示词中的概括要求;若数据获取不准确,则需检查插件配置参数。

工作流发布后就可以作为一个“工具”插入到Agent项目的在线聊天中。
个人感觉ReAct可以编排如下:

但是他不支持MCP(多智能体协作),对于复杂和高要求Agent的编排也同样比较吃力。
2.2.3.Dify
Dify 是一个开源的大语言模型(LLM)应用开发平台,融合了后端即服务(BaaS) 和 LLMOps 理念,为从原型设计到生产部署提供全流程支持。它采用分层模块化架构,分为数据层、开发层、编排层和基础层,各层解耦便于扩展。Dify官网。
Demo:超级个人助手
搭建过程在教程中已经很详细清晰。
Dify里的这个问题分类器感觉挺有意思,而且没在Coze中找到相同作用的插件。(修改,coze中也有“意图识别”插件)

2.2.4.FastGPT
FastGPT官网。FastGPT 的核心开发范式:知识库构建、MCP 工具接入、可视化工作流编排和多轮对话交互设计。
FastGPT 最核心的竞争力在于其强大的知识库能力。平台支持多种文件格式的导入,包括 Word、Markdown、PDF 等常见文档类型。系统会自动对文件进行分块处理并建立索引,状态显示为"已就绪"后即可在对话中被检索引用。
在文件处理层面,FastGPT 提供了精细化的参数配置。用户可以选择"分块存储"或"问答对提取"两种处理方式,设置分块条件(如原文长度大于 1000 字符时触发分块),并开启多种索引增强选项,包括将标题加入索引、自动生成补充索引以及图片自动索引等。对于包含大量图文混排内容的文档(如教材、研报),图片自动索引功能尤为重要,它能让大模型在回答时理解并引用文档中的视觉信息。
上传完成后,用户可以查看文件被分块后的具体内容。平台展示了每个分块的文本预览,同时右侧元数据面板显示了文件大小、原文长度、处理模式(分块存储)、图片索引状态等关键信息。这种透明化的分块展示,方便开发者进行知识库的调试与优化。
除了知识库,FastGPT 在工具集成方面也紧跟生态趋势。平台原生支持 MCP(Model Context Protocol)工具,用户可以在"我的工具"模块中统一管理各类 MCP 服务。这使得智能体的工具扩展能力不再受限于平台内置的插件库,开发者可以自由接入任何符合 MCP 标准的第三方工具。
Demo:智能投顾助手
教程链接。魔搭社区(ModelScope)的 MCP 市场和阿里云百炼平台都提供了丰富的官方 MCP 服务,在 FastGPT 的 MCP 工具配置界面中,填写相应的服务地址、认证信息后,即可完成工具的接入。每个 MCP 工具都可以设置独立的描述和调用参数,便于智能体在决策时理解各工具的用途。
2.3.智能体框架
2.3.1.智能体框架引言
一个框架的本质,是提供一套经过验证的“规范”。它将所有智能体共有的、重复性的工作(如主循环、状态管理、工具调用、日志记录等)进行抽象和封装,让我们在构建新的智能体时,能够专注于其独特的业务逻辑,而非通用的底层实现。(个人理解就像pytorch库分装了比如交叉熵损失函数,直接用nn库即可,不用在实际过程中手撕代码)
相比于直接编写独立的智能体脚本,使用框架的价值主要体现在以下几个方面:
- 提升代码复用与开发效率:这是最直接的价值。一个好的框架会提供一个通用的
Agent基类或执行器,它封装了智能体运行的核心循环(Agent Loop)。无论是 ReAct 还是 Plan-and-Solve,都可以基于框架提供的标准组件快速搭建,从而避免重复劳动。 - 实现核心组件的解耦与可扩展性:一个健壮的智能体系统应该由多个松散耦合的模块组成。框架的设计会强制我们分离不同的关注点:
- 模型层 (Model Layer):负责与大语言模型交互,可以轻松替换不同的模型(OpenAI, Anthropic, 本地模型)。
- 工具层 (Tool Layer):提供标准化的工具定义、注册和执行接口,添加新工具不会影响其他代码。
- 记忆层 (Memory Layer):处理短期和长期记忆,可以根据需求切换不同的记忆策略(如滑动窗口、摘要记忆)。 这种模块化的设计使得整个系统极具可扩展性,更换或升级任何一个组件都变得简单。
- 标准化复杂的状态管理:我们在
ReflectionAgent中实现的Memory类只是一个简单的开始。在真实的、长时运行的智能体应用中,状态管理是一个巨大的挑战,它需要处理上下文窗口限制、历史信息持久化、多轮对话状态跟踪等问题。一个框架可以提供一套强大而通用的状态管理机制,开发者无需每次都重新处理这些复杂问题。 - 简化可观测性与调试过程:当智能体的行为变得复杂时,理解其决策过程变得至关重要。一个精心设计的框架可以内置强大的可观测性能力。例如,通过引入事件回调机制(Callbacks),我们可以在智能体生命周期的关键节点(如
on_llm_start,on_tool_end,on_agent_finish)自动触发日志记录或数据上报,从而轻松地追踪和调试智能体的完整运行轨迹。这远比在代码中手动添加print语句要高效和系统化。
如果说 LangChain 和 LlamaIndex 定义了第一代通用 LLM 应用框架的范式,那么新一代的框架则更加专注于解决特定领域的深层挑战,尤其是多智能体协作 (Multi-Agent Collaboration) 和 复杂工作流控制 (Complex Workflow Control)。
Hello-Agent教程主要介绍了以下表格中的四种框架,我自己简单补充了LangChain和LlamaIndex的基础相关知识。LangChain 强调标准化组件与 Agent 工具调用;LlamaIndex 更偏向“围绕私有数据构建 Agent”,其 RAG、索引、检索器和查询引擎生态更完整。
2.3.2.LangChain
LangChain 是一个面向大语言模型应用开发的通用框架。与 AutoGen、CAMEL 等强调多智能体对话的框架不同,LangChain 更注重对模型、提示词、工具、记忆和工作流等组件进行统一封装。开发者可以像搭积木一样组合这些组件,快速构建问答助手、RAG 系统和工具调用 Agent。
LangChain核心机制
LangChain 的 Agent 可以理解为一个由大语言模型驱动的循环决策系统。用户提出问题后,模型会结合用户需求和可用工具,判断是否需要调用外部工具。如果需要,框架会执行工具,并将执行结果返回给模型。模型读取工具结果后,再决定继续调用其他工具,还是生成最终回答。
其基本流程如下:
- 用户向 Agent 输入任务。
- 大语言模型分析任务,并判断是否需要调用工具。
- 如果需要调用工具,模型生成工具名称和参数。
- LangChain 执行对应工具,并将结果返回给模型。
- 模型基于工具结果生成最终回答,或继续调用其他工具。
LangChain 当前提供了 create_agent() 方法,可以快速创建一个具备工具调用能力的 Agent。开发者只需要配置模型、工具列表和系统提示词,不需要手动编写 Agent 循环。
框架体现在?
create_agent() 方法本质上已经使用了 ReAct 风格的工具调用循环。模型会自行判断调用哪个工具、传入什么参数,以及是否还需要继续调用其他工具。严格来说,当前 create_agent() 不要求模型显式输出(Thought, Action, Observation),现代模型通常支持原生 Function Calling 或 Tool Calling,因此模型可以直接生成结构化的工具调用请求。LangChain暂时没有Plan-and-Execute风格的Agent封装。
Demo:几何计算助手
核心函数:ChatOpenAI、create_agent
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain.messages import AIMessage, ToolMessage # 观察到模型决策的结果
# 使用 @tool 装饰器,将普通 Python 函数注册为 Agent 可以调用的工具。
# 函数名称、参数类型和文档字符串都会提供给大语言模型,
# 帮助模型理解工具的用途以及调用时需要传入哪些参数。
@tool
def calculate_rectangle_area(length: float, width: float) -> str:
"""
计算矩形面积。
Args:
length: 矩形的长度。
width: 矩形的宽度。
Returns:
计算结果。
"""
area = length * width
return f"矩形面积为 {area}"
@tool
def calculate_circle_area(radius: float) -> str:
"""
计算圆形面积。
Args:
radius: 圆形的半径。
Returns:
计算结果。
"""
pi = 3.1415926
area = pi * radius * radius
return f"圆形面积约为 {area:.2f}"
def create_model() -> ChatOpenAI:
"""
创建大语言模型客户端。
ChatOpenAI 不仅可以连接 OpenAI 官方模型,
也可以连接兼容 OpenAI Chat Completions 协议的模型服务。
本例使用阿里云百炼提供的兼容接口调用千问模型。
"""
api_key = os.getenv("LLM_API_KEY")
# 如果没有配置 API Key,则主动抛出异常,
# 避免程序在调用模型时才出现不容易定位的错误。
if not api_key:
raise RuntimeError("请先在 .env 文件中设置 API_KEY")
return ChatOpenAI(
# 可以通过环境变量更换模型。
# 如果没有配置,则默认使用 qwen-plus。
model=os.getenv("LLM_MODEL_ID", "qwen-plus"),
# 从环境变量读取 API Key,避免在源代码中硬编码密钥。
api_key=api_key, # type: ignore
# 阿里云百炼提供的 OpenAI 兼容接口地址。
base_url=os.getenv(
"LLM_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
),
# 温度设置为 0,使模型输出相对稳定。
temperature=0,
)
def build_agent():
"""
创建几何计算 Agent。
create_agent() 会自动管理工具调用循环:
1. 将用户问题发送给模型;
2. 让模型选择工具;
3. 执行工具;
4. 将工具结果返回给模型;
5. 输出最终回答。
"""
return create_agent(
model=create_model(),
# Agent 可以使用的工具列表。
tools=[
calculate_rectangle_area,
calculate_circle_area,
],
# 系统提示词用于约束 Agent 的行为。
# 明确要求调用工具,可以避免模型直接口算。
system_prompt=(
"你是一个几何计算助手。"
"遇到矩形面积或圆形面积问题时,必须调用合适的工具完成计算;"
"获得工具结果后,请用简洁的中文回答用户。"
),
)
def run_agent() -> None:
"""
程序入口。
"""
# 读取项目根目录下的 .env 文件。
load_dotenv()
# 创建 Agent。
agent = build_agent()
# invoke() 用于运行 Agent。
# messages 表示发送给 Agent 的对话消息。
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "一个长为 8、宽为 5 的矩形面积是多少?",
}
]
}
)
# create_agent() 会返回完整的消息历史。
# 最后一条消息就是 Agent 的最终回答。
print(result["messages"][-1].content)
def run_agent_with_trace(agent, question: str) -> None:
"""
运行 Agent,并实时打印中间执行轨迹。
stream_mode="updates" 表示:
每当 Agent 完成一个步骤,就返回一次状态更新。
对于一次普通的工具调用,通常会依次看到:
1. 模型决定调用工具;
2. 工具完成执行并返回结果;
3. 模型根据工具结果生成最终回答。
"""
print("\n========== 用户问题 ==========")
print(question)
# 与 invoke() 不同,stream() 不会等到全部步骤结束后才返回结果。
# 它会在 Agent 运行过程中逐步返回中间状态。
for chunk in agent.stream( # 每当 Agent 完成一个步骤,就取出一次更新,并将这次更新保存到变量 chunk 中。
{
"messages": [ # 向 Agent 发送用户消息
{
"role": "user",
"content": question,
}
]
},
# updates:每完成一个 Agent 步骤,就返回一次更新。
stream_mode="updates",
# v2:使用统一的数据格式。
version="v2",
):
print("\n========== 新的 chunk ==========")
print(chunk) # 打印原始更新内容,便于观察数据结构。
# 使用 v2 格式时,每个 chunk 都包含 type 和 data 字段。
if chunk["type"] != "updates":
continue
# 在这种格式下,每次返回的 chunk 通常具有类似结构:
# {
# "type": "updates",
# "data": {
# "model": {
# "messages": [...]
# },
# "tools": {
# "messages": [...]
# }
# }
# }
# 一次更新中可能包含一个或多个节点的执行结果。
for step_name, step_data in chunk["data"].items():
print(f"当前节点:{step_name}")
latest_message = step_data["messages"][-1]
# AIMessage 表示模型生成的消息。
if isinstance(latest_message, AIMessage):
# 如果 tool_calls 不为空,说明模型决定调用工具。
if latest_message.tool_calls:
print("\n========== 模型决策 ==========")
for tool_call in latest_message.tool_calls:
print(f"调用工具:{tool_call['name']}")
print(f"工具参数:{tool_call['args']}")
# 如果没有工具调用但存在文本,则通常是最终回答。
elif latest_message.content:
print("\n========== 最终回答 ==========")
print(latest_message.content)
# ToolMessage 表示工具执行完成后返回给模型的结果。
elif isinstance(latest_message, ToolMessage):
print("\n========== 工具执行结果 ==========")
print(f"工具名称:{latest_message.name}")
print(f"返回内容:{latest_message.content}")
if __name__ == "__main__":
run_agent()
print("\n\n========== 带执行轨迹的演示 ==========")
run_agent_with_trace(build_agent(), "一个半径为 3 的圆形面积是多少?")
PS D:\Microsoft VS Code\vs work\codeworkvs\py\agent\demo> python .\LangChaindemo.py
========== 直接执行的演示 ==========
========== 用户问题 ==========
一个长为 8、宽为 5 的矩形面积是多少?
矩形面积是 40。
========== 带执行轨迹的演示 ==========
========== 用户问题 ==========
一个半径为 3 的圆形面积是多少?
========== 新的 chunk ==========
{'type': 'updates', 'ns': (), 'data': {'model': {'messages': [AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 255, 'total_tokens': 274, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-5.5-2026-04-24', 'system_fingerprint': None, 'id': 'chatcmpl-DoVOPq0X5MK0cWD6Lc3fV19vkdXS5', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019ea7aa-d2db-7132-9e56-83ce51045e5e-0', tool_calls=[{'name': 'calculate_circle_area', 'args': {'radius': 3}, 'id': 'call_HeeE0R8S5Zw0hQ3mkYOTM4Bx', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 255, 'output_tokens': 19, 'total_tokens': 274, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}}
当前节点:model
========== 模型决策 ==========
调用工具:calculate_circle_area
工具参数:{'radius': 3}
========== 新的 chunk ==========
{'type': 'updates', 'ns': (), 'data': {'tools': {'messages': [ToolMessage(content='圆形面积约为 28.27', name='calculate_circle_area', id='d000d484-1567-4cdd-a21b-ac4d502acc65', tool_call_id='call_HeeE0R8S5Zw0hQ3mkYOTM4Bx')]}}}
当前节点:tools
========== 工具执行结果 ==========
工具名称:calculate_circle_area
返回内容:圆形面积约为 28.27
========== 新的 chunk ==========
{'type': 'updates', 'ns': (), 'data': {'model': {'messages': [AIMessage(content='半径为 3 的圆形面积约为 28.27。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 294, 'total_tokens': 314, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-5.5-2026-04-24', 'system_fingerprint': None, 'id': 'chatcmpl-DoVOQJhMxZIoGT9vRvawXvzvLT47W', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019ea7aa-d909-79b1-a909-7b8aa27c2163-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 294, 'output_tokens': 20, 'total_tokens': 314, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}}
当前节点:model
========== 最终回答 ==========
半径为 3 的圆形面积约为 28.27。
LangChain 的优势在于生态完整、组件丰富,并且对模型调用、工具调用、提示词管理和工作流编排提供了统一接口。对于需要快速开发原型的应用,开发者可以直接复用框架提供的组件,不必重复编写底层逻辑。
与此同时,LangChain 的模块数量较多,更新速度较快。对于初学者而言,如果一开始就使用过多高级组件,可能会增加学习成本。因此,在学习 LangChain 时,建议先掌握模型调用、提示词、工具调用和 Agent,再逐步学习 RAG、记忆管理和 LangGraph 等扩展能力。
2.3.3.LlamaIndex
LlamaIndex 是一个面向数据增强型大语言模型应用的开发框架。它最初以 RAG 为核心,重点解决文档加载、文本切分、索引构建、向量检索和问答生成等问题。随着框架不断发展,LlamaIndex 也加入了工具调用、Agent 和 Workflow 等能力。
与 LangChain 相比,LlamaIndex 更关注如何让大语言模型安全、高效地访问私有数据。它可以将本地文档、数据库、API 和向量数据库封装为可查询的数据源,也可以进一步将查询引擎封装为 Agent 工具。
LlamaIndex核心机制
在传统的 RAG 应用中,LlamaIndex 通常包含以下几个步骤:
- 加载文档或其他数据源。
- 将文档切分为较小的文本片段。
- 将文本片段转换为向量,并建立索引。
- 根据用户问题检索相关内容。
- 将检索结果和用户问题一起发送给大语言模型。
- 由模型生成最终回答。
在 Agent 场景中,LlamaIndex 会进一步让模型自主决定是否调用工具。工具既可以是普通 Python 函数,也可以是一个查询引擎。例如,一个 Agent 可以先调用知识库查询引擎检索企业文档,再调用计算工具完成数据分析。
LlamaIndex 提供了 FunctionAgent,适合使用原生 Function Calling 能力较强的模型。开发者只需要提供模型、工具列表和系统提示词,框架就会自动执行工具调用循环。
Demo:异步物流查询助手
Agent 的运行过程通常不仅仅是一次本地计算。它可能需要:
调用大模型 API
→ 等待网络响应
→ 判断是否调用工具
→ 调用工具
→ 等待工具返回结果
→ 再次调用大模型 API
→ 生成最终回答
其中,大量时间消耗在等待外部响应上。在等待过程中,Python 不一定需要一直阻塞整个程序。异步机制允许程序在等待网络响应时处理其他任务。
核心函数:OpenAILike、FunctionAgent
import asyncio
import os
import time
from dotenv import load_dotenv
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai_like import OpenAILike
async def query_delivery_status(order_id: str) -> str:
"""
查询订单的配送状态。
Args:
order_id: 需要查询的订单编号。
Returns:
订单当前的配送状态。
"""
print(f"[工具开始执行] 正在查询订单 {order_id} ...")
# 使用 asyncio.sleep() 模拟调用远程物流 API。
# await 表示当前工具暂时进入等待状态。
# 等待期间,事件循环可以调度其他任务继续执行。
await asyncio.sleep(2)
print(f"[工具执行完成] 已获得订单 {order_id} 的查询结果")
return f"订单 {order_id} 已到达配送站,预计今天送达。"
def create_model() -> OpenAILike:
"""
创建大语言模型客户端。
OpenAILike 是 LlamaIndex 提供的兼容层,
用于接入支持 OpenAI API 协议的第三方模型服务。
"""
api_key = os.getenv("LLM_API_KEY")
# 如果没有配置 API Key,则主动抛出异常,
# 避免程序在调用模型时才出现不容易定位的错误。
if not api_key:
raise RuntimeError("请先在 .env 文件中设置 API_KEY")
return OpenAILike(
# 可以通过环境变量更换模型。
# 如果没有配置,则默认使用 qwen-plus。
model=os.getenv("LLM_MODEL_ID", "qwen-plus"),
# 从环境变量读取 API Key,避免在源代码中硬编码密钥。
api_key=api_key, # type: ignore
# 阿里云百炼提供的 OpenAI 兼容接口地址。
api_base=os.getenv(
"LLM_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
),
# 表示当前模型使用 Chat Completions 接口,
# 而不是传统的文本补全接口。
is_chat_model=True,
# 表示模型支持 Function Calling。
# FunctionAgent 需要模型能够根据问题选择并调用工具。
is_function_calling_model=True,
# 上下文窗口大小。
context_window=int(
os.getenv("MODEL_CONTEXT_WINDOW", "32768")
),
# 温度设置为 0,使模型输出相对稳定。
temperature=0,
)
def build_agent() -> FunctionAgent:
"""
创建 FunctionAgent。
创建订单查询 Agent。
LlamaIndex 可以直接读取 Python 函数的名称、
参数类型和文档字符串,并将其转换为工具描述。
"""
# 将异步 Python 函数转换为 Agent 可以调用的工具。
# FunctionTool 是对现有 Python 函数的简单包装,并且同步函数和异步函数都支持。
# 在异步工具场景中,显式包装比直接放入tool中注册更清楚。
delivery_tool = FunctionTool.from_defaults(
async_fn=query_delivery_status,
)
return FunctionAgent(
llm=create_model(),
# 为 Agent 注册异步工具。
tools=[
delivery_tool,
],
# 明确要求 Agent 使用工具查询订单状态,
# 避免模型直接编造答案。
system_prompt=(
"你是一个订单物流查询助手。"
"当用户询问订单配送状态时,必须调用 query_delivery_status 工具。"
"获得工具结果后,请用简洁的中文回答用户。"
),
)
async def ask_agent(order_id: str) -> str:
"""
创建一个 Agent,并查询指定订单。
每一个订单查询任务使用一个独立 Agent,
避免多个用户请求共享同一份对话状态。
"""
agent = build_agent()
response = await agent.run(
user_msg=f"请查询订单 {order_id} 的配送状态。"
)
return str(response)
async def run_sequentially() -> None:
"""
顺序执行两个订单查询任务。
第二个任务必须等待第一个任务结束后才能开始。
"""
print("\n========== 顺序执行 ==========")
start_time = time.perf_counter()
result_a = await ask_agent("A100")
result_b = await ask_agent("B200")
elapsed_time = time.perf_counter() - start_time
print("\n订单 A100 的回答:")
print(result_a)
print("\n订单 B200 的回答:")
print(result_b)
print(f"\n顺序执行总耗时:{elapsed_time:.2f} 秒")
async def run_concurrently() -> None:
"""
并发执行两个订单查询任务。
asyncio.gather() 会同时启动两个任务。
当一个任务等待远程响应时,另一个任务可以继续运行。
"""
print("\n========== 并发执行 ==========")
start_time = time.perf_counter()
result_a, result_b = await asyncio.gather(
ask_agent("A100"),
ask_agent("B200"),
)
elapsed_time = time.perf_counter() - start_time
print("\n订单 A100 的回答:")
print(result_a)
print("\n订单 B200 的回答:")
print(result_b)
print(f"\n并发执行总耗时:{elapsed_time:.2f} 秒")
async def main() -> None:
"""
异步程序入口。
先顺序执行两个订单查询任务,
再并发执行相同的两个任务,
通过运行时间对比观察异步并发的作用。
"""
load_dotenv()
await run_sequentially()
await run_concurrently()
if __name__ == "__main__":
# 创建并启动事件循环。
asyncio.run(main())
========== 顺序执行 ==========
[工具开始执行] 正在查询订单 A100 ...
[工具执行完成] 已获得订单 A100 的查询结果
[工具开始执行] 正在查询订单 B200 ...
[工具执行完成] 已获得订单 B200 的查询结果
订单 A100 的回答:
订单 A100 已到达配送站,预计今天送达。
订单 B200 的回答:
订单 B200 已到达配送站,预计今天送达。
顺序执行总耗时:13.33 秒
========== 并发执行 ==========
[工具开始执行] 正在查询订单 A100 ...
[工具开始执行] 正在查询订单 B200 ...
[工具执行完成] 已获得订单 A100 的查询结果
[工具执行完成] 已获得订单 B200 的查询结果
订单 A100 的回答:
订单 A100 已到达配送站,预计今天送达。
订单 B200 的回答:
订单 B200 已到达配送站,预计今天送达。
并发执行总耗时:7.32 秒
async def:定义异步函数(协程函数)await:等待一个异步操作完成(挂起当前协程,让出控制权)await asyncio.gather(...)- 并发await ask_agent("A100")- 串行
LlamaIndex 的优势在于数据处理能力较强。它不仅能够调用普通工具,还提供了文档加载、索引构建、向量检索、查询引擎和 RAG 工作流等组件。因此,在企业知识库、文档问答和数据分析等场景中,LlamaIndex 具有较强的实用价值。
此外,LlamaIndex 可以将查询引擎封装为 Agent 工具。这样,Agent 不仅可以调用普通函数,也可以根据任务需要查询私有知识库,从而实现 Agent 与 RAG 的结合。
Demo:简易RAG问答助手
import os
from dotenv import load_dotenv
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.openai_like import OpenAILikeEmbedding
from llama_index.llms.openai_like import OpenAILike
def configure_models() -> None:
"""
配置大语言模型和 Embedding 模型。
RAG 中需要使用两个模型:
1. Embedding 模型:
将知识库文本和用户问题转换为向量,
用于计算文本之间的语义相似度。
2. 大语言模型:
读取检索到的知识库内容,
并生成最终回答。
"""
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
raise RuntimeError("请先在 .env 文件中设置 DASHSCOPE_API_KEY")
api_base = os.getenv(
"OPENAI_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 配置用于生成回答的大语言模型。
Settings.llm = OpenAILike(
model=os.getenv("MODEL_NAME", "qwen-plus"),
api_base=api_base,
api_key=api_key,
# 表示当前模型使用 Chat Completions 接口。
is_chat_model=True,
# RAG 查询不需要 Function Calling,
# 因此不需要设置 is_function_calling_model。
context_window=int(
os.getenv("MODEL_CONTEXT_WINDOW", "32768")
),
# 温度设置为 0,使回答相对稳定。
temperature=0,
)
# 配置用于向量检索的 Embedding 模型。
Settings.embed_model = OpenAILikeEmbedding(
model_name=os.getenv(
"EMBEDDING_MODEL_NAME",
"text-embedding-v4",
),
api_base=api_base,
api_key=api_key,
# text-embedding-v4 默认支持 1024 维向量。
dimensions=int(
os.getenv("EMBEDDING_DIMENSIONS", "1024")
),
)
def create_knowledge_base() -> list[Document]:
"""
创建一个很小的本地知识库。
在实际项目中,这些内容通常来自:
- 本地 TXT 文件;
- PDF 文档;
- Word 文档;
- 数据库;
- 企业内部知识库。
为了让示例尽量简单,这里直接在代码中创建文档。
"""
return [
Document(
text=(
"RAG 是 Retrieval-Augmented Generation 的缩写,"
"中文通常翻译为检索增强生成。"
"它的基本流程是:先根据用户问题检索相关文档,"
"再将检索结果作为上下文交给大语言模型生成回答。"
)
),
Document(
text=(
"RAG 和模型微调不是同一种技术。"
"RAG 主要通过外部知识库为模型补充信息,"
"通常不需要修改模型参数。"
"模型微调则会使用训练数据更新模型参数。"
)
),
Document(
text=(
"DPR 是 Dense Passage Retrieval 的缩写。"
"它是一种稠密检索方法,"
"通常使用两个编码器分别将问题和文档转换为向量。"
)
),
Document(
text=(
"FAISS 是一个高效的向量检索库。"
"它可以在大量向量中快速查找与查询向量最相似的内容。"
)
),
]
def build_query_engine():
"""
创建 RAG 查询引擎。
VectorStoreIndex.from_documents() 会执行以下工作:
1. 读取文档;
2. 将文档切分为较小的文本块;
3. 使用 Embedding 模型将文本块转换为向量;
4. 建立向量索引。
本示例未连接外部向量数据库,
因此向量索引默认存储在内存中。
程序退出后,索引会消失。
"""
documents = create_knowledge_base()
index = VectorStoreIndex.from_documents(
documents=documents,
# 在终端显示构建索引的进度。
show_progress=True,
)
# 将向量索引转换为查询引擎。
#
# similarity_top_k=2 表示:
# 每次收到问题后,检索相似度最高的两个文本块,
# 再将它们提供给大语言模型。
return index.as_query_engine(
similarity_top_k=2,
)
def print_retrieved_nodes(response) -> None:
"""
打印本次查询检索到的文本块。
这一步不是 RAG 必需的,
但有助于理解检索过程和排查问题。
"""
print("\n========== 检索到的知识库内容 ==========")
for index, source_node in enumerate(
response.source_nodes,
start=1,
):
print(f"\n--- 文本块 {index} ---")
# score 表示该文本块与问题的语义相似度。
# 数值通常越高,说明文本块与问题越相关。
print(f"相似度分数:{source_node.score}")
# node.text 表示文本块的实际内容。
print(f"文本内容:{source_node.node.text}")
def main() -> None:
"""
程序入口。
"""
# 加载 .env 文件中的环境变量。
load_dotenv()
# 配置大模型和 Embedding 模型。
configure_models()
# 构建向量索引,并创建查询引擎。
query_engine = build_query_engine()
# 用户问题。
question = "RAG 和模型微调有什么区别?"
print("\n========== 用户问题 ==========")
print(question)
# query() 会执行完整的 RAG 流程:
#
# 1. 将问题转换为向量;
# 2. 在向量索引中检索相关文本;
# 3. 将检索结果和问题发送给大语言模型;
# 4. 返回模型生成的答案。
response = query_engine.query(question)
# 输出检索到的文本块。
print_retrieved_nodes(response)
# 输出大语言模型根据检索结果生成的答案。
print("\n========== 最终回答 ==========")
print(str(response))
if __name__ == "__main__":
main()
LlamaIndex 的局限性在于其概念较多。初学者除了需要理解 Agent,还需要逐步掌握 Document、Node、索引、检索器和查询引擎等组件。对于完全不涉及私有数据的简单工具调用任务,LlamaIndex 的优势并不明显;但对于以知识库为核心的 Agent 应用,它通常更加适合。
LangChain v.s. LlamaIndex
同一个过程,用两种框架表达。
LangChain 同时提供同步和异步接口;
LlamaIndex 当前的 FunctionAgent 更偏向使用异步工作流接口。
LangChain / LangGraph 的表达方式:START -> model -> tools -> model -> tools -> model -> END;关注点是:当前运行到哪个节点、下一条边走向哪里、状态如何变化;
LlamaIndex Workflow 的表达方式:StartEvent -> all_llm Step -> ToolCallEvent -> execute_tool Step -> ToolResultEvent -> call_llm Step -> StopEvent;关注点是:当前产生了什么事件、哪个步骤消费这个事件、这个步骤又产生什么新事件。
本质上都是:模型判断 → 工具执行 → 返回结果 → 再次判断,只是组织方式不同。
2.3.4.AutoGen
AutoGen 的设计哲学根植于"以对话驱动协作"。它巧妙地将复杂的任务解决流程,映射为不同角色的智能体之间的一系列自动化对话。基于这一核心理念,AutoGen 框架持续演进。Hello-Agent教程以 0.7.4 版本为例。

AutoGen核心机制
该版本相较于之前的版本最显著的变化是引入了清晰的分层和异步优先的设计理念。
- 分层设计: 框架被拆分为两个核心模块:
autogen-core:作为框架的底层基础,封装了与语言模型交互、消息传递等核心功能。它的存在保证了框架的稳定性和未来扩展性。autogen-agentchat:构建于core之上,提供了用于开发对话式智能体应用的高级接口,简化了多智能体应用的开发流程。 这种分层策略使得各组件职责明确,降低了系统的耦合度。
- 异步优先: 新架构全面转向异步编程 (
async/await)。在多智能体协作场景中,网络请求(如调用 LLM API)是主要耗时操作。异步模式允许系统在等待一个智能体响应时处理其他任务,从而避免了线程阻塞,显著提升了并发处理能力和系统资源的利用效率。
该版本智能体的设计更加专注和模块化。
- AssistantAgent (助理智能体): 这是任务的主要解决者,其核心是封装了一个大型语言模型(LLM)。它的职责是根据对话历史生成富有逻辑和知识的回复,例如提出计划、撰写文章或编写代码。通过不同的系统消息(System Message),我们可以为其赋予不同的“专家”角色。
-
扮演AI助手角色,执行任务、编写代码、回答问题;不直接与用户交互,等待指令。
-
- UserProxyAgent (用户代理智能体): 这是 AutoGen 中功能独特的组件。它扮演着双重角色:既是人类用户的“代言人”,负责发起任务和传达意图;又是一个可靠的“执行器”,可以配置为执行代码或调用工具,并将结果反馈给其他智能体。这种设计清晰地区分了“思考”(由
AssistantAgent完成)与“行动”。- 扮演人类代理角色,代表用户执行动作(运行代码、执行命令)并接受Assistant的回复及决定下一步。
当任务需要多个智能体协作时,就需要一个机制来协调对话流程。在早期版本中,GroupChatManager 承担了这一职责。而在新架构中,引入了更灵活的 Team 或群聊概念,例如 RoundRobinGroupChat。
- 轮询群聊 (RoundRobinGroupChat): 这是一种明确的、顺序化的对话协调机制。它会让参与的智能体按照预定义的顺序依次发言。这种模式非常适用于流程固定的任务,例如一个典型的软件开发流程:产品经理先提出需求,然后工程师编写代码,最后由代码审查员进行检查。
- 工作流:
- 首先,创建一个
RoundRobinGroupChat实例,并将所有参与协作的智能体(如产品经理、工程师等)加入其中。 - 当一个任务开始时,群聊会按照预设的顺序,依次激活相应的智能体。
- 被选中的智能体根据当前的对话上下文进行响应。
- 群聊将新的回复加入对话历史,并激活下一个智能体。
- 这个过程会持续进行,直到达到最大对话轮次或满足预设的终止条件。
- 首先,创建一个
通过这种方式,AutoGen 将复杂的协作关系,简化为一个流程清晰、易于管理的自动化“圆桌会议”。开发者只需定义好每个团队成员的角色和发言顺序,剩下的协作流程便可由群聊机制自主驱动。
Demo:软件开发团队
核心函数:RoundRobinGroupChat、Console
目标是开发一个功能明确的 Web 应用:实时显示当前比特币的价格。它完整地覆盖了软件开发的典型环节:从需求分析、技术选型、编码实现到代码审查和最终测试。设计了四个职责分明的智能体角色:
- ProductManager (产品经理): 负责将用户的模糊需求转化为清晰、可执行的开发计划。
- Engineer (工程师): 依据开发计划,负责编写具体的应用程序代码。
- CodeReviewer (代码审查员): 负责审查工程师提交的代码,确保其质量、可读性和健壮性。
- UserProxy (用户代理): 代表最终用户,发起初始任务,并负责执行和验证最终交付的代码。
UserProxyAgent是一个特殊的智能体,它不依赖 LLM 进行回复,而是作为用户在系统中的代理。它的description字段清晰地描述了其职责,尤其重要的是,它负责在任务最终完成后发出TERMINATE指令,以正常结束整个协作流程。
这种角色划分是多智能体系统设计中的关键一步,它将一个复杂任务分解为多个由领域“专家”处理的子任务。定义智能体的核心在于编写高质量的系统消息 (System Message)。系统消息就像是给智能体设定的“行为准则”和“专业知识库”,它精确地规定了智能体的角色、职责、工作流程、输出结构,甚至是与其他智能体交互的方式(包含引导对话转向下一环节的明确指令)。一个精心设计的系统消息是确保多智能体系统能够高效、准确协作的关键。
import asyncio
import os
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.agents import UserProxyAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.messages import Console
def create_openai_model_client():
"""创建并配置 OpenAI 模型客户端"""
return OpenAIChatCompletionClient(
model=os.getenv("LLM_MODEL_ID", "gpt-4o"),
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
)
def create_product_manager(model_client):
"""创建产品经理智能体"""
system_message = """你是一位经验丰富的产品经理,专门负责软件产品的需求分析和项目规划。
你的核心职责包括:
1. **需求分析**:深入理解用户需求,识别核心功能和边界条件
2. **技术规划**:基于需求制定清晰的技术实现路径
3. **风险评估**:识别潜在的技术风险和用户体验问题
4. **协调沟通**:与工程师和其他团队成员进行有效沟通
当接到开发任务时,请按以下结构进行分析:
1. 需求理解与分析
2. 功能模块划分
3. 技术选型建议
4. 实现优先级排序
5. 验收标准定义
请简洁明了地回应,并在分析完成后说"请工程师开始实现"。"""
return AssistantAgent(
name="ProductManager",
model_client=model_client,
system_message=system_message,
)
def create_engineer(model_client):
"""创建软件工程师智能体"""
system_message = """你是一位资深的软件工程师,擅长 Python 开发和 Web 应用构建。
你的技术专长包括:
1. **Python 编程**:熟练掌握 Python 语法和最佳实践
2. **Web 开发**:精通 Streamlit、Flask、Django 等框架
3. **API 集成**:有丰富的第三方 API 集成经验
4. **错误处理**:注重代码的健壮性和异常处理
当收到开发任务时,请:
1. 仔细分析技术需求
2. 选择合适的技术方案
3. 编写完整的代码实现
4. 添加必要的注释和说明
5. 考虑边界情况和异常处理
请提供完整的可运行代码,并在完成后说"请代码审查员检查"。"""
return AssistantAgent(
name="Engineer",
model_client=model_client,
system_message=system_message,
)
def create_code_reviewer(model_client):
"""创建代码审查员智能体"""
system_message = """你是一位经验丰富的代码审查专家,专注于代码质量和最佳实践。
你的审查重点包括:
1. **代码质量**:检查代码的可读性、可维护性和性能
2. **安全性**:识别潜在的安全漏洞和风险点
3. **最佳实践**:确保代码遵循行业标准和最佳实践
4. **错误处理**:验证异常处理的完整性和合理性
审查流程:
1. 仔细阅读和理解代码逻辑
2. 检查代码规范和最佳实践
3. 识别潜在问题和改进点
4. 提供具体的修改建议
5. 评估代码的整体质量
请提供具体的审查意见,完成后说"代码审查完成,请用户代理测试"。"""
return AssistantAgent(
name="CodeReviewer",
model_client=model_client,
system_message=system_message,
)
def create_user_proxy():
"""创建用户代理智能体"""
return UserProxyAgent(
name="UserProxy",
description="""用户代理,负责以下职责:
1. 代表用户提出开发需求
2. 执行最终的代码实现
3. 验证功能是否符合预期
4. 提供用户反馈和建议
完成测试后请回复 TERMINATE。""",
)
async def run_software_development_team():
"""运行软件开发团队协作"""
# 初始化模型客户端
model_client = create_openai_model_client()
# 创建所有智能体
product_manager = create_product_manager(model_client)
engineer = create_engineer(model_client)
code_reviewer = create_code_reviewer(model_client)
user_proxy = create_user_proxy()
# 定义团队聊天和协作规则
team_chat = RoundRobinGroupChat(
participants=[product_manager, engineer, code_reviewer, user_proxy],
termination_condition=TextMentionTermination("TERMINATE"),
max_turns=20,
)
# 定义任务描述
task = """我们需要开发一个比特币价格显示应用,具体要求如下:
核心功能:
- 实时显示比特币当前价格(USD)
- 显示24小时价格变化趋势(涨跌幅和涨跌额)
- 提供价格刷新功能
技术要求:
- 使用 Streamlit 框架创建 Web 应用
- 界面简洁美观,用户友好
- 添加适当的错误处理和加载状态
请团队协作完成这个任务,从需求分析到最终实现。"""
# 异步执行团队协作,并流式输出对话过程
result = await Console(team_chat.run_stream(task=task)) # 异步等待单个任务,并非并行执行
return result
# 主程序入口
if __name__ == "__main__":
result = asyncio.run(run_software_development_team())
- 参与者顺序:
participants列表的顺序决定了智能体发言的先后次序。 - 终止条件:
termination_condition是控制协作流程何时结束的关键。这里我们设定,当任何消息中包含关键词 "TERMINATE" 时,对话便结束。在我们的设计中,这个指令由UserProxy在完成最终测试后发出。 - 最大轮次:
max_turns是一个安全阀,用于防止对话陷入无限循环,避免不必要的资源消耗。 - 由于 AutoGen
0.7.4采用异步架构,整个协作流程的启动和运行都在一个异步函数中完成,并最终通过asyncio.run()来执行。
🔧 正在初始化模型客户端...
👥 正在创建智能体团队...
🚀 启动 AutoGen 软件开发团队协作...
============================================================
---------- TextMessage (user) ----------
我们需要开发一个比特币价格显示应用,具体要求如下:
...
请团队协作完成这个任务,从需求分析到最终实现。
---------- TextMessage (ProductManager) ----------
### 1. 需求理解与分析
...
请工程师开始实现。
---------- TextMessage (Engineer) ----------
### 技术方案实施
...
请代码审查员检查。
---------- TextMessage (CodeReviewer) ----------
### 代码审查
...
代码审查完成,请用户代理测试。
---------- TextMessage (UserProxy) ----------
已经完成需求
---------- TextMessage (ProductManager) ----------
太好了,感谢您的反馈!如果在使用过程中有任何问题,或者有其他功能需求和改进建议,请随时告知我们。我们会持续提供支持和改进。期待您对我们的应用
有愉快的使用体验!
---------- TextMessage (Engineer) ----------
很高兴听到项目顺利完成。如果您或用户有任何问题或者需要帮助,请随时联系我们。感谢您对我们工作的支持,让我们一起确保应用稳定运行并不断优化用户
体验!
---------- TextMessage (CodeReviewer) ----------
非常感谢大家的努力与协作,使得项目能够顺利完成。未来若有更多技术支持的需求或者需要改进的地方,我们愿意为项目的持续优化贡
献力量。期待用户能够享受到流畅的体验,同时也欢迎提出更多的反馈与建议。再次感谢团队的合作!
---------- TextMessage (UserProxy) ----------
Enter your response: TERMINATE
============================================================
✅ 团队协作完成!
📋 协作结果摘要:
- 参与智能体数量:4个
- 任务完成状态:成功
如果想使用非 OpenAI 系列的模型(如 DeepSeek、通义千问等),在 0.7.4 版本中需要在 OpenAIChatCompletionClient 的参数中传入模型信息字典。这个 model_info 字典帮助 AutoGen 了解模型的能力边界,从而更好地适配不同的模型服务。
from autogen_ext.models.openai import OpenAIChatCompletionClient
model_client = OpenAIChatCompletionClient(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
model_info={
"function_calling": True,
"max_tokens": 4096,
"context_length": 32768,
"vision": False,
"json_output": True,
"family": "deepseek",
"structured_output": True,
}
)
AutoGen的优缺点
(1)优势
- 如案例所示,我们无需为智能体团队设计复杂的状态机或控制流逻辑,而是将一个完整的软件开发流程,自然地映射为产品经理、工程师和审查员之间的对话。这种方式更贴近人类团队的协作模式,显著降低了为复杂任务建模的门槛。开发者可以将更多精力聚焦于定义“谁(角色)”以及“做什么(职责)”,而非“如何做(流程控制)”。
- 框架允许通过系统消息(System Message)为每个智能体赋予高度专业化的角色。在案例中,
ProductManager专注于需求,而CodeReviewer则专注于质量。一个精心设计的智能体可以在不同项目中被复用,易于维护和扩展。 - 对于流程化任务,
RoundRobinGroupChat这样机制提供了清晰、可预测的协作流程。同时,UserProxyAgent的设计为“人类在环”(Human-in-the-loop)提供了天然的接口。它既可以作为任务的发起者,也可以是流程的监督者和最终的验收者。这种设计确保了自动化系统始终处于人类的监督之下。
(2)局限性
- 虽然
RoundRobinGroupChat提供了顺序化的流程,但基于 LLM 的对话本质上具有不确定性。智能体可能会产生偏离预期的回复,导致对话走向意外的分支,甚至陷入循环。 - 当智能体团队的工作结果未达预期时,调试过程可能非常棘手。与传统程序不同,我们得到的不是清晰的错误堆栈,而是一长串的对话历史。这被称为“对话式调试”的难题。
2.3.5.AgentScope
如果说 AutoGen 的设计哲学是"以对话驱动协作",那么 AgentScope 则代表了另一种技术路径:工程化优先的多智能体平台。
AgentScope核心设计
与 AutoGen 相比,AgentScope 的核心差异在于其消息驱动的架构设计和工业级的工程实践。如果说 AutoGen 更像是一个灵活的"对话工作室",那么 AgentScope 就是一个完整的"智能体操作系统",为开发者提供了从开发、测试到部署的全生命周期支持。与许多框架采用的继承式设计不同,AgentScope 选择了组合式架构和消息驱动模式。这种设计不仅增强了系统的模块化程度,也为其出色的并发性能和分布式能力奠定了基础。
AgentScope 采用了清晰的分层模块化设计,从底层的基础组件到上层的应用编排,形成了一个完整的智能体开发生态。
- 在这个架构中,最底层是基础组件层 (Foundational Components),它为整个框架提供了核心的构建块。
Message组件定义了统一的消息格式,支持从简单的文本交互到复杂的多模态内容;Memory组件提供了短期和长期记忆管理;Model API层抽象了对不同大语言模型的调用;而Tool组件则封装了智能体与外部世界交互的能力。 - 在基础组件之上,智能体基础设施层 (Agent-level Infrastructure) 提供了更高级的抽象。这一层不仅包含了各种预构建的智能体(如浏览器使用智能体、深度研究智能体),还实现了经典的 ReAct 范式,支持智能体钩子、并行工具调用、状态管理等高级特性。特别值得注意的是,这一层原生支持异步执行与实时控制,这是 AgentScope 相比其他框架的一个重要优势。
- 多智能体协作层 (Multi-Agent Cooperation) 是 AgentScope 的核心创新所在。
MsgHub作为消息中心,负责智能体间的消息路由和状态管理;而Pipeline系统则提供了灵活的工作流编排能力,支持顺序、并发等多种执行模式。这种设计使得开发者可以轻松构建复杂的多智能体协作场景。 - 最上层的开发与部署层 (Deployment & Development)则体现了 AgentScope 对工程化的重视。
AgentScope Runtime提供了生产级的运行时环境,而AgentScope Studio则为开发者提供了完整的可视化开发工具链。
AgentScope 的核心创新在于其消息驱动架构。在这个架构中,所有的智能体交互都被抽象为消息的发送和接收,而不是传统的函数调用。将消息作为交互的基础单元,带来了几个关键优势:
- 异步解耦: 消息的发送方和接收方在时间上解耦,无需相互等待,天然支持高并发场景。
- 位置透明: 智能体无需关心另一个智能体是在本地进程还是在远程服务器上,消息系统会自动处理路由。
- 可观测性: 每一条消息都可以被记录、追踪和分析,极大地简化了复杂系统的调试与监控。
- 可靠性: 消息可以被持久化存储和重试,即使系统出现故障,也能保证交互的最终一致性,提升了系统的容错能力。
from agentscope.message import Msg
# 消息的标准结构
message = Msg(
name="Alice", # 发送者名称
content="Hello, Bob!", # 消息内容
role="user", # 角色类型
metadata={ # 元数据信息
"timestamp": "2024-01-15T10:30:00Z",
"message_type": "text",
"priority": "normal"
}
)
在 AgentScope 中,每个智能体都有明确的生命周期(初始化、运行、暂停、销毁等),并基于一个统一的基类 AgentBase 来实现。开发者通常只需要关注其核心的 reply 方法。这种设计模式分离了智能体的内部逻辑与外部通信,开发者只需在 reply 方法中定义智能体“思考和回应”的方式即可。
from agentscope.agents import AgentBase
class CustomAgent(AgentBase):
def __init__(self, name: str, **kwargs):
super().__init__(name=name, **kwargs)
# 智能体初始化逻辑
def reply(self, x: Msg) -> Msg:
# 智能体的核心响应逻辑
response = self.model(x.content)
return Msg(name=self.name, content=response, role="assistant")
def observe(self, x: Msg) -> None:
# 智能体的观察逻辑(可选)
self.memory.add(x)
AgentScope 内置了一个消息中心 (MsgHub),它是整个消息驱动架构的中枢。MsgHub 不仅负责消息的路由和分发,还集成了持久化和分布式通信等高级功能,它有以下这些特点。
- 灵活的消息路由: 支持点对点、广播、组播等多种通信模式,可以构建灵活复杂的交互网络。
- 消息持久化: 能够将所有消息自动保存到数据库(如 SQLite, MongoDB),确保了长期运行任务的状态可以被恢复。
- 原生分布式支持: 这是 AgentScope 的标志性特性。智能体可以被部署在不同的进程或服务器上,
MsgHub会通过 RPC(远程过程调用)自动处理跨节点的通信,对开发者完全透明。
这些由底层架构提供的工程化能力,使得 AgentScope 在处理需要高并发、高可靠性的复杂应用场景时,比传统的对话驱动框架更具优势。当然,这也要求开发者理解并适应消息驱动的异步编程范式。
Demo:三国狼人杀游戏
这个案例不仅展示了 AgentScope 在处理复杂多智能体交互方面的优势,更重要的是,它演示了如何在一个需要实时协作、角色扮演和策略博弈的场景中,充分发挥消息驱动架构的威力。与传统狼人杀不同,我们的"三国狼人杀"将刘备、关羽、诸葛亮等经典角色引入游戏,每个智能体不仅要完成狼人杀的基本任务(如狼人击杀、预言家查验、村民推理),还要体现出对应三国人物的性格特点和行为模式。这种设计让我们能够观察到 AgentScope 在处理多层次角色建模方面的表现。
本案例的系统设计遵循了分层解耦的原则,将游戏逻辑划分为三个独立的层次,每个层次都映射了 AgentScope 的一个或多个核心组件:
- 游戏控制层 (Game Control Layer):由一个
ThreeKingdomsWerewolfGame类作为游戏的主控制器,负责维护全局状态(如玩家存活列表、当前游戏阶段)、推进游戏流程(调用夜晚阶段、白天阶段)以及裁定胜负。 - 智能体交互层 (Agent Interaction Layer):完全由
MsgHub驱动。所有智能体间的通信,无论是狼人间的秘密协商,还是白天的公开辩论,都通过消息中心进行路由和分发。 - 角色建模层 (Role Modeling Layer):每个玩家都是一个基于
DialogAgent的实例。我们通过精心设计的系统提示词,为每个智能体注入了“游戏角色”和“三国人格”的双重身份。
本案例最核心的设计是以消息驱动代替状态机来管理游戏流程。在传统实现中,游戏阶段的转换通常由一个中心化的状态机(State Machine)控制。而在 AgentScope 的范式下,游戏流程被自然地建模为一系列定义好的消息交互模式。
例如,狼人阶段的实现,并非一个简单的函数调用,而是通过 MsgHub 动态创建一个临时的、仅包含狼人玩家的私密通信频道。这种设计的优势在于,游戏逻辑被清晰地表达为“在特定上下文中,以何种模式进行消息交换”,而不是一连串僵硬的状态转换。白天讨论(全员广播)、预言家查验(点对点请求)等阶段也都遵循同样的设计范式:
async def werewolf_phase(self, round_num: int):
"""狼人阶段 - 展示消息驱动的协作模式"""
if not self.werewolves:
return None
# 通过消息中心建立狼人专属通信频道
async with MsgHub(
self.werewolves,
enable_auto_broadcast=True,
announcement=await self.moderator.announce(
f"狼人们,请讨论今晚的击杀目标。存活玩家:{format_player_list(self.alive_players)}"
),
) as werewolves_hub:
# 讨论阶段:狼人通过消息交换策略
for _ in range(MAX_DISCUSSION_ROUND):
for wolf in self.werewolves:
await wolf(structured_model=DiscussionModelCN)
# 投票阶段:收集并统计狼人的击杀决策
werewolves_hub.set_auto_broadcast(False)
kill_votes = await fanout_pipeline(
self.werewolves,
msg=await self.moderator.announce("请选择击杀目标"),
structured_model=WerewolfKillModelCN,
enable_gather=False,
)
狼人杀游戏的一个关键挑战是如何确保智能体的行为符合游戏规则。AgentScope 的结构化输出机制为这个问题提供了解决方案。通过这种方式,我们不仅确保了智能体输出的格式一致性,更重要的是实现了游戏规则的自动化约束。例如,女巫智能体无法同时对同一目标使用解药和毒药,预言家每晚只能查验一名玩家,这些约束都通过数据模型的字段定义和验证逻辑自动执行。我们为不同的游戏行为定义了严格的数据模型(并且在实际实现过程中用了pydantic来检查输出结构):
class DiscussionModelCN(BaseModel):
"""讨论阶段的输出格式"""
reach_agreement: bool = Field(
description="是否已达成一致意见",
default=False
)
confidence_level: int = Field(
description="对当前推理的信心程度(1-10)",
ge=1, le=10,
default=5
)
key_evidence: Optional[str] = Field(
description="支持你观点的关键证据",
default=None
)
class WitchActionModelCN(BaseModel):
"""女巫行动的输出格式"""
use_antidote: bool = Field(description="是否使用解药")
use_poison: bool = Field(description="是否使用毒药")
target_name: Optional[str] = Field(description="毒药目标玩家姓名")
在这个案例中,最有趣的技术挑战是如何让智能体同时扮演好两个层面的角色:游戏功能角色(狼人、预言家等)和文化人格角色(刘备、曹操等)。我们通过提示词工程来解决这个问题:
def get_role_prompt(role: str, character: str) -> str:
"""获取角色提示词 - 融合游戏规则与人物性格"""
base_prompt = f"""你是{character},在这场三国狼人杀游戏中扮演{role}。
重要规则:
1. 你只能通过对话和推理参与游戏
2. 不要尝试调用任何外部工具或函数
3. 严格按照要求的JSON格式回复
角色特点:
"""
if role == "狼人":
return base_prompt + f"""
- 你是狼人阵营,目标是消灭所有好人
- 夜晚可以与其他狼人协商击杀目标
- 白天要隐藏身份,误导好人
- 以{character}的性格说话和行动
"""
这种设计让我们观察到了一个有趣的现象:不同的三国人物在扮演相同游戏角色时,会表现出截然不同的策略和话语风格。例如,扮演狼人的"曹操"可能会表现得更加狡猾和善于伪装,而扮演狼人的"张飞"则可能显得更加直接和冲动。
AgentScope 的异步架构在这个多智能体游戏中发挥了重要作用。游戏中经常出现需要同时收集多个智能体决策的场景,比如投票阶段:
# 并行收集所有玩家的投票决策
vote_msgs = await fanout_pipeline(
self.alive_players,
await self.moderator.announce("请投票选择要淘汰的玩家"),
structured_model=get_vote_model_cn(self.alive_players),
enable_gather=False,
)
fanout_pipeline 允许我们并行地向所有智能体发送相同的消息,并异步收集它们的响应。这不仅提高了游戏的执行效率,更重要的是模拟了真实狼人杀游戏中"同时投票"的场景。同时,我们在关键环节加入了容错处理:
try:
response = await wolf(
"请分析当前局势并表达你的观点。",
structured_model=DiscussionModelCN
)
except Exception as e:
print(f"⚠️ {wolf.name} 讨论时出错: {e}")
# 创建默认响应,确保游戏继续进行
default_response = DiscussionModelCN(
reach_agreement=False,
confidence_level=5,
key_evidence="暂时无法分析"
)
这种设计确保了即使某个智能体出现异常,整个游戏流程也能继续进行。
我把这个项目改成了使用LlamaIndex的OpenAILike接口的自定义agent编排项目,LlamaIndex-Werewolf(github地址)
2.3.6.CAMEL
与 AutoGen 和 AgentScope 这样功能全面的框架不同,CAMEL最初的核心目标是探索如何在最少的人类干预下,让两个智能体通过“角色扮演”自主协作解决复杂任务。
CAMEL自主协助
CAMEL 实现自主协作的基石是两大核心概念:角色扮演 (Role-Playing) 和 引导性提示 (Inception Prompting)。双智能体。
(1)角色扮演
在 CAMEL 最初的设计中,一个任务通常由两个智能体协作完成。这两个智能体被赋予了互补的、明确定义的“角色”。一个扮演“AI 用户” (AI User),负责提出需求、下达指令和构思任务步骤;另一个则扮演“AI 助理” (AI Assistant),负责根据指令执行具体操作和提供解决方案。
例如,在一个“开发股票交易策略分析工具”的任务中:
- AI 用户 的角色可能是一位“资深股票交易员”。它懂市场、懂策略,但不懂编程。
- AI 助理 的角色则是一位“优秀的 Python 程序员”。它精通编程,但对股票交易一无所知。
通过这种设定,任务的解决过程就被自然地转化为一场两位“跨领域专家”之间的对话。交易员提出专业需求,程序员将其转化为代码实现,两者协作完成任何一方都无法独立完成的复杂任务。
(2)引导性提示
仅仅设定角色还不够,如何确保两个 AI 在没有人类持续监督的情况下,能始终“待在自己的角色里”,并且高效地朝着共同目标前进呢?这就是 CAMEL 最核心的技术,引导性提示发挥作用的地方。“引导性提示”是在对话开始前,分别注入给两个智能体的一段精心设计的、结构化的初始指令(System Prompt)。这段指令就像是为智能体植入的“行动纲领”,它通常包含以下几个关键部分:
- 明确自身角色:例如,“你是一位资深的股票交易员...”
- 告知协作者角色:例如,“你正在与一位优秀的 Python 程序员合作...”
- 定义共同目标:例如,“你们的共同目标是开发一个股票交易策略分析工具。”
- 设定行为约束和沟通协议:这是最关键的一环。例如,指令会要求 AI 用户“一次只提出一个清晰、具体的步骤”,并要求 AI 助理“在完成上一步之前不要追问更多细节”,同时规定双方需在回复的末尾使用特定标志(如
<SOLUTION>)来标识任务的完成。
这些约束条件确保了对话不会偏离主题、不会陷入无效循环,而是以一种高度结构化、任务驱动的方式向前推进。
Demo:AI科普电子书
该案例让一位 AI 心理学家与一位 AI 作者合作,共同创作一本关于"拖延症心理学"的短篇电子书。这个案例体现了 CAMEL 的核心优势,让两个智能体在各自专业领域发挥所长,协作完成单个智能体难以胜任的复杂创作任务。
核心函数:RolePlaying、init_chat()、step()
场景设定:创作一本面向普通读者的拖延症心理学科普电子书,要求既有科学严谨性,又具备良好的可读性。
智能体角色:
- 心理学家(Psychologist):具备深厚的心理学理论基础,熟悉认知行为科学、神经科学等相关领域,能够提供专业的学术见解和实证研究支持
- 作家(Writer):拥有优秀的写作技巧和叙述能力,善于将复杂的学术概念转化为生动易懂的文字,注重读者体验和内容的可读性
(这个demo我没运行,直接复制了Hello-Agent的教程)
首先,我们需要明确两位 AI 专家的共同目标。我们通过一个内容详实的字符串 task_prompt 来定义这个任务。task_prompt 是整个协作的“任务说明书”。它不仅是我们要完成的目标,也将在幕后被 CAMEL 用来生成“引导性提示”,确保两位智能体的对话始终围绕这个核心目标展开。
from colorama import Fore
from camel.societies import RolePlaying
from camel.utils import print_text_animated
from camel.models import ModelFactory
from camel.types import ModelPlatformType
from dotenv import load_dotenv
import os
load_dotenv()
LLM_API_KEY = os.getenv("LLM_API_KEY")
LLM_BASE_URL = os.getenv("LLM_BASE_URL")
LLM_MODEL = os.getenv("LLM_MODEL")
#创建模型,在这里以Qwen为例,调用的百炼大模型平台API
model = ModelFactory.create(
model_platform=ModelPlatformType.QWEN,
model_type=LLM_MODEL,
url=LLM_BASE_URL,
api_key=LLM_API_KEY
)
# 定义协作任务
task_prompt = """
创作一本关于"拖延症心理学"的短篇电子书,目标读者是对心理学感兴趣的普通大众。
要求:
1. 内容科学严谨,基于实证研究
2. 语言通俗易懂,避免过多专业术语
3. 包含实用的改善建议和案例分析
4. 篇幅控制在8000-10000字
5. 结构清晰,包含引言、核心章节和总结
"""
print(Fore.YELLOW + f"协作任务:\n{task_prompt}\n")
接下来,我们创建 RolePlaying 会话实例。这是 CAMEL 的核心操作,它根据我们提供的角色和任务,快速构建一个双智能体协作“社会”。RolePlaying 是 CAMEL 提供的高级 API,它封装了复杂的提示工程。我们只需传入两个角色的名称和任务即可。在 CAMEL 的设计中,user 角色是对话的“推动者”和“需求方”,而 assistant 角色是“执行者”和“方案提供方”。因此,我们将负责规划结构的“作家”分配给 user_role_name,将负责提供专业知识的“心理学家”分配给 assistant_role_name。
# 初始化角色扮演会话
# AI 作家作为 "user",负责提出写作结构和要求
# AI 心理学家作为 "assistant",负责提供专业知识和内容
role_play_session = RolePlaying(
assistant_role_name="心理学家",
user_role_name="作家",
task_prompt=task_prompt,
model=model,
with_task_specify=False, # 在本例中,我们直接使用给定的task_prompt
)
print(Fore.CYAN + f"具体任务描述:\n{role_play_session.task_prompt}\n")
最后,我们编写一个循环来驱动整个对话过程,让两位 AI 专家开始它们的自动化协作。这段 while 循环是自动化协作的核心。对话由 init_chat() 方法基于任务和角色自动开启,无需人工编写开场白。循环的每一步都通过调用 step() 来驱动一轮完整的交互(作家提需求、心理学家给内容),并将上一轮心理学家的输出作为下一轮的输入,形成环-环相扣的创作链。整个过程将持续进行,直到达到预设的对话轮次上限,或任一智能体输出任务完成标志 <CAMEL_TASK_DONE> 后自动终止。
# 开始协作对话
chat_turn_limit, n = 30, 0
# 调用 init_chat() 来获得由 AI 生成的初始对话消息
input_msg = role_play_session.init_chat()
while n < chat_turn_limit:
n += 1
# step() 方法驱动一轮完整的对话,AI 用户和 AI 助理各发言一次
assistant_response, user_response = role_play_session.step(input_msg)
# 检查是否有消息返回,防止对话提前终止
if assistant_response.msg is None or user_response.msg is None:
break
print_text_animated(Fore.BLUE + f"作家 (AI User):\n\n{user_response.msg.content}\n")
print_text_animated(Fore.GREEN + f"心理学家 (AI Assistant):\n\n{assistant_response.msg.content}\n")
# 检查任务完成标志
if "<CAMEL_TASK_DONE>" in user_response.msg.content or "<CAMEL_TASK_DONE>" in assistant_response.msg.content:
print(Fore.MAGENTA + "✅ 电子书创作完成!")
break
# 将助理的回复作为下一轮对话的输入
input_msg = assistant_response.msg
print(Fore.YELLOW + f"总共进行了 {n} 轮协作对话")
当执行上述代码后,我们并非只是得到一长串单调的问答,而是能够观察到一个高度结构化的、如同人类专家团队般的协作流程在自动进行。整个创作过程自然地分为几个阶段:
第一阶段 (约 1-5 轮): 框架搭建与目标对齐 在对话的初期,“作家”智能体首先会扮演起主导者的角色,提出对电子书整体结构和章节安排的初步设想。随后,“心理学家”会从其专业角度对这个框架进行审视和补充,确保核心的学术模块(如理论基础、关键概念等)没有遗漏,从而在协作开始之初就对最终产出物达成共识。(个人感觉类似于plan)
第二阶段 (约 6-20 轮): 核心内容生成与知识转译 这是最高效的内容创作阶段。协作模式会变为一种稳定的“请求-响应”循环:(个人感觉类似于execute)
- 心理学家:负责提供“硬核”的专业知识,如对“时间折扣理论”、“执行功能缺陷”等核心概念的科学解释,并引用相关的实验研究来支撑观点。
- 作家:则发挥其“翻译官”的作用,将这些严谨但可能晦涩的学术概念,转化为生动、形象的比喻和贴近生活的案例。例如,它可能会将“大脑中的‘现在偏见’”这个概念,比作“一个只顾眼前糖果、不顾长远健康的任性孩子”。
第三阶段 (约 21-25 轮): 迭代优化与质量保证 当书籍的主体内容完成后,对话的重心会转移到对已有文本的打磨和完善上。此时,两位智能体的角色会发生微妙的变化:(个人感觉类似于reflection)
- 作家:更侧重于审视文章的整体流畅性、逻辑衔接和语言风格,从“读者体验”出发提出修改建议。
- 心理学家:则再次扮演“事实核查员”,确保在转译和润色的过程中,核心知识的科学准确性没有丢失,并为某些观点补充更有力的实证研究支持。
第四阶段 (收尾): 总结与升华 在最后的几轮对话中,双方会协作完成实用建议的总结和全书的回顾,确保电子书有一个清晰、有力的结尾,为读者留下深刻印象并提供实际价值。
CAMEL的优缺点
CAMEL 最大的优势在于其"轻架构、重提示"的设计哲学。相比 AutoGen 的复杂对话管理和 AgentScope 的分布式架构,CAMEL 通过精心设计的初始提示就能实现高质量的智能体协作。这种自然涌现的协作行为,往往比硬编码的工作流更加灵活和高效,特别适合需要深度协作和创造性思维的任务。值得注意的是,CAMEL 框架正在经历快速的发展和演进。从其 GitHub 仓库 可以看到,CAMEL 已经远不止是一个简单的双智能体协作框架,目前已经具备:
- 多模态能力:支持文本、图像、音频等多种模态的智能体协作
- 工具集成:内置了丰富的工具库,包括搜索、计算、代码执行等
- 模型适配:支持 OpenAI、Anthropic、Google、开源模型等多种 LLM 后端
- 生态联动:与 LangChain、CrewAI、AutoGen 等主流框架实现了互操作性
但是 CAMEL 的成功很大程度上取决于初始提示的质量,带来提示词设计和调试复杂性的困难。并且在处理大规模多智能体场景时面临挑战,比如对话管理、状态同步和冲突解决。
2.3.7.LangGraph
LangGraph结构梳理
LangGraph 作为 LangChain 生态系统的重要扩展,代表了智能体框架设计的一个全新方向(他可以独立于LangChain存在)。与前面介绍的基于“对话”的框架(如 AutoGen 和 CAMEL)不同,LangGraph 将智能体的执行流程建模为一种状态机(State Machine),并将其表示为有向图(Directed Graph)。在这种范式中,图的节点(Nodes)代表一个具体的计算步骤(如调用 LLM、执行工具),而边(Edges)则定义了从一个节点到另一个节点的跳转逻辑。这种设计的革命性之处在于它天然支持循环,使得构建能够进行迭代、反思和自我修正的复杂智能体工作流变得前所未有的直观和简单。
- 全局状态(State)。整个图的执行过程都围绕一个共享的状态对象进行。这个状态通常被定义为一个 Python 的
TypedDict,它可以包含任何你需要追踪的信息,如对话历史、中间结果、迭代次数等。所有的节点都能读取和更新这个中心状态。from typing import TypedDict, List # 定义全局状态的数据结构 class AgentState(TypedDict): messages: List[str] # 对话历史 current_task: str # 当前任务 final_answer: str # 最终答案 # ... 任何其他需要追踪的状态 - 节点(Nodes)。每个节点都是一个接收当前状态作为输入、并返回一个更新后的状态作为输出的 Python 函数。节点是执行具体工作的单元。
# 定义一个“规划者”节点函数 def planner_node(state: AgentState) -> AgentState: """根据当前任务制定计划,并更新状态。""" current_task = state["current_task"] # ... 调用LLM生成计划 ... plan = f"为任务 '{current_task}' 生成的计划..." # 将新消息追加到状态中 state["messages"].append(plan) return state # 定义一个“执行者”节点函数 def executor_node(state: AgentState) -> AgentState: """执行最新计划,并更新状态。""" latest_plan = state["messages"][-1] # ... 执行计划并获得结果 ... result = f"执行计划 '{latest_plan}' 的结果..." state["messages"].append(result) return state - 边(Edges)。边负责连接节点,定义工作流的方向。最简单的边是常规边,它指定了一个节点的输出总是流向另一个固定的节点。 LangGraph 最强大的功能在于条件边(Conditional Edges)。它通过一个函数来判断当前的状态,然后动态地决定下一步应该跳转到哪个节点。这正是实现循环和复杂逻辑分支的关键。
def should_continue(state: AgentState) -> str: """条件函数:根据状态决定下一步路由。""" # 假设如果消息少于3条,则需要继续规划 if len(state["messages"]) < 3: # 返回的字符串需要与添加条件边时定义的键匹配 return "continue_to_planner" else: state["final_answer"] = state["messages"][-1] return "end_workflow"
在定义了状态、节点和边之后,我们可以像搭积木一样将它们组装成一个可执行的工作流。
from langgraph.graph import StateGraph, END
# 初始化一个状态图,并绑定我们定义的状态结构
workflow = StateGraph(AgentState)
# 将节点函数添加到图中
workflow.add_node("planner", planner_node)
workflow.add_node("executor", executor_node)
# 设置图的入口点
workflow.set_entry_point("planner")
# 添加常规边,连接 planner 和 executor
workflow.add_edge("planner", "executor")
# 添加条件边,实现动态路由
workflow.add_conditional_edges(
# 起始节点
"executor",
# 判断函数
should_continue,
# 路由映射:将判断函数的返回值映射到目标节点
{
"continue_to_planner": "planner", # 如果返回"continue_to_planner",则跳回planner节点
"end_workflow": END # 如果返回"end_workflow",则结束流程
}
)
# 编译图,生成可执行的应用
app = workflow.compile()
# 运行图
inputs = {"current_task": "分析最近的AI行业新闻", "messages": []}
for event in app.stream(inputs):
print(event)
Demo:简化的问答助手
我们将构建一个简化的问答对话助手,它会遵循一个清晰、固定的三步流程来回答用户的问题:
- 理解 (Understand):首先,分析用户的查询意图。
- 搜索 (Search):然后,模拟搜索与意图相关的信息。
- 回答 (Answer):最后,基于意图和搜索到的信息,生成最终答案。
这个案例将清晰地展示如何定义状态、创建节点以及将它们线性地连接成一个完整的工作流。我们将代码分解为四个核心步骤:定义状态、创建节点、构建图、以及运行应用。
- 首先,我们需要定义一个贯穿整个工作流的全局状态。这是一个共享的数据结构,它在图的每个节点之间传递,作为工作流的持久化上下文。 每个节点都可以读取该结构中的数据,并对其进行更新。
- 我们创建了
SearchState这个TypedDict,为状态对象定义了一个清晰的数据模式(Schema)。一个关键的设计是同时包含了user_query和search_query字段。这允许智能体先将用户的自然语言提问,优化成更适合搜索引擎的精炼关键词,从而显著提升搜索结果的质量。
- 我们创建了
- 定义好状态结构后,下一步是创建构成我们工作流的各个节点。在 LangGraph 中,每个节点都是一个执行具体任务的 Python 函数。这些函数接收当前的状态对象作为输入,并返回一个包含更新后字段的字典。有三个核心节点:
- 理解与查询节点:此节点是工作流的第一步,此节点的职责是理解用户意图,并为其生成一个最优化的搜索查询。该节点通过一个结构化的提示,要求 LLM 同时完成“意图理解”和“关键词生成”两个任务,并将解析出的专用搜索关键词更新到状态的
search_query字段中,为下一步的精确搜索做好准备。 - 搜索节点:该节点负责执行智能体的“工具使用”能力,它将调用 Tavily API 进行真实的互联网搜索,并具备基础的错误处理功能。
- 回答节点:最后的回答节点能够根据上一步的搜索是否成功,来选择不同的回答策略,具备了一定的弹性。
- 理解与查询节点:此节点是工作流的第一步,此节点的职责是理解用户意图,并为其生成一个最优化的搜索查询。该节点通过一个结构化的提示,要求 LLM 同时完成“意图理解”和“关键词生成”两个任务,并将解析出的专用搜索关键词更新到状态的
- 然后我们将所有节点连接起来,构建图。
"""
智能搜索助手 - 基于 LangGraph + Tavily API 的真实搜索系统
1. 理解用户需求
2. 使用Tavily API真实搜索信息
3. 生成基于搜索结果的回答
"""
import asyncio
from typing import TypedDict, Annotated
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
import os
from dotenv import load_dotenv
from tavily import TavilyClient
# 加载环境变量
load_dotenv()
# 定义状态结构
class SearchState(TypedDict):
messages: Annotated[list, add_messages]
user_query: str # 用户查询
search_query: str # 优化后的搜索查询
search_results: str # Tavily搜索结果
final_answer: str # 最终答案
step: str # 当前步骤
# 初始化模型和Tavily客户端
llm = ChatOpenAI(
model=os.getenv("LLM_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("LLM_API_KEY"), # type: ignore
base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
temperature=0.7
)
# 初始化Tavily客户端
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
def understand_query_node(state: SearchState) -> SearchState:
"""步骤1:理解用户查询并生成搜索关键词"""
# 获取最新的用户消息
user_message = ""
for msg in reversed(state["messages"]):
if isinstance(msg, HumanMessage):
user_message = msg.content
break
understand_prompt = f"""分析用户的查询:"{user_message}"
请完成两个任务:
1. 简洁总结用户想要了解什么
2. 生成最适合搜索的关键词(中英文均可,要精准)
格式:
理解:[用户需求总结]
搜索词:[最佳搜索关键词]
"""
response = llm.invoke([SystemMessage(content=understand_prompt)]) # invoke方法同LangChain,会返回一个AIMessage对象,包含模型的回复内容
# 提取搜索关键词
response_text = response.content
search_query = user_message # 默认使用原始查询
if "搜索词:" in response_text:
search_query = response_text.split("搜索词:")[1].strip()
elif "搜索关键词:" in response_text:
search_query = response_text.split("搜索关键词:")[1].strip()
return {
"user_query": response.content,
"search_query": search_query,
"step": "understood",
"messages": [AIMessage(content=f"我理解您的需求:{response.content}")]
}
def tavily_search_node(state: SearchState) -> SearchState:
"""步骤2:使用Tavily API进行真实搜索"""
search_query = state["search_query"]
try:
print(f"🔍 正在搜索: {search_query}")
# 调用Tavily搜索API
response = tavily_client.search(
query=search_query,
search_depth="basic",
include_answer=True,
include_raw_content=False,
max_results=5
)
# 处理搜索结果
search_results = ""
# 优先使用Tavily的综合答案
if response.get("answer"):
search_results = f"综合答案:\n{response['answer']}\n\n"
# 添加具体的搜索结果
if response.get("results"):
search_results += "相关信息:\n"
for i, result in enumerate(response["results"][:3], 1):
title = result.get("title", "")
content = result.get("content", "")
url = result.get("url", "")
search_results += f"{i}. {title}\n{content}\n来源:{url}\n\n"
if not search_results:
search_results = "抱歉,没有找到相关信息。"
return {
"search_results": search_results,
"step": "searched",
"messages": [AIMessage(content=f"✅ 搜索完成!找到了相关信息,正在为您整理答案...")]
}
except Exception as e:
error_msg = f"搜索时发生错误: {str(e)}"
print(f"❌ {error_msg}")
return {
"search_results": f"搜索失败:{error_msg}",
"step": "search_failed",
"messages": [AIMessage(content="❌ 搜索遇到问题,我将基于已有知识为您回答")]
}
def generate_answer_node(state: SearchState) -> SearchState:
"""步骤3:基于搜索结果生成最终答案"""
# 检查是否有搜索结果
if state["step"] == "search_failed":
# 如果搜索失败,基于LLM知识回答
fallback_prompt = f"""搜索API暂时不可用,请基于您的知识回答用户的问题:
用户问题:{state['user_query']}
请提供一个有用的回答,并说明这是基于已有知识的回答。
"""
response = llm.invoke([SystemMessage(content=fallback_prompt)])
return {
"final_answer": response.content,
"step": "completed",
"messages": [AIMessage(content=response.content)]
}
# 基于搜索结果生成答案
answer_prompt = f"""基于以下搜索结果为用户提供完整、准确的答案:
用户问题:{state['user_query']}
搜索结果:
{state['search_results']}
请要求:
1. 综合搜索结果,提供准确、有用的回答
2. 如果是技术问题,提供具体的解决方案或代码
3. 引用重要信息的来源
4. 回答要结构清晰、易于理解
5. 如果搜索结果不够完整,请说明并提供补充建议
"""
response = llm.invoke([SystemMessage(content=answer_prompt)])
return {
"final_answer": response.content,
"step": "completed",
"messages": [AIMessage(content=response.content)]
}
# 构建搜索工作流
def create_search_assistant():
workflow = StateGraph(SearchState)
# 添加三个节点
workflow.add_node("understand", understand_query_node)
workflow.add_node("search", tavily_search_node)
workflow.add_node("answer", generate_answer_node)
# 设置线性流程
workflow.add_edge(START, "understand")
workflow.add_edge("understand", "search")
workflow.add_edge("search", "answer")
workflow.add_edge("answer", END)
# 编译图
memory = InMemorySaver()
app = workflow.compile(checkpointer=memory)
return app
async def main():
"""主函数:运行智能搜索助手"""
# 检查API密钥
if not os.getenv("TAVILY_API_KEY"):
print("❌ 错误:请在.env文件中配置TAVILY_API_KEY")
return
app = create_search_assistant()
print("🔍 智能搜索助手启动!")
print("我会使用Tavily API为您搜索最新、最准确的信息")
print("支持各种问题:新闻、技术、知识问答等")
print("(输入 'quit' 退出)\n")
session_count = 0
while True:
user_input = input("🤔 您想了解什么: ").strip()
if user_input.lower() in ['quit', 'q', '退出', 'exit']:
print("感谢使用!再见!👋")
break
if not user_input:
continue
session_count += 1
config = {"configurable": {"thread_id": f"search-session-{session_count}"}}
# 初始状态
initial_state = {
"messages": [HumanMessage(content=user_input)],
"user_query": "",
"search_query": "",
"search_results": "",
"final_answer": "",
"step": "start"
}
try:
print("\n" + "="*60)
# 执行工作流
async for output in app.astream(initial_state, config=config):
for node_name, node_output in output.items():
if "messages" in node_output and node_output["messages"]:
latest_message = node_output["messages"][-1]
if isinstance(latest_message, AIMessage):
if node_name == "understand":
print(f"🧠 理解阶段: {latest_message.content}")
elif node_name == "search":
print(f"🔍 搜索阶段: {latest_message.content}")
elif node_name == "answer":
print(f"\n💡 最终回答:\n{latest_message.content}")
print("\n" + "="*60 + "\n")
except Exception as e:
print(f"❌ 发生错误: {e}")
print("请重新输入您的问题。\n")
if __name__ == "__main__":
asyncio.run(main())
PS D:\Microsoft VS Code\vs work\codeworkvs\py\myother\agent\project> python .\demo.py
🔍 智能搜索助手启动!
我会使用Tavily API为您搜索最新、最准确的信息
支持各种问题:新闻、技术、知识问答等
(输入 'quit' 退出)
🤔 您想了解什么: 下周我要去青岛,天气怎么样?有合适的景点吗?
============================================================
🧠 理解阶段: 我理解您的需求:理解:用户计划下周去青岛旅行,需要了解当地天气情况以及推荐合适的旅游景点。
搜索词:青岛下周天气预报;青岛旅游景点推荐
🔍 正在搜索: 青岛下周天气预报;青岛旅游景点推荐
🔍 搜索阶段: ✅ 搜索完成!找到了相关信息,正在为您整理答案...
💡 最终回答:
根据您“下周去青岛旅行”的计划,我为您综合整理了当地的天气预报和旅游景点推荐,以帮助您更好地准备行程。
### **一、天气与着装建议**
- **天气预报摘要**:根据搜索结果,青岛**下周预计为多云天气,气温在20-24摄氏度之间**,气候总体温和舒适。
- **着装建议**:鉴于此温度范围和青岛秋季(9-11月)气候宜人、偶有微风的特点,**建议您穿着舒适的长袖衬衫、薄外套或风衣**。由于沿海城市可能湿度较高且早晚温差存在,携带一件方便穿脱的外套是明智之选。
- **气候参考**:青岛秋季(9-11月)平均气温在17.2-23.6°C,雨量减少,阳光明媚,非常适合户外活动。
**请注意**:以上天气预报综合自搜索结果中的信息。为确保准确,请您在出发前通过**中国天气网(weather.com.cn)** 或手机天气应用查询最新的**青岛未来7天具体天气预报**。
### **二、旅游景点推荐**
青岛作为“海滨明珠”,集山、海、城、湾于一体。以下为您分类推荐必游景点:
#### **1. 经典必游(文化与地标)**
- **栈桥**:青岛的象征和标志,是感受城市历史与海景的绝佳起点。
- **青岛啤酒博物馆**:深入了解享誉世界的青岛啤酒历史与酿造工艺,并可品尝新鲜啤酒,是青岛独特的文化体验。
#### **2. 海滨风光(休闲与漫步)**
- **石老人海水浴场**:著名的海滩,沙质细腻,适合踏浪、散步和观海。
- **小青岛公园 & 鲁迅公园**:优美的海滨公园,适合漫步、拍照,欣赏海岸线风光。
- **八大关风景区**:以各国风格别墅闻名的街区,绿树成荫,尤其秋季景色极佳。
#### **3. 文化与历史体验**
- **历史德式建筑群**:漫步老城区,观赏保存完好的德式建筑,感受独特的欧陆风情。
- **天主教堂(圣弥厄尔大教堂)**:典型的哥特式建筑,是拍照和了解宗教文化的好去处。
#### **4. 自然与风光**
- **崂山风景区**:道教名山,拥有壮丽的山海风光和道教庙宇,适合半日或一日游,是登山爱好者的首选。
### **三、补充建议与节庆活动**
- **最佳旅行季节**:资料显示,**8-11月**是青岛的最佳旅行时间。秋季气候宜人,海鲜肥美,且能赶上热闹的节庆。
- **特色节庆(如时间契合)**:
- **青岛国际啤酒节**:通常在8月第二个周末开始,为期两周,是盛大的狂欢派对。
- **秋季海鲜季**:此时各类海鲜上市,是品尝美食的好时机。
- **实用贴士**:建议穿着舒适的鞋子以便步行。市内酒店、餐厅普遍有Wi-Fi覆盖,出行交通便利。
**总结**:下周您前往青岛旅行,天气以多云温和为主,适合户外活动。请务必带一件薄外套。行程上,**栈桥、青岛啤酒博物馆、石老人海滩和八大关**是经典组合,若时间充裕,**崂山**也值得一游。
(信息综合自KKday青岛旅游指南及天气网相关资料)
如上demo实现的是一个“固定工作流”,即用户只是问“你好”“解释一下 RAG 是什么”或“计算 123 × 456”,程序仍然会执行 Tavily 搜索。这里虽然使用了 LangGraph,但工具调用时机完全由开发者预先规定。
可以将它改成一个 Agentic Tool-Calling Loop:开发者只提供可用工具,模型自行判断是否调用工具、调用哪个工具、使用什么参数,以及是否需要连续调用多次工具。
- LLM 节点判断是否发起工具调用;
- 条件边根据模型输出决定进入工具节点或结束;
- 工具执行完成后,再回到 LLM 节点继续判断。
模型并不能直接执行 Python 函数。实际过程分为两步:
llm.bind_tools(tools)将工具名称、参数结构和说明提供给模型。- 模型返回一个结构化的
tool_call
随后,ToolNode 根据工具名称找到对应 Python 函数并执行。ToolNode 是 LangGraph 提供的预构建节点,能够处理工具执行、多个工具调用、错误处理和状态注入。
"""
智能搜索 Agent - 基于 LangGraph + Tavily API
Agent 可以自行决定:
1. 是否需要调用工具
2. 调用哪个工具
3. 工具参数是什么
4. 是否需要连续调用多个工具
5. 什么时候结束并直接回答用户
"""
import ast
import operator
import os
from datetime import datetime
from zoneinfo import ZoneInfo
from typing import Any
from dotenv import load_dotenv
from tavily import TavilyClient
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver
# ============================================================
# 1. 初始化
# ============================================================
load_dotenv()
llm = ChatOpenAI(
model=os.getenv("LLM_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
# 工具选择任务通常使用较低温度,使行为更稳定
temperature=0,
)
tavily_client = TavilyClient(
api_key=os.getenv("TAVILY_API_KEY")
)
# ============================================================
# 2. 定义工具
# ============================================================
@tool
def tavily_search(query: str) -> str:
"""
搜索互联网中的最新信息和外部资料。
适用于:
- 新闻、热点、最新技术进展
- 软件版本、产品信息、政策、价格等可能变化的信息
- 用户明确要求查询、搜索或核实的信息
- 需要引用外部来源的信息
不适用于:
- 简单寒暄
- 基础算术
- 可以直接回答的稳定知识
Args:
query: 精确、适合搜索引擎使用的搜索关键词。
"""
response = tavily_client.search(
query=query,
search_depth="basic",
include_answer=True,
include_raw_content=False,
max_results=5,
)
sections: list[str] = []
if response.get("answer"):
sections.append(
f"搜索摘要:\n{response['answer']}"
)
results = response.get("results", [])
if results:
formatted_results = ["搜索来源:"]
for index, result in enumerate(results, start=1):
title = result.get("title", "无标题")
content = result.get("content", "")
url = result.get("url", "")
formatted_results.append(
f"{index}. {title}\n"
f"{content}\n"
f"URL: {url}"
)
sections.append("\n\n".join(formatted_results))
if not sections:
return "没有检索到有效结果。"
return "\n\n".join(sections)
_ALLOWED_BINARY_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
}
_ALLOWED_UNARY_OPERATORS = {
ast.UAdd: operator.pos,
ast.USub: operator.neg,
}
def _evaluate_expression(node: ast.AST) -> int | float:
"""安全地解析基础算术表达式,避免直接使用 eval。"""
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
return node.value
raise ValueError("表达式中只能出现数字。")
if isinstance(node, ast.BinOp):
operator_type = type(node.op)
if operator_type not in _ALLOWED_BINARY_OPERATORS:
raise ValueError("包含不支持的运算符。")
left = _evaluate_expression(node.left)
right = _evaluate_expression(node.right)
# 防止模型生成过大的指数运算
if isinstance(node.op, ast.Pow) and abs(right) > 20:
raise ValueError("指数过大。")
return _ALLOWED_BINARY_OPERATORS[operator_type](left, right)
if isinstance(node, ast.UnaryOp):
operator_type = type(node.op)
if operator_type not in _ALLOWED_UNARY_OPERATORS:
raise ValueError("包含不支持的一元运算符。")
value = _evaluate_expression(node.operand)
return _ALLOWED_UNARY_OPERATORS[operator_type](value)
raise ValueError("表达式格式不受支持。")
@tool
def calculator(expression: str) -> str:
"""
计算基础算术表达式。
适用于:
- 加减乘除
- 取模
- 幂运算
- 带括号的算术表达式
Args:
expression: 算术表达式,例如 "(125 + 75) * 3"。
"""
try:
parsed = ast.parse(expression, mode="eval")
result = _evaluate_expression(parsed.body)
return f"计算结果:{result}"
except Exception as exc:
return f"计算失败:{exc}"
@tool
def get_current_time(timezone_name: str = "Asia/Singapore") -> str:
"""
获取指定时区的当前日期和时间。
适用于:
- 查询当前时间
- 查询今天的日期
- 回答涉及“今天”“现在”等相对时间的问题
Args:
timezone_name: IANA 时区名称,例如 Asia/Shanghai、Asia/Singapore。
"""
try:
current_time = datetime.now(ZoneInfo(timezone_name))
return (
f"时区:{timezone_name}\n"
f"当前时间:{current_time.strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
return (
"无法识别该时区。请使用 IANA 时区名称,"
"例如 Asia/Shanghai 或 Asia/Singapore。"
)
tools = [
tavily_search,
calculator,
get_current_time,
]
# bind_tools 只是将工具结构提供给模型。
# 模型会自行判断是否生成 tool_call。
llm_with_tools = llm.bind_tools(tools)
# ============================================================
# 3. 定义 Agent 节点
# ============================================================
SYSTEM_PROMPT = """
你是一名智能搜索助手。
你可以自行判断是否调用工具。请遵循以下规则:
1. 用户询问新闻、最新进展、软件版本、政策、产品信息、
实时数据或明确要求核实信息时,优先调用 tavily_search。
2. 用户提出算术问题时,调用 calculator。
3. 用户询问当前日期、时间或涉及时区时,调用 get_current_time。
4. 用户提出基础知识问题、解释性问题或简单寒暄时,
可以直接回答,不要为了调用工具而调用工具。
5. 一次工具调用不足以回答问题时,可以继续调用工具。
6. 使用 tavily_search 时,请自行将用户问题改写为精确的搜索关键词。
7. 搜索结果包含 URL 时,在最终回答中列出关键来源。
8. 搜索结果不充分时,请明确说明信息不足,不要编造事实。
"""
def agent_node(state: MessagesState) -> dict[str, list[AIMessage]]:
"""
Agent 节点:
- 阅读历史消息
- 自行决定直接回答或调用工具
"""
response = llm_with_tools.invoke(
[
SystemMessage(content=SYSTEM_PROMPT),
*state["messages"],
]
)
return {
"messages": [response]
}
# ============================================================
# 4. 构建动态工具调用图
# ============================================================
def create_agent():
workflow = StateGraph(MessagesState) # MessagesState 是 LangGraph 为聊天模型场景提供的预构建状态,它包含一个 messages 键,并使用 add_messages reducer 合并新消息。
# LLM 节点:负责思考和选择工具
workflow.add_node("agent", agent_node)
# 工具节点:负责执行模型选中的工具
workflow.add_node("tools", ToolNode(tools))
workflow.add_edge(START, "agent")
# tools_condition 会检查 agent 返回的最后一条 AIMessage:
# - 存在 tool_calls:进入 tools
# - 不存在 tool_calls:进入 END
workflow.add_conditional_edges(
"agent",
tools_condition,
{
"tools": "tools",
END: END,
},
)
# 工具执行完成后回到 agent。
# Agent 可以决定继续调用工具,也可以输出最终回答。
workflow.add_edge("tools", "agent")
memory = InMemorySaver()
return workflow.compile(
checkpointer=memory
)
# ============================================================
# 5. 交互式运行
# ============================================================
async def main():
if not os.getenv("LLM_API_KEY"):
print("❌ 错误:请在 .env 文件中配置 LLM_API_KEY")
return
if not os.getenv("TAVILY_API_KEY"):
print("❌ 错误:请在 .env 文件中配置 TAVILY_API_KEY")
return
app = create_agent()
print("🤖 LangGraph 智能搜索 Agent 已启动!")
print("Agent 会自行判断是否需要使用工具。")
print("输入 'quit' 退出。\n")
# 使用同一个 thread_id,保留同一轮对话中的上下文。
config = {
"configurable": {
"thread_id": "search-session-1"
}
}
while True:
user_input = input("🤔 您想了解什么:").strip()
if user_input.lower() in ["quit", "q", "退出", "exit"]:
print("感谢使用!再见!👋")
break
if not user_input:
continue
print("\n" + "=" * 60)
try:
async for update in app.astream(
{
"messages": [
HumanMessage(content=user_input)
]
},
config=config,
stream_mode="updates",
):
for node_name, node_output in update.items():
messages = node_output.get("messages", [])
if not messages:
continue
latest_message = messages[-1]
if node_name == "agent":
if isinstance(latest_message, AIMessage):
if latest_message.tool_calls:
for tool_call in latest_message.tool_calls:
print(
"🛠️ Agent 选择工具:"
f"{tool_call['name']}\n"
f" 参数:{tool_call['args']}"
)
elif latest_message.content:
print(
"\n💡 最终回答:\n"
f"{latest_message.content}"
)
elif node_name == "tools":
print("✅ 工具执行完成,Agent 正在继续分析。")
except Exception as exc:
print(f"❌ 发生错误:{exc}")
print("=" * 60 + "\n")
if __name__ == "__main__":
import asyncio
asyncio.run(main())
PS D:\Microsoft VS Code\vs work\codeworkvs\py\myother\agent\project> python .\demo.py
🤖 LangGraph 智能搜索 Agent 已启动!
Agent 会自行判断是否需要使用工具。
输入 'quit' 退出。
🤔 您想了解什么:下周我要去青岛,天气怎么样?有合适的景点吗?
============================================================
🛠️ Agent 选择工具:get_current_time
参数:{'timezone_name': 'Asia/Shanghai'}
✅ 工具执行完成,Agent 正在继续分析。
🛠️ Agent 选择工具:tavily_search
参数:{'query': '青岛 2026年6月16日 天气预报 下周'}
✅ 工具执行完成,Agent 正在继续分析。
🛠️ Agent 选择工具:tavily_search
参数:{'query': '青岛 旅游景点 推荐 必去 景点 2026'}
✅ 工具执行完成,Agent 正在继续分析。
💡 最终回答:
根据搜索到的信息,我来为您整理一下下周(6月16日-22日)青岛的天气和旅游景点建议:
## 🌤️ 天气情况
- **6月16日**:气温17°C~29°C,以多云天气为主
- **整体趋势**:青岛6月平均气温约20.4°C,最高温通常在22-24°C,最低温17-19°C
- **降水**:6月降水量约87mm,无明显持续性降水
- **水温**:海水温度约19°C,适合海边活动
- **建议**:早晚温差较大,建议带薄外套;多云天气适合户外活动
## 🏖️ 推荐景点
根据2026年最新旅游攻略,青岛必去景点包括:
### 经典地标
1. **栈桥**:青岛百年地标,红色八角亭“回澜阁”是青岛啤酒商标的原型
2. **圣弥厄尔教堂**:德国文艺复兴风格天主教堂,奶油色花岗岩外墙搭配橘红色尖顶
3. **八大关**:万国建筑博览馆,百栋风格各异的老建筑,四季景色不同
### 自然风光
4. **崂山**:“海上第一名山”,海拔1132.7米,道教圣地,可游览“九水十八潭”
5. **小鱼山公园**:海拔60米的最佳观景台,可一览红瓦碧海的经典青岛风貌
6. **第二海水浴场**:沙细浪小人少,与八大关相邻,适合散步拍照
### 文化体验
7. **青岛啤酒博物馆**:了解啤酒百年历史,品尝原浆啤酒(门票60元)
8. **德国总督楼旧址博物馆**:百年德式古堡,红蓝绿三色屋顶极具特色
9. **五四广场**:现代城市地标,30米高“五月的风”雕塑夜晚灯光壮观
### 小众打卡
10. **宫崎骏漫画街**:老城小巷改造,充满吉卜力风格
11. **银鱼巷**:百年老建筑改造的潮流艺术街区
12. **李慰農公園**:年轻人拍照热点,树影与海景同框
## 💡 游玩建议
- **行程参考**:可规划4-5天,重点游览栈桥-德国总督楼-小鱼山-天主教堂-啤酒博物馆-八大关-第二海水浴场
- **崂山游览**:景区较大,建议只选1-2个片区深度游,可乘观光车节省体力
- **最佳时间**:傍晚前游览小鱼山或栈桥,可欣赏金色日落光线
- **注意事项**:6月是旅游旺季,建议提前预约崂山等热门景点
## 📌 重要提醒
以上信息来自互联网搜索,天气预报和景点开放情况可能有变化。建议您:
1. 出发前3-5天再次查询最新天气预报
2. 提前预约崂山、啤酒博物馆等热门景点
3. 关注当地旅游公众号获取实时信息
祝您青岛之旅愉快!🌊
============================================================
🤔 您想了解什么:你好。
============================================================
💡 最终回答:
你好!很高兴为您服务。请问有什么我可以帮助您的吗?无论是查询信息、解决问题还是需要推荐,我都很乐意为您提供帮助。😊
============================================================
🤔 您想了解什么:计算321*(1.23+6)
============================================================
🛠️ Agent 选择工具:calculator
参数:{'expression': '321 * (1.23 + 6)'}
✅ 工具执行完成,Agent 正在继续分析。
💡 最终回答:
计算结果是:**2320.83**
计算过程:
- 先计算括号内:1.23 + 6 = 7.23
- 再计算乘法:321 × 7.23 = 2320.83
如果您有其他计算需求或问题,请随时告诉我!
很明显,对于同样的“旅游攻略”问题,这一版的搜索更加精准;对于数学计算问题,以下是关于模型流程的解读:
实际上经历了两轮 LLM 调用。
第一次进入 agent_node,模型读取用户问题,判断这是算术任务,于是返回一个 tool_call:
{
"name": "calculator",
"args": {
"expression": "321 * (1.23 + 6)"
}
}
因此,在控制台中看到了:
🛠️ Agent 选择工具:calculator
参数:{'expression': '321 * (1.23 + 6)'}
此时模型还没有给出最终回答,只是提出:请帮我调用 calculator 工具,并将这个表达式传进去。
ToolNode 执行计算器工具。ToolNode 收到工具调用后,会自动找到:
@tool
def calculator(expression: str) -> str:
然后执行,最后,工具只返回一行文本return f"计算结果:{result}"。
工具执行完成后,第二次进入 agent_node,因为边workflow.add_edge("tools", "agent"),此时,messages 中大致保存了以下内容:
[
HumanMessage(
content="计算321*(1.23+6)"
),
AIMessage(
content="",
tool_calls=[
{
"name": "calculator",
"args": {
"expression": "321 * (1.23 + 6)"
}
}
]
),
ToolMessage(
content="计算结果:2320.83"
)
]
模型第二次看到这些信息后,会根据工具结果生成面向用户的自然语言回答:
计算结果是:2320.83
计算过程:
- 先计算括号内:1.23 + 6 = 7.23
- 再计算乘法:321 × 7.23 = 2320.83
所以,这两行过程是 LLM 根据原始表达式和工具返回值自行推导并补充的说明。
完整执行流程:
用户输入:
计算321*(1.23+6)
↓
第一次调用 agent_node
模型决定调用 calculator
↓
AIMessage.tool_calls:
calculator("321 * (1.23 + 6)")
↓
ToolNode 执行 calculator
↓
calculator 返回:
"计算结果:2320.83"
↓
ToolNode 将结果包装为 ToolMessage
↓
第二次调用 agent_node
模型读取原始问题和工具结果
↓
模型生成自然语言回答:
结果 + 计算步骤 + 客套话
↓
没有新的 tool_calls
↓
tools_condition 路由到 END
LangGraph的优缺点
(1)优势
- 如我们的智能搜索助手案例所示,LangGraph 将一个完整的实时问答流程,显式地定义为一个由状态、节点和边构成的“流程图”。这种设计的最大优势是高度的可控性与可预测性。开发者可以精确地规划智能体的每一步行为,这对于构建需要高可靠性和可审计性的生产级应用至关重要。其最强大的特性在于对循环(Cycles)的原生支持。通过条件边,我们可以轻松构建“反思-修正”循环,例如在我们的案例中,如果搜索失败,可以设计一个回退到备用方案的路径。这是构建能够自我优化和具备容错能力的智能体的关键。
- 此外,由于每个节点都是一个独立的 Python 函数,这带来了高度的模块化。同时,在流程中插入一个等待人类审核的节点也变得非常直接,为实现可靠的“人机协作”(Human-in-the-loop)提供了坚实的基础。
(2)局限性
- 与基于对话的框架相比,LangGraph 需要开发者编写更多的前期代码(Boilerplate)。定义状态、节点、边等一系列操作,使得对于简单任务而言,开发过程显得更为繁琐。开发者需要更多地思考“如何控制流程(how)”,而不仅仅是“做什么(what)”。由于工作流是预先定义的,LangGraph 的行为虽然可控,但也缺少了对话式智能体那种动态的、“涌现”式的交互。它的强项在于执行一个确定的、可靠的流程,而非模拟开放式的、不可预测的社会性协作。
- 调试过程同样存在挑战。虽然流程比对话历史更清晰,但问题可能出在多个环节:某个节点内部的逻辑错误、在节点间传递的状态数据发生异变,或是边跳转的条件判断失误。这要求开发者对整个图的运行机制有全局性的理解。
3.1.智能体性能评估
在构建智能体系统时,我们还需要解决一个核心问题:如何客观地评估智能体的性能? 具体来说,我们需要回答以下问题:
- 智能体是否具备预期的能力?
- 在不同任务上的表现如何?
- 与其他智能体相比处于什么水平?
3.1.1.为何需要评估以及评估benchmark
当我们优化提示词或更换 LLM 模型后,如何知道是否真的有改进?在部署到生产环境前,如何保证智能体的可靠性?这些问题都需要通过系统化的评估来解决。
智能体评估的核心价值在于提供标准化的方法来衡量智能体的能力。通过评估,我们可以用具体的数字指标量化智能体的表现,客观比较不同设计方案的优劣,及时发现智能体在特定场景下的弱点,并向用户证明智能体的可靠性。
与传统软件测试不同,智能体评估面临着独特的挑战。首先是输出的不确定性,同一问题可能有多个正确答案,很难用简单的对错来判断。其次是评估标准的多样性,不同任务需要不同的评估方法,工具调用需要检查函数签名,问答任务需要评估语义相似度。最后是评估成本的高昂,每次评估都需要大量的 API 调用,成本可能达到数百元甚至更多。
为了应对这些挑战,学术界和工业界提出了多个标准化的评估基准(Benchmark)。这些基准提供了统一的数据集、评估指标和评分方法,使我们能够在相同的标准下评估和对比不同的智能体系统。下面介绍一些主流的评估基准和指标:
(1)工具调用能力评估
工具调用是智能体的核心能力之一。智能体需要理解用户意图,选择合适的工具,并正确构造函数调用。相关的评估基准包括:
- BFCL (Berkeley Function Calling Leaderboard):UC Berkeley 推出,包含 1120+测试样本,涵盖 simple、multiple、parallel、irrelevance 四个类别,使用 AST 匹配算法评估,数据集规模适中,社区活跃。
- AST:Abstract Syntax Tree,抽象语法树。它主要用来判断模型生成的函数调用是否和标准答案“结构一致”,而不是只看字符串一不一样。(如json的字段、函数名、参数值是否对应。允许参数和函数调用顺序不同,但是相同键下的值必须匹配。)
- ToolBench:清华大学推出,包含 16000+真实 API 调用场景,覆盖真实世界的复杂工具使用场景。
- API-Bank:Microsoft Research 推出,包含 53 个常用 API 工具,专注于评估智能体对 API 文档的理解和调用能力。
(2)通用能力评估
评估智能体在真实世界任务中的综合表现,包括多步推理、知识运用、多模态理解等能力:
- GAIA (General AI Assistants):Meta AI 和 Hugging Face 联合推出,包含 466 个真实世界问题,分为 Level 1/2/3 三个难度级别,评估多步推理、工具使用、文件处理、网页浏览等能力,使用准精确匹配(Quasi Exact Match)算法,任务真实且综合性强。
- 准精确匹配:去掉无关的格式差异(如大小写、多余空格、标点符号、数字格式等),但仍然不是语义打分。它比字符串精确匹配宽松一点,但仍然要求最终答案本质上完全正确。
- AgentBench:清华大学推出,包含 8 个不同领域的任务,全面评估智能体的通用能力。
- WebArena:CMU 推出,评估智能体在真实网页环境中的任务完成能力和网页交互能力。
(3)多智能体协作评估
评估多个智能体协同工作的能力:
- ChatEval:评估多智能体对话系统的质量。
- SOTOPIA:评估智能体在社交场景中的互动能力。
- 自定义协作场景:根据具体应用场景设计的评估任务。
(4)常用评估指标
不同基准使用不同的评估指标,常见的包括:
- 准确性指标:Accuracy(准确率)、Exact Match(精确匹配)、F1 Score(F1 分数),用于衡量答案的正确性。
- 效率指标:Response Time(响应时间)、Token Usage(Token 使用量),用于衡量执行效率。
- 鲁棒性指标:Error Rate(错误率)、Failure Recovery(故障恢复),用于衡量容错能力。
- 协作指标:Communication Efficiency(通信效率)、Task Completion(任务完成度),用于衡量协作效果。
Hello-Agent教程主要介绍以下三个场景:
3.1.2.BFCL:工具调用能力评估
BFCL 基准包含四个评估类别,难度递增。从最基础的单函数调用(Simple)开始,逐步增加到需要调用多个函数的场景(Multiple),再到需要并行调用多个函数的复杂场景(Parallel),最后是需要判断是否需要调用函数的场景(Irrelevance)。这四个类别覆盖了智能体在实际应用中可能遇到的各种工具调用场景:
BFCL 的评估流程遵循标准的基准测试流程:首先加载数据集并选择评估类别,然后运行智能体获取预测结果,接着将预测结果解析为抽象语法树(AST),最后通过 AST 匹配算法判断预测是否正确。整个流程会遍历所有测试样本,最终计算出准确率等评估指标并生成评估报告。完整的评估流程如图所示:
BFCL 数据集采用 JSON 格式,每个测试样本包含以下字段:
{
"id": "simple_001",
"question": "What's the weather like in Beijing today?",
"function": [
{
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city name"
}
},
"required": ["location"]
}
}
],
"ground_truth": [
{
"name": "get_weather",
"arguments": {
"location": "Beijing"
}
}
]
}
BFCL 提供官方 CLI 工具进行评估:
# 安装BFCL评估工具
pip install bfcl
# 运行官方评估
bfcl evaluate \
--model-result-path ./results.json \
--test-category simple_python
BFCL 数据集可以从官方 GitHub 仓库克隆获取完整的数据集和 ground truth:
# 克隆BFCL仓库
git clone https://github.com/ShishirPatil/gorilla.git temp_gorilla
cd temp_gorilla/berkeley-function-call-leaderboard
# 查看BFCL v4数据集
ls bfcl_eval/data/
# 输出: BFCL_v4_simple_python.json BFCL_v4_multiple.json BFCL_v4_parallel.json ...
# 查看ground truth
ls bfcl_eval/data/possible_answer/
# 输出: BFCL_v4_simple_python.json BFCL_v4_multiple.json ...
3.1.3.GAIA:通用AI能力评估
与 BFCL 专注于工具调用不同,GAIA 评估的是智能体在真实世界任务中的综合表现,包括:
- 多步推理:将复杂问题分解为多个子问题
- 知识运用:利用内置知识和外部知识库
- 多模态理解:处理文本、图片、文件等多种输入
- 网页浏览:从互联网获取最新信息
- 文件操作:读取和处理各种格式的文件
GAIA 包含 466 个精心设计的真实世界问题,这些问题按照复杂度和所需推理步骤分为三个难度级别,从简单的零步推理任务到需要多步复杂推理的困难任务,全面覆盖了智能体在实际应用中可能遇到的各种场景,样例:
{
"task_id": "gaia_001",
"Question": "What is the total population of the top 3 most populous cities in California?",
"Level": 2,
"Final answer": "12847521",
"file_name": "",
"file_path": "",
"Annotator Metadata": {
"Steps": [
"Search for most populous cities in California",
"Get population data for top 3 cities",
"Sum the populations"
],
"Number of steps": 3,
"How long did this take?": "5 minutes",
"Tools": ["web_search", "calculator"]
}
}
GAIA 要求使用特定的系统提示词,确保模型输出符合评估格式:
GAIA_SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."""
GAIA 是受限数据集(Gated Dataset),需要先在 HuggingFace 上申请访问权限。
步骤 1:申请访问权限
- 访问 https://huggingface.co/datasets/gaia-benchmark/GAIA
- 点击"Request access"按钮
- 填写申请表单(通常会在几秒内批准)
- 获取 HuggingFace Token:https://huggingface.co/settings/tokens
步骤 2:配置环境变量,在.env文件中添加 HuggingFace Token。然后手动下载数据集:
from huggingface_hub import snapshot_download
import os
# 设置Token
os.environ["HF_TOKEN"] = "hf_your_token_here"
# 下载数据集
snapshot_download(
repo_id="gaia-benchmark/GAIA",
repo_type="dataset",
local_dir="./data/gaia",
token=os.getenv("HF_TOKEN")
)
# 查看数据集统计
stats = dataset.get_statistics()
print(f"总样本数: {stats['total_samples']}")
print(f"级别分布: {stats['level_distribution']}")
# 输出:
# 总样本数: 165
# 级别分布: {1: 53, 2: 62, 3: 50}
3.1.4.数据生成能力评估
Hello-Agent以 AIME(美国数学邀请赛)风格的数学题目生成为例。
- AIME 题目具有鲜明的特点:每道题的答案都是 0 到 999 之间的整数,题目涵盖代数、几何、数论、组合、概率等多个数学领域,需要多步推理但不涉及高深理论,难度适中(相当于 AIME 第 6-9 题的水平)。这些特点使得 AIME 题目成为评估数学题目生成质量的理想基准:答案格式统一便于自动化评估,题目难度适中适合大规模生成。
- 使用 HuggingFace 上的
TianHongZXY/aime-1983-2025数据集作为参考,该数据集包含从 1983 年到 2025 年的 900 多道 AIME 真题。
在数据生成质量评估中,我们采用三种互补的评估方法:LLM Judge、Win Rate 和人工打分。选择这三种方法有两个重要原因。首先,从方法论角度来看,这些是当前智能体领域常用的自动化测评方案,也是许多学术论文中的主流做法,具有广泛的认可度和实践基础。其次,从适用性角度来看,这三种方法天然适合我们的评估场景:LLM Judge 和 Win Rate 用于评估题目生成质量(从正确性、清晰度、难度匹配等维度进行多维度评估),而人工打分用于评估答案生成质量(通过人类专家验证答案的准确性),这种分工非常合理且易于理解。
具体评估demo代码就不在这里展示了,Hello-Agent教程链接。本学习博客主要介绍评估方法的概念。
- LLM Judge 评估:在数据生成质量评估中,我们需要对大量生成的题目进行快速、一致的质量评估。传统的人工评估虽然准确,但成本高、效率低,难以应对大规模数据生成的需求。LLM Judge 通过使用大语言模型作为评委,可以自动化地从多个维度评估生成数据的质量,不仅大幅提升评估效率,还能保持评估标准的一致性。更重要的是,LLM Judge 可以提供详细的评分理由和改进建议,帮助我们理解生成数据的优缺点,为后续优化提供方向。
- 自行定义评估维度和评估指标,接入一个大模型让它输出分数。
- Win Rate 评估:虽然 LLM Judge 可以提供多维度的绝对评分,但我们还需要一个相对评估指标来衡量生成题目与真题的质量差距。Win Rate 评估通过成对对比的方式,让 LLM 直接判断生成题目和真题哪个更好,这种相对比较比绝对评分更符合人类的判断习惯,也更容易发现生成题目的相对优势和劣势。理想情况下,如果生成题目的质量接近真题,Win Rate 应该在 50%左右(即生成题目和真题各有 50%的胜率)。这个指标简单直观,可以快速判断生成系统的整体质量水平。
- 在成对对比评估中,每次比较会产生三种可能的结果:生成题目获胜(Win)、真题获胜(Loss)或平局(Tie)。我们通过统计这三种结果的比例来评估生成题目的质量。理想结果:Win Rate ≈ 50%(说明生成质量接近真题)。如果 Win Rate 显著低于 50%,说明生成题目质量不如真题,需要优化生成策略;如果 Win Rate 显著高于 50%,可能说明生成题目在某些方面超越了真题,或者评估标准存在偏差。
- 人工验证:尽管 LLM Judge 和 Win Rate 可以自动化评估题目质量,但对于数学题目这种需要严格逻辑推理的内容,人工验证仍然是不可或缺的。特别是在评估答案生成质量时,需要人类专家验证答案的准确性、解答步骤的完整性和数学推理的严密性。此外,人工验证还可以发现自动化评估可能遗漏的问题,如题目的创新性、趣味性等主观因素。
3.1.5.Agentic RAG要评估什么?
最基础的是要判断是否是正确回答,若回答错误,要判断是因为:
- 正确 chunk 压根就没有;
- 还是正确 chunk 没被找到;
- 还是正确 chunk 被找到后融入回答被幻觉替代了。
更多推荐
所有评论(0)