大模型应用开发--Agent学习笔记(定义、范式、低代码平台、框架、记忆、上下文工程、通信协议、强化学习、Skills、Harness、Loop Engineering、评估)
写在前面
本文是我的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.记忆与检索
如果智能体无法记住之前的交互内容,也无法从历史经验中学习,那么在连续对话或复杂任务中,其表现将受到极大限制。本节将介绍两个核心能力:记忆系统(Memory System)和检索增强生成(Retrieval-Augmented Generation, RAG)。
3.1.1.为何Agent需要记忆
人类记忆是一个多层级的认知系统,它不仅能存储信息,还能根据重要性、时间和上下文对信息进行分类和整理。
人类记忆可以分为以下几个层次:
- 感觉记忆(Sensory Memory):持续时间极短(0.5-3秒),容量巨大,负责暂时保存感官接收到的所有信息
- 工作记忆(Working Memory):持续时间短(15-30秒),容量有限(7±2个项目),负责当前任务的信息处理
- 长期记忆(Long-term Memory):持续时间长(可达终生),容量几乎无限,进一步分为:
- 程序性记忆:技能和习惯(如骑自行车)
- 陈述性记忆:可以用语言表达的知识,又分为:
- 语义记忆:一般知识和概念(如"巴黎是法国首都")
- 情景记忆:个人经历和事件(如"昨天的会议内容")
一个真正智能的智能体也需要具备记忆能力。对于基于LLM的智能体而言,通常面临两个根本性局限:对话状态的遗忘和内置知识的局限。
(1)局限一:无状态导致的对话遗忘
当前的大语言模型虽然强大,但设计上是无状态的。这意味着,每一次用户请求(或API调用)都是一次独立的、无关联的计算。模型本身不会自动“记住”上一次对话的内容。这带来了几个问题:
- 上下文丢失:在长对话中,早期的重要信息可能会因为上下文窗口限制而丢失
- 个性化缺失:Agent无法记住用户的偏好、习惯或特定需求
- 学习能力受限:无法从过往的成功或失败经验中学习改进
- 一致性问题:在多轮对话中可能出现前后矛盾的回答
要解决这个问题,我们的框架需要引入记忆系统。
(2)局限二:模型内置知识的局限性
除了遗忘对话历史,LLM 的另一个核心局限在于其知识是静态的、有限的。这些知识完全来自于它的训练数据,并因此带来一系列问题:
- 知识时效性:大模型的训练数据有时间截止点,无法获取最新信息
- 专业领域知识:通用模型在特定领域的深度知识可能不足
- 事实准确性:通过检索验证,减少模型的幻觉问题
- 可解释性:提供信息来源,增强回答的可信度
为了克服这一局限,RAG技术应运而生。
在实现上,我们可以将记忆和RAG设计为两个独立的工具:memory_tool负责存储和维护对话过程中的交互信息,rag_tool则负责从用户提供的知识库中检索相关信息作为上下文,并可将重要的检索结果自动存储到记忆系统中。
3.1.2.记忆系统
(这一部分的代码完全来自Hello-Agent教程文档中所展示的,并不是完整的记忆实现,只是核心流程的演示。)
我们需要先定义记忆系统的工作流程。根据认知科学的研究,人类记忆的形成经历以下几个阶段:
- 编码(Encoding):将感知到的信息转换为可存储的形式
- 存储(Storage):将编码后的信息保存在记忆系统中
- 检索(Retrieval):根据需要从记忆中提取相关信息
- 整合(Consolidation):将短期记忆转化为长期记忆
- 遗忘(Forgetting):删除不重要或过时的信息
基于该启发,记忆系统的核心思想是模仿人类大脑处理不同类型信息的方式,将记忆划分为多个专门的模块,并建立一套智能化的管理机制。可以是如下图所示的结构:
该结构由四种不同类型的记忆模块构成,每种模块都针对特定的应用场景和生命周期进行了优化:
- 首先是工作记忆 (Working Memory),它扮演着智能体“短期记忆”的角色,主要用于存储当前对话的上下文信息。为确保高速访问和响应,其容量被有意限制(例如,默认50条),并且生命周期与单个会话绑定,会话结束后便会自动清理。
- 其次是情景记忆 (Episodic Memory),它负责长期存储具体的交互事件和智能体的学习经历。与工作记忆不同,情景记忆包含了丰富的上下文信息,并支持按时间序列或主题进行回顾式检索,是智能体“复盘”和学习过往经验的基础。
- 与具体事件相对应的是语义记忆 (Semantic Memory),它存储的是更为抽象的知识、概念和规则。例如,通过对话了解到的用户偏好、需要长期遵守的指令或领域知识点,都适合存放在这里。这部分记忆具有高度的持久性和重要性,是智能体形成“知识体系”和进行关联推理的核心。
- 最后,为了与日益丰富的多媒体交互,我们引入了感知记忆 (Perceptual Memory)。该模块专门处理图像、音频等多模态信息,并支持跨模态检索。其生命周期会根据信息的重要性和可用存储空间进行动态管理。
MemoryTool
我们采用自顶向下的方式,从MemoryTool支持的具体操作开始,逐步深入到底层实现。MemoryTool(Class)作为记忆系统的统一接口,其设计遵循了"统一入口,分发处理"的架构模式:
def execute(self, action: str, **kwargs) -> str:
"""执行记忆操作
支持的操作:
- add: 添加记忆(支持4种类型: working/episodic/semantic/perceptual)
- search: 搜索记忆
- summary: 获取记忆摘要
- stats: 获取统计信息
- update: 更新记忆
- remove: 删除记忆
- forget: 遗忘记忆(多种策略)
- consolidate: 整合记忆(短期→长期)
- clear_all: 清空所有记忆
"""
if action == "add":
return self._add_memory(**kwargs)
elif action == "search":
return self._search_memory(**kwargs)
elif action == "summary":
return self._get_summary(**kwargs)
# ... 其他操作
add操作是记忆系统的基础,它模拟了人类大脑将感知信息编码为记忆的过程。在实现中,我们不仅要存储记忆内容,还要为每个记忆添加丰富的上下文信息,这些信息将在后续的检索和管理中发挥重要作用。
def _add_memory(
self,
content: str = "",
memory_type: str = "working",
importance: float = 0.5,
file_path: str = None,
modality: str = None,
**metadata
) -> str:
"""添加记忆"""
try:
# 确保会话ID存在
if self.current_session_id is None:
self.current_session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
# 感知记忆文件支持
if memory_type == "perceptual" and file_path:
inferred = modality or self._infer_modality(file_path)
metadata.setdefault("modality", inferred)
metadata.setdefault("raw_data", file_path)
# 添加会话信息到元数据
metadata.update({
"session_id": self.current_session_id,
"timestamp": datetime.now().isoformat()
})
memory_id = self.memory_manager.add_memory(
content=content,
memory_type=memory_type,
importance=importance,
metadata=metadata,
auto_classify=False
)
return f"✅ 记忆已添加 (ID: {memory_id[:8]}...)"
except Exception as e:
return f"❌ 添加记忆失败: {str(e)}"
这里主要实现了三个关键任务:会话ID的自动管理(确保每个记忆都有明确的会话归属)、多模态数据的智能处理(自动推断文件类型并保存相关元数据)、以及上下文信息的自动补充(为每个记忆添加时间戳和会话信息)。其中,importance参数(默认0.5)用于标记记忆的重要程度,取值范围0.0-1.0,这个机制模拟了人类大脑对不同信息重要性的评估。这种设计让Agent能够自动区分不同时间段的对话,并为后续的检索和管理提供丰富的上下文信息。以下是对不同记忆类型的使用示例:
# 1. 工作记忆 - 临时信息,容量有限
memory_tool.execute("add",
content="用户刚才问了关于Python函数的问题",
memory_type="working",
importance=0.6
)
# 2. 情景记忆 - 具体事件和经历
memory_tool.execute("add",
content="2024年3月15日,用户张三完成了第一个Python项目",
memory_type="episodic",
importance=0.8,
event_type="milestone",
location="在线学习平台"
)
# 3. 语义记忆 - 抽象知识和概念
memory_tool.execute("add",
content="Python是一种解释型、面向对象的编程语言",
memory_type="semantic",
importance=0.9,
knowledge_type="factual"
)
# 4. 感知记忆 - 多模态信息
memory_tool.execute("add",
content="用户上传了一张Python代码截图,包含函数定义",
memory_type="perceptual",
importance=0.7,
modality="image",
file_path="./uploads/code_screenshot.png"
)
search操作是记忆系统的核心功能,它需要在大量记忆中快速找到与查询最相关的内容。它涉及语义理解、相关性计算和结果排序等多个环节。
def _search_memory(
self,
query: str,
limit: int = 5,
memory_types: List[str] = None,
memory_type: str = None,
min_importance: float = 0.1
) -> str:
"""搜索记忆"""
try:
# 参数标准化处理
if memory_type and not memory_types:
memory_types = [memory_type]
results = self.memory_manager.retrieve_memories(
query=query,
limit=limit,
memory_types=memory_types,
min_importance=min_importance
)
if not results:
return f"🔍 未找到与 '{query}' 相关的记忆"
# 格式化结果
formatted_results = []
formatted_results.append(f"🔍 找到 {len(results)} 条相关记忆:")
for i, memory in enumerate(results, 1):
memory_type_label = {
"working": "工作记忆",
"episodic": "情景记忆",
"semantic": "语义记忆",
"perceptual": "感知记忆"
}.get(memory.memory_type, memory.memory_type)
# .get(key, default) 安全取值
# 如果 memory.memory_type 在字典里存在 → 返回对应的中文名;
# 如果 不存在(比如出现了未知类型)→ 返回 memory.memory_type 本身(即原始值)
content_preview = memory.content[:80] + "..." if len(memory.content) > 80 else memory.content
formatted_results.append(
f"{i}. [{memory_type_label}] {content_preview} (重要性: {memory.importance:.2f})"
)
return "\n".join(formatted_results)
except Exception as e:
return f"❌ 搜索记忆失败: {str(e)}"
搜索操作在设计上支持单数和复数两种参数形式(memory_type和memory_types),让用户以最自然的方式表达需求。其中,min_importance参数(默认0.1)用于过滤低质量记忆。对于搜索功能的使用,可以参考以下示例:
# 基础搜索
result = memory_tool.execute("search", query="Python编程", limit=5)
# 指定记忆类型搜索
result = memory_tool.execute("search",
query="学习进度",
memory_type="episodic",
limit=3
)
# 多类型搜索
result = memory_tool.execute("search",
query="函数定义",
memory_types=["semantic", "episodic"],
min_importance=0.5
)
遗忘机制是最具认知科学色彩的功能,它模拟人类大脑的选择性遗忘过程,支持三种策略:基于重要性(删除不重要的记忆)、基于时间(删除过时的记忆)和基于容量(当存储接近上限时删除最不重要的记忆)。
def _forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 30) -> str:
"""遗忘记忆(支持多种策略)"""
try:
count = self.memory_manager.forget_memories(
strategy=strategy,
threshold=threshold,
max_age_days=max_age_days
)
return f"🧹 已遗忘 {count} 条记忆(策略: {strategy})"
except Exception as e:
return f"❌ 遗忘记忆失败: {str(e)}"
三种遗忘策略的使用:
# 1. 基于重要性的遗忘 - 删除重要性低于阈值的记忆
memory_tool.execute("forget",
strategy="importance_based",
threshold=0.2
)
# 2. 基于时间的遗忘 - 删除超过指定天数的记忆
memory_tool.execute("forget",
strategy="time_based",
max_age_days=30
)
# 3. 基于容量的遗忘 - 当记忆数量超限时删除最不重要的
memory_tool.execute("forget",
strategy="capacity_based",
threshold=0.3
)
consolidate(整合)操作借鉴了神经科学中的记忆固化概念,模拟人类大脑将短期记忆转化为长期记忆的过程。默认设置是将重要性超过0.7的工作记忆转换为情景记忆,这个阈值确保只有真正重要的信息才会被长期保存。整个过程是自动化的,用户无需手动选择具体的记忆,系统会智能地识别符合条件的记忆并执行类型转换。
def _consolidate(self, from_type: str = "working", to_type: str = "episodic", importance_threshold: float = 0.7) -> str:
"""整合记忆(将重要的短期记忆提升为长期记忆)"""
try:
count = self.memory_manager.consolidate_memories(
from_type=from_type,
to_type=to_type,
importance_threshold=importance_threshold,
)
return f"🔄 已整合 {count} 条记忆为长期记忆({from_type} → {to_type},阈值={importance_threshold})"
except Exception as e:
return f"❌ 整合记忆失败: {str(e)}"
记忆整合的使用示例:
# 将重要的工作记忆转为情景记忆
memory_tool.execute("consolidate",
from_type="working",
to_type="episodic",
importance_threshold=0.7
)
# 将重要的情景记忆转为语义记忆
memory_tool.execute("consolidate",
from_type="episodic",
to_type="semantic",
importance_threshold=0.8
)
通过以上几个核心操作协作,MemoryTool构建了一个完整的记忆生命周期管理体系。从记忆的创建、检索、摘要到遗忘、整合和管理,形成了一个闭环的智能记忆管理系统,让Agent真正具备了类人的记忆能力。
MemoryManager
我们深入到底层实现,看看MemoryTool是如何与MemoryManager协作的。这种分层设计体现了软件工程中的关注点分离原则,MemoryTool专注于用户接口和参数处理,而MemoryManager则负责核心的记忆管理逻辑。
MemoryTool在初始化时会创建一个MemoryManager实例,并根据配置启用不同类型的记忆模块。这种设计让用户可以根据具体需求选择启用哪些记忆类型,既保证了功能的完整性,又避免了不必要的资源消耗。
class MemoryTool(Tool):
"""记忆工具 - 为Agent提供记忆功能"""
def __init__(
self,
user_id: str = "default_user",
memory_config: MemoryConfig = None,
memory_types: List[str] = None
):
super().__init__(
name="memory",
description="记忆工具 - 可以存储和检索对话历史、知识和经验"
)
# 初始化记忆管理器
self.memory_config = memory_config or MemoryConfig()
self.memory_types = memory_types or ["working", "episodic", "semantic"]
self.memory_manager = MemoryManager(
config=self.memory_config,
user_id=user_id,
enable_working="working" in self.memory_types,
enable_episodic="episodic" in self.memory_types,
enable_semantic="semantic" in self.memory_types,
enable_perceptual="perceptual" in self.memory_types
)
MemoryManager作为记忆系统的核心协调者,负责管理不同类型的记忆模块,并提供统一的操作接口。
class MemoryManager:
"""记忆管理器 - 统一的记忆操作接口"""
def __init__(
self,
config: Optional[MemoryConfig] = None,
user_id: str = "default_user",
enable_working: bool = True,
enable_episodic: bool = True,
enable_semantic: bool = True,
enable_perceptual: bool = False
):
self.config = config or MemoryConfig()
self.user_id = user_id
# 初始化存储和检索组件
self.store = MemoryStore(self.config)
self.retriever = MemoryRetriever(self.store, self.config)
# 初始化各类型记忆
self.memory_types = {}
if enable_working:
self.memory_types['working'] = WorkingMemory(self.config, self.store)
if enable_episodic:
self.memory_types['episodic'] = EpisodicMemory(self.config, self.store)
if enable_semantic:
self.memory_types['semantic'] = SemanticMemory(self.config, self.store)
if enable_perceptual:
self.memory_types['perceptual'] = PerceptualMemory(self.config, self.store)
四种记忆类型
工作记忆是记忆系统中最活跃的部分,它负责存储当前对话会话中的临时信息。工作记忆的设计重点在于快速访问和自动清理,这种设计确保了系统的响应速度和资源效率。工作记忆采用了纯内存存储方案,配合TTL(Time To Live)机制进行自动清理。这种设计的优势在于访问速度极快,但也意味着工作记忆的内容在系统重启后会丢失。这种特性正好符合工作记忆的定位,存储临时的、易变的信息。
工作记忆的检索采用了混合检索策略,首先尝试使用TF-IDF向量化进行语义检索,如果失败则回退到关键词匹配。这种设计确保了在各种环境下都能提供可靠的检索服务。评分算法结合了语义相似度、时间衰减和重要性权重
class WorkingMemory:
"""工作记忆实现
特点:
- 容量有限(默认50条)+ TTL自动清理
- 纯内存存储,访问速度极快
- 混合检索:TF-IDF向量化 + 关键词匹配
"""
def __init__(self, config: MemoryConfig):
self.max_capacity = config.working_memory_capacity or 50
self.max_age_minutes = config.working_memory_ttl or 60
self.memories = []
def add(self, memory_item: MemoryItem) -> str:
"""添加工作记忆"""
self._expire_old_memories() # 过期清理
if len(self.memories) >= self.max_capacity:
self._remove_lowest_priority_memory() # 容量管理
self.memories.append(memory_item)
return memory_item.id
def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
"""混合检索:TF-IDF向量化 + 关键词匹配"""
self._expire_old_memories()
# 尝试TF-IDF向量检索
vector_scores = self._try_tfidf_search(query)
# 计算综合分数
scored_memories = []
for memory in self.memories:
vector_score = vector_scores.get(memory.id, 0.0)
keyword_score = self._calculate_keyword_score(query, memory.content)
# 混合评分
base_relevance = vector_score * 0.7 + keyword_score * 0.3 if vector_score > 0 else keyword_score
time_decay = self._calculate_time_decay(memory.timestamp)
importance_weight = 0.8 + (memory.importance * 0.4)
final_score = base_relevance * time_decay * importance_weight
if final_score > 0:
scored_memories.append((final_score, memory))
scored_memories.sort(key=lambda x: x[0], reverse=True)
return [memory for _, memory in scored_memories[:limit]]
情景记忆负责存储具体的事件和经历,它的设计重点在于保持事件的完整性和时间序列关系。情景记忆采用了SQLite+Qdrant的混合存储方案,SQLite负责结构化数据的存储和复杂查询,Qdrant负责高效的向量检索。
情景记忆既需要"精确查找"(查询某天发生了什么),也需要"模糊联想"(回忆起和当前问题相似的经历)。单一数据库无法同时满足,混合架构是最优解。情景记忆的检索实现展现了复杂的多因素评分机制。它不仅考虑了语义相似度,还加入了时间近因性的考量,最终通过重要性权重进行调节。
class EpisodicMemory:
"""情景记忆实现
特点:
- SQLite+Qdrant混合存储架构
- 支持时间序列和会话级检索
- 结构化过滤 + 语义向量检索
"""
def __init__(self, config: MemoryConfig):
self.doc_store = SQLiteDocumentStore(config.database_path)
self.vector_store = QdrantVectorStore(config.qdrant_url, config.qdrant_api_key)
self.embedder = create_embedding_model_with_fallback()
self.sessions = {} # 会话索引
def add(self, memory_item: MemoryItem) -> str:
"""添加情景记忆"""
# 创建情景对象
episode = Episode(
episode_id=memory_item.id,
session_id=memory_item.metadata.get("session_id", "default"),
timestamp=memory_item.timestamp,
content=memory_item.content,
context=memory_item.metadata
)
# 更新会话索引
session_id = episode.session_id
if session_id not in self.sessions:
self.sessions[session_id] = []
self.sessions[session_id].append(episode.episode_id)
# 持久化存储(SQLite + Qdrant)
self._persist_episode(episode)
return memory_item.id
def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
"""核心所在:混合检索:结构化过滤 + 语义向量检索"""
# 1. 用SQLite结构化预过滤(时间范围、重要性等)
candidate_ids = self._structured_filter(**kwargs)
# 2. 向量语义检索(超采样为后续重排序留足空间)
hits = self._vector_search(query, limit * 5, kwargs.get("user_id"))
# 3. 综合评分与排序
results = []
for hit in hits:
if self._should_include(hit, candidate_ids, kwargs):
score = self._calculate_episode_score(hit)
memory_item = self._create_memory_item(hit)
results.append((score, memory_item))
results.sort(key=lambda x: x[0], reverse=True)
return [item for _, item in results[:limit]]
def _calculate_episode_score(self, hit) -> float:
"""情景记忆评分算法"""
# 语义相似度(与查询意图最相关的内容优先)
vec_score = float(hit.get("score", 0.0))
# 时间近因性(最近的记忆更有参考价值)
recency_score = self._calculate_recency(hit["metadata"]["timestamp"])
# 重要性
importance = hit["metadata"].get("importance", 0.5)
# 评分公式:(向量相似度 × 0.8 + 时间近因性 × 0.2) × 重要性权重(0.8 ~ 1.2 倍,标记为"重要"的记忆获得额外加分)
base_relevance = vec_score * 0.8 + recency_score * 0.2
importance_weight = 0.8 + (importance * 0.4)
return base_relevance * importance_weight
语义记忆是记忆系统中最复杂的部分,它负责存储抽象的概念、规则和知识。语义记忆的设计重点在于知识的结构化表示和智能推理能力。语义记忆采用了Neo4j图数据库和Qdrant向量数据库的混合架构,这种设计让系统既能进行快速的语义检索,又能利用知识图谱进行复杂的关系推理。
class SemanticMemory(BaseMemory):
"""语义记忆实现
特点:
- 使用HuggingFace中文预训练模型进行文本嵌入
- 向量检索进行快速相似度匹配
- 知识图谱存储实体和关系
- 混合检索策略:向量+图+语义推理
"""
def __init__(self, config: MemoryConfig, storage_backend=None):
super().__init__(config, storage_backend)
# 嵌入模型(统一提供)
self.embedding_model = get_text_embedder()
# 专业数据库存储
self.vector_store = QdrantConnectionManager.get_instance(**qdrant_config)
self.graph_store = Neo4jGraphStore(**neo4j_config)
# 实体和关系缓存
self.entities: Dict[str, Entity] = {}
self.relations: List[Relation] = []
# NLP处理器(支持中英文)
self.nlp = self._init_nlp()
语义记忆的添加过程体现了知识图谱构建的完整流程。系统不仅存储记忆内容,还会自动提取实体和关系,构建结构化的知识表示:
def add(self, memory_item: MemoryItem) -> str:
"""添加语义记忆"""
# 1. 生成文本嵌入
embedding = self.embedding_model.encode(memory_item.content)
# 2. 提取实体和关系
entities = self._extract_entities(memory_item.content)
relations = self._extract_relations(memory_item.content, entities)
# 3. 存储到Neo4j图数据库
for entity in entities:
self._add_entity_to_graph(entity, memory_item)
for relation in relations:
self._add_relation_to_graph(relation, memory_item)
# 4. 存储到Qdrant向量数据库
metadata = {
"memory_id": memory_item.id,
"entities": [e.entity_id for e in entities],
"entity_count": len(entities),
"relation_count": len(relations)
}
self.vector_store.add_vectors(
vectors=[embedding.tolist()],
metadata=[metadata],
ids=[memory_item.id]
)
- Qdrant存储关系数量(relation_count)而非关系本身,这是因为:
- Qdrant是向量检索引擎,不是图数据库。如果把关系结构存入Qdrant也无法做图遍历查询、无法做多跳推理。
- 架构职责分离,Qdrant负责"找到相似的记忆",Neo4j负责"理解记忆之间的关系"。
-
为什么还要存
relation_count:检索时的快速过滤、关系数量越多说明这段记忆包含的信息越复杂、避免每次检索都需要join查询Neo4j获取关系数量。
语义记忆的检索实现了混合搜索策略,结合了向量检索的语义理解能力和图检索的关系推理能力(召回+精排):
def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
"""检索语义记忆"""
# 1. 向量检索
vector_results = self._vector_search(query, limit * 2, user_id)
# 2. 图检索
graph_results = self._graph_search(query, limit * 2, user_id)
# 3. 混合排序
combined_results = self._combine_and_rank_results(
vector_results, graph_results, query, limit
)
return combined_results[:limit]
混合排序算法采用了多因素评分机制,在该例中,语义记忆的评分公式为:(向量相似度 × 0.7 + 图相似度 × 0.3) × (0.8 + 重要性 × 0.4)。这种设计的核心思想是:
- 向量检索权重(0.7):语义相似度是主要因素,确保检索结果与查询语义相关
- 图检索权重(0.3):关系推理作为补充,发现概念间的隐含关联
- 重要性权重范围[0.8, 1.2]:避免重要性过度影响相似度排序,保持检索的准确性
def _combine_and_rank_results(self, vector_results, graph_results, query, limit):
"""混合排序结果"""
combined = {}
# 合并向量和图检索结果
for result in vector_results:
combined[result["memory_id"]] = {
**result,
"vector_score": result.get("score", 0.0),
"graph_score": 0.0
}
for result in graph_results:
memory_id = result["memory_id"]
if memory_id in combined:
combined[memory_id]["graph_score"] = result.get("similarity", 0.0)
else:
combined[memory_id] = {
**result,
"vector_score": 0.0,
"graph_score": result.get("similarity", 0.0)
}
# 计算混合分数
for memory_id, result in combined.items():
vector_score = result["vector_score"]
graph_score = result["graph_score"]
importance = result.get("importance", 0.5)
# 基础相似度得分
base_relevance = vector_score * 0.7 + graph_score * 0.3
# 重要性权重 [0.8, 1.2]
importance_weight = 0.8 + (importance * 0.4)
# 最终得分:相似度 * 重要性权重
combined_score = base_relevance * importance_weight
result["combined_score"] = combined_score
# 排序并返回
sorted_results = sorted(
combined.values(),
key=lambda x: x["combined_score"],
reverse=True
)
return sorted_results[:limit]
感知记忆支持文本、图像、音频等多种模态的数据存储和检索。它采用了模态分离的存储策略,为不同模态的数据创建独立的向量集合,这种设计避免了维度不匹配的问题,同时保证了检索的准确性:
class PerceptualMemory(BaseMemory):
"""感知记忆实现
特点:
- 支持多模态数据(文本、图像、音频等)
- 跨模态相似性搜索
- 感知数据的语义理解
- 支持内容生成和检索
"""
def __init__(self, config: MemoryConfig, storage_backend=None):
super().__init__(config, storage_backend)
# 多模态编码器
self.text_embedder = get_text_embedder()
self._clip_model = self._init_clip_model() # 图像编码
self._clap_model = self._init_clap_model() # 音频编码
# 按模态分离的向量存储
self.vector_stores = {
"text": QdrantConnectionManager.get_instance(
collection_name="perceptual_text",
vector_size=self.vector_dim
),
"image": QdrantConnectionManager.get_instance(
collection_name="perceptual_image",
vector_size=self._image_dim
),
"audio": QdrantConnectionManager.get_instance(
collection_name="perceptual_audio",
vector_size=self._audio_dim
)
}
感知记忆的检索支持同模态和跨模态两种模式。同模态检索利用专业的编码器进行精确匹配,而跨模态检索则需要更复杂的语义对齐机制:
def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
"""检索感知记忆(可筛模态;同模态向量检索+时间/重要性融合)"""
user_id = kwargs.get("user_id")
target_modality = kwargs.get("target_modality")
query_modality = kwargs.get("query_modality", target_modality or "text")
# 同模态向量检索
try:
query_vector = self._encode_data(query, query_modality)
store = self._get_vector_store_for_modality(target_modality or query_modality)
where = {"memory_type": "perceptual"}
if user_id:
where["user_id"] = user_id
if target_modality:
where["modality"] = target_modality
hits = store.search_similar(
query_vector=query_vector,
limit=max(limit * 5, 20),
where=where
)
except Exception:
hits = []
# 融合排序(向量相似度 + 时间近因性 + 重要性权重)
results = []
for hit in hits:
vector_score = float(hit.get("score", 0.0))
recency_score = self._calculate_recency_score(hit["metadata"]["timestamp"])
importance = hit["metadata"].get("importance", 0.5)
# 评分算法
base_relevance = vector_score * 0.8 + recency_score * 0.2
importance_weight = 0.8 + (importance * 0.4)
combined_score = base_relevance * importance_weight
results.append((combined_score, self._create_memory_item(hit)))
results.sort(key=lambda x: x[0], reverse=True)
return [item for _, item in results[:limit]]
def _calculate_recency_score(self, timestamp: str) -> float:
"""计算时间近因性得分"""
try:
memory_time = datetime.fromisoformat(timestamp)
current_time = datetime.now()
age_hours = (current_time - memory_time).total_seconds() / 3600
# 指数衰减:24小时内保持高分,之后逐渐衰减
decay_factor = 0.1 # 衰减系数
recency_score = math.exp(-decay_factor * age_hours / 24)
return max(0.1, recency_score) # 最低保持0.1的基础分数
except Exception:
return 0.5 # 默认中等分数
3.1.3.RAG系统
RAG基础知识
检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合了信息检索和文本生成的技术。它的核心思想是:在生成回答之前,先从外部知识库中检索相关信息,然后将检索到的信息作为上下文提供给大语言模型,从而生成更准确、更可靠的回答。因此,检索增强生成可以拆分为三个词汇。检索是指从知识库中查询相关内容;增强是将检索结果融入提示词,辅助模型生成;生成则输出兼具准确性与透明度的答案。
一个完整的RAG应用流程主要分为两大核心环节。在数据准备阶段,系统通过数据提取、文本分割和向量化,将外部知识构建成一个可检索的数据库。随后在应用阶段,系统会响应用户的提问,从数据库中检索相关信息,将其注入Prompt,并最终驱动大语言模型生成答案。
其发展经历过三个阶段:
- 第一阶段:朴素RAG(Naive RAG, 2020-2021)。这是RAG技术的萌芽阶段,其流程直接而简单,通常被称为“检索-读取”(Retrieve-Read)模式。检索方式:主要依赖传统的关键词匹配算法,如
TF-IDF或BM25。这些方法计算词频和文档频率来评估相关性,对字面匹配效果好,但难以理解语义上的相似性。生成模式:将检索到的文档内容不加处理地直接拼接到提示词的上下文中,然后送给生成模型。 - 第二阶段:高级RAG(Advanced RAG, 2022-2023)。随着向量数据库和文本嵌入技术的成熟,RAG进入了快速发展阶段。研究者和开发者们在“检索”和“生成”的各个环节引入了大量优化技术。检索方式:转向基于稠密嵌入(Dense Embedding)的语义检索。通过将文本转换为高维向量,模型能够理解和匹配语义上的相似性,而不仅仅是关键词。生成模式:引入了很多优化技术,例如查询重写,文档分块,重排序等。
- 第三阶段:模块化RAG(Modular RAG, 2023-至今)。在高级RAG的基础上,现代RAG系统进一步向着模块化、自动化和智能化的方向发展。系统的各个部分被设计成可插拔、可组合的独立模块,以适应更多样化和复杂的应用场景。检索方式:如混合检索,多查询扩展,假设性文档嵌入等。生成模式:思维链推理,自我反思与修正等。
RAG工作原理
其完整工作流程可以如:
RAG系统的两个主要工作模式:
- 数据处理流程:处理和存储知识文档,在这里Hello-Agent教程采取工具
Markitdown,设计思路是将传入的一切外部知识源统一转化为Markdown格式进行处理。 - 查询与生成流程:根据查询检索相关信息并生成回答。
RAG系统架构设计
因为Memory_tool是系统性的实现,而RAG在Hello-Agent的设计中被定义为一种工具,可以梳理为一条pipeline。其的RAG系统的核心架构可以概括为"五层七步"的设计模式:
用户层:RAGTool统一接口
↓
应用层:智能问答、搜索、管理
↓
处理层:文档解析、分块、向量化
↓
存储层:向量数据库、文档存储
↓
基础层:嵌入模型、LLM、数据库
这种分层设计的优势在于每一层都可以独立优化和替换,同时保持整体系统的稳定性。例如,可以轻松地将嵌入模型从sentence-transformers切换到百炼API,而不影响上层的业务逻辑。同样的,这些处理的流程代码是完全可复用的,也可以选取自己需要的部分放进自己的项目中。RAGTool作为RAG系统的统一入口,提供了简洁的API接口。
class RAGTool(Tool):
"""RAG工具
提供完整的 RAG 能力:
- 添加多格式文档(PDF、Office、图片、音频等)
- 智能检索与召回
- LLM 增强问答
- 知识库管理
"""
def __init__(
self,
knowledge_base_path: str = "./knowledge_base",
qdrant_url: str = None,
qdrant_api_key: str = None,
collection_name: str = "rag_knowledge_base",
rag_namespace: str = "default"
):
# 初始化RAG管道
self._pipelines: Dict[str, Dict[str, Any]] = {}
self.llm = HelloAgentsLLM()
# 创建默认管道
default_pipeline = create_rag_pipeline(
qdrant_url=self.qdrant_url,
qdrant_api_key=self.qdrant_api_key,
collection_name=self.collection_name,
rag_namespace=self.rag_namespace
)
self._pipelines[self.rag_namespace] = default_pipeline
整个处理流程如下所示:
任意格式文档 → MarkItDown转换 → Markdown文本 → 智能分块 → 向量化 → 存储检索
RAG系统的核心优势之一是其强大的多模态文档处理能力。系统使用MarkItDown作为统一的文档转换引擎,支持几乎所有常见的文档格式。MarkItDown是微软开源的通用文档转换工具,它是HelloAgents RAG系统的核心组件,负责将任意格式的文档统一转换为结构化的Markdown文本。无论输入是PDF、Word、Excel、图片还是音频,最终都会转换为标准的Markdown格式,然后进入统一的分块、向量化和存储流程。
def _convert_to_markdown(path: str) -> str:
"""
Universal document reader using MarkItDown with enhanced PDF processing.
核心功能:将任意格式文档转换为Markdown文本
支持格式:
- 文档:PDF、Word、Excel、PowerPoint
- 图像:JPG、PNG、GIF(通过OCR)
- 音频:MP3、WAV、M4A(通过转录)
- 文本:TXT、CSV、JSON、XML、HTML
- 代码:Python、JavaScript、Java等
"""
if not os.path.exists(path):
return ""
# 对PDF文件使用增强处理
ext = (os.path.splitext(path)[1] or '').lower()
if ext == '.pdf':
return _enhanced_pdf_processing(path)
# 其他格式使用MarkItDown统一转换
md_instance = _get_markitdown_instance()
if md_instance is None:
return _fallback_text_reader(path)
try:
result = md_instance.convert(path)
markdown_text = getattr(result, "text_content", None)
if isinstance(markdown_text, str) and markdown_text.strip():
print(f"[RAG] MarkItDown转换成功: {path} -> {len(markdown_text)} chars Markdown")
return markdown_text
return ""
except Exception as e:
print(f"[WARNING] MarkItDown转换失败 {path}: {e}")
return _fallback_text_reader(path)
经过MarkItDown转换后,所有文档都统一为标准的Markdown格式。这为后续的智能分块提供了结构化的基础。HelloAgents实现了专门针对Markdown格式的智能分块策略,充分利用Markdown的结构化特性进行精确分割。Markdown结构感知的分块流程:
标准Markdown文本 → 标题层次解析 → 段落语义分割 → Token计算分块 → 重叠策略优化 → 向量化准备
↓ ↓ ↓ ↓ ↓ ↓
统一格式 #/##/### 语义边界 大小控制 信息连续性 嵌入向量
结构清晰 层次识别 完整性保证 检索优化 上下文保持 相似度匹配
由于所有文档都已转换为Markdown格式,系统可以利用Markdown的标题结构(#、##、###等)进行精确的语义分割:
def _split_paragraphs_with_headings(text: str) -> List[Dict]:
"""根据标题层次分割段落,保持语义完整性"""
lines = text.splitlines()
heading_stack: List[str] = []
paragraphs: List[Dict] = []
buf: List[str] = []
char_pos = 0
def flush_buf(end_pos: int):
if not buf:
return
content = "\n".join(buf).strip()
if not content:
return
paragraphs.append({
"content": content,
"heading_path": " > ".join(heading_stack) if heading_stack else None, # 标题层级路径,如 "章节1 > 子章节1.1"
"start": max(0, end_pos - len(content)),
"end": end_pos,
})
for ln in lines:
raw = ln
if raw.strip().startswith("#"):
# 处理标题行
flush_buf(char_pos) # 先保存当前段落
level = len(raw) - len(raw.lstrip('#')) # 计算标题级别
title = raw.lstrip('#').strip()
# 调整标题栈
if level <= 0:
level = 1
if level <= len(heading_stack):
heading_stack = heading_stack[:level-1] # 截断到当前层级
heading_stack.append(title)
char_pos += len(raw) + 1
continue
# 段落内容累积
if raw.strip() == "":
flush_buf(char_pos) # 空行触发段落结束
buf = []
else:
buf.append(raw) # 非空行累积到缓冲区
char_pos += len(raw) + 1
flush_buf(char_pos)
if not paragraphs:
paragraphs = [{"content": text, "heading_path": None, "start": 0, "end": len(text)}]
return paragraphs
在Markdown段落分割的基础上,系统进一步根据Token数量进行智能分块。由于输入已经是结构化的Markdown文本,系统可以更精确地控制分块边界,确保每个分块既适合向量化处理,又保持Markdown结构的完整性:
# 核心策略是以段落为最小单位进行累积,当超出token限制时触发分块并构建重叠部分。
# 重叠机制:通过保留尾部段落实现上下文连贯(保留已累积的段落作为下一个块的起始)
def _chunk_paragraphs(paragraphs: List[Dict], chunk_tokens: int, overlap_tokens: int) -> List[Dict]:
"""基于Token数量的智能分块"""
chunks: List[Dict] = []
cur: List[Dict] = []
cur_tokens = 0
i = 0
while i < len(paragraphs):
p = paragraphs[i]
p_tokens = _approx_token_len(p["content"]) or 1
if cur_tokens + p_tokens <= chunk_tokens or not cur:
cur.append(p)
cur_tokens += p_tokens
i += 1
else:
# 生成当前分块
content = "\n\n".join(x["content"] for x in cur)
start = cur[0]["start"]
end = cur[-1]["end"]
heading_path = next((x["heading_path"] for x in reversed(cur) if x.get("heading_path")), None)
chunks.append({
"content": content,
"start": start,
"end": end,
"heading_path": heading_path,
})
# 构建重叠部分
if overlap_tokens > 0 and cur:
kept: List[Dict] = []
kept_tokens = 0
for x in reversed(cur):
t = _approx_token_len(x["content"]) or 1
if kept_tokens + t > overlap_tokens:
break
kept.append(x)
kept_tokens += t
cur = list(reversed(kept))
cur_tokens = kept_tokens
else:
cur = []
cur_tokens = 0
# 处理最后一个分块
if cur:
content = "\n\n".join(x["content"] for x in cur)
start = cur[0]["start"]
end = cur[-1]["end"]
heading_path = next((x["heading_path"] for x in reversed(cur) if x.get("heading_path")), None)
chunks.append({
"content": content,
"start": start,
"end": end,
"heading_path": heading_path,
})
return chunks
同时为了兼容不同语言,系统实现了针对中英文混合文本的Token估算算法,这对于准确控制分块大小至关重要:
def _approx_token_len(text: str) -> int:
"""近似估计Token长度,支持中英文混合"""
# CJK字符按1 token计算
cjk = sum(1 for ch in text if _is_cjk(ch))
# 其他字符按空白分词计算
non_cjk_tokens = len([t for t in text.split() if t])
return cjk + non_cjk_tokens
def _is_cjk(ch: str) -> bool:
"""判断是否为CJK字符"""
code = ord(ch)
return (
0x4E00 <= code <= 0x9FFF or # CJK统一汉字
0x3400 <= code <= 0x4DBF or # CJK扩展A
0x20000 <= code <= 0x2A6DF or # CJK扩展B
0x2A700 <= code <= 0x2B73F or # CJK扩展C
0x2B740 <= code <= 0x2B81F or # CJK扩展D
0x2B820 <= code <= 0x2CEAF or # CJK扩展E
0xF900 <= code <= 0xFAFF # CJK兼容汉字
)
嵌入模型是RAG系统的核心,它负责将文本转换为高维向量,使得计算机能够理解和比较文本的语义相似性。RAG系统的检索能力很大程度上取决于嵌入模型的质量和向量存储的效率。HelloAgents实现了统一的嵌入接口。在这里为了演示,使用百炼API,如果尚未配置可以切换为本地的all-MiniLM-L6-v2模型,如果两种方案都不支持,也配置了TF-IDF算法来兜底。实际使用可以替换为自己想要的模型或者API:
def index_chunks(
store = None,
chunks: List[Dict] = None,
cache_db: Optional[str] = None,
batch_size: int = 64,
rag_namespace: str = "default"
) -> None:
"""
Index markdown chunks with unified embedding and Qdrant storage.
Uses百炼 API with fallback to sentence-transformers.
"""
if not chunks:
print("[RAG] No chunks to index")
return
# 使用统一嵌入模型
embedder = get_text_embedder()
dimension = get_dimension(384)
# 创建默认Qdrant存储
if store is None:
store = _create_default_vector_store(dimension)
print(f"[RAG] Created default Qdrant store with dimension {dimension}")
# 预处理Markdown文本以获得更好的嵌入质量
processed_texts = []
for c in chunks:
raw_content = c["content"]
processed_content = _preprocess_markdown_for_embedding(raw_content)
processed_texts.append(processed_content)
print(f"[RAG] Embedding start: total_texts={len(processed_texts)} batch_size={batch_size}")
# 批量编码
vecs: List[List[float]] = []
for i in range(0, len(processed_texts), batch_size):
part = processed_texts[i:i+batch_size]
try:
# 使用统一嵌入器(内部处理缓存)
part_vecs = embedder.encode(part)
# 标准化为List[List[float]]格式
if not isinstance(part_vecs, list):
if hasattr(part_vecs, "tolist"):
part_vecs = [part_vecs.tolist()]
else:
part_vecs = [list(part_vecs)]
# 处理向量格式和维度
for v in part_vecs:
try:
if hasattr(v, "tolist"):
v = v.tolist()
v_norm = [float(x) for x in v]
# 维度检查和调整
if len(v_norm) != dimension:
print(f"[WARNING] 向量维度异常: 期望{dimension}, 实际{len(v_norm)}")
if len(v_norm) < dimension:
v_norm.extend([0.0] * (dimension - len(v_norm)))
else:
v_norm = v_norm[:dimension]
vecs.append(v_norm)
except Exception as e:
print(f"[WARNING] 向量转换失败: {e}, 使用零向量")
vecs.append([0.0] * dimension)
except Exception as e:
print(f"[WARNING] Batch {i} encoding failed: {e}")
# 实现重试机制
# ... 重试逻辑 ...
print(f"[RAG] Embedding progress: {min(i+batch_size, len(processed_texts))}/{len(processed_texts)}")
高级检索策略
在实际应用中,用户的查询表述与文档中的实际内容可能存在用词差异,导致相关文档无法被检索到。为了解决这个问题,HelloAgents实现了三种互补的高级检索策略:多查询扩展(MQE)、假设文档嵌入(HyDE)和统一的扩展检索框架。
多查询扩展(Multi-Query Expansion)是一种通过生成语义等价的多样化查询来提高检索召回率的技术。这种方法的核心洞察是:同一个问题可以有多种不同的表述方式,而不同的表述可能匹配到不同的相关文档。例如,"如何学习Python"可以扩展为"Python入门教程"、"Python学习方法"、"Python编程指南"等多个查询。通过并行执行这些扩展查询并合并结果,系统能够覆盖更广泛的相关文档,避免因用词差异而遗漏重要信息。MQE的优势在于它能够自动理解用户查询的多种可能含义,特别是对于模糊查询或专业术语查询效果显著。系统使用LLM生成扩展查询,确保扩展的多样性和语义相关性:
prompt = [
{"role": "system", "content": "你是检索查询扩展助手。生成语义等价或互补的多样化查询。使用中文,简短,避免标点。"},
{"role": "user", "content": f"原始查询:{query}\n请给出{n}个不同表述的查询,每行一个。"}
]
假设文档嵌入(Hypothetical Document Embeddings)是一种创新的检索技术,它的核心思想是"用答案找答案"。传统的检索方法是用问题去匹配文档,但问题和答案在语义空间中的分布往往存在差异——问题通常是疑问句,而文档内容是陈述句。HyDE通过让LLM先生成一个假设性的答案段落,然后用这个答案段落去检索真实文档,从而缩小了查询和文档之间的语义鸿沟。这种方法的优势在于,假设答案与真实答案在语义空间中更加接近,因此能够更准确地匹配到相关文档。即使假设答案的内容不完全正确,它所包含的关键术语、概念和表述风格也能有效引导检索系统找到正确的文档。特别是对于专业领域的查询,HyDE能够生成包含领域术语的假设文档,显著提升检索精度:(只用生成的答案去检索,如果用问题+答案,会引入噪音,偏离真实文档分布,embedding既不像Query也不像Document,并且Query信息已经包含在Answer中,会造成信息冗余)
prompt = [
{"role": "system", "content": "根据用户问题,先写一段可能的答案性段落,用于向量检索的查询文档(不要分析过程)。"},
{"role": "user", "content": f"问题:{query}\n请直接写一段中等长度、客观、包含关键术语的段落。"}
]
HelloAgents将MQE和HyDE两种策略整合到统一的扩展检索框架中。系统通过enable_mqe和enable_hyde参数让用户可以根据具体场景选择启用哪些策略:对于需要高召回率的场景可以同时启用两种策略,对于性能敏感的场景可以只使用基础检索。扩展检索的核心机制是"扩展-检索-合并"三步流程。首先,系统根据原始查询生成多个扩展查询(包括MQE生成的多样化查询和HyDE生成的假设文档);然后,对每个扩展查询并行执行向量检索,获取候选文档池;最后,通过去重和分数排序合并所有结果,返回最相关的top-k文档。这种设计的巧妙之处在于,它通过candidate_pool_multiplier参数(默认为4)扩大候选池,确保有足够的候选文档进行筛选,同时通过智能去重避免返回重复内容。
MQE擅长处理用词多样性问题,HyDE擅长处理语义鸿沟问题,而统一框架则确保了结果的质量和多样性。对于一般查询,建议启用MQE;对于专业领域查询,建议同时启用MQE和HyDE;对于性能敏感场景,可以只使用基础检索或仅启用MQE。
3.2.上下文工程
要让智能体在真实复杂场景中稳定地“思考”与“行动”,仅有记忆与检索还不够——我们需要一套工程化方法,持续、系统地为模型构造恰当的“上下文”。这就是本章的主题:上下文工程(Context Engineering)。它关注的是“在每一次模型调用前,如何以可复用、可度量、可演进的方式,拼装并优化输入上下文”,从而提升正确性、鲁棒性与效率。
本章主要介绍上下文工程的核心概念与实践,并在HelloAgents框架中新增了上下文构建器和两个配套工具:
- ContextBuilder:上下文构建器,实现 GSSC (Gather-Select-Structure-Compress) 流水线,提供统一的上下文管理接口
- NoteTool:结构化笔记工具,支持智能体进行持久化记忆管理
- TerminalTool:终端工具,支持智能体进行文件系统操作和即时上下文检索
这些组件共同构成了完整的上下文工程解决方案,是实现长时程任务管理和智能体式搜索的关键。
(个人感觉,可以说记忆系统是上下文工程的一个子集)
3.2.1.什么是上下文工程
| 概念 | 核心思想 | 工作方式 | 局限性 |
|---|---|---|---|
| 提示词工程 | 问对问题 | 精心设计一个完美的 Prompt | 知识过时,无法与外部世界交互 |
| RAG | 给予参考资料 | 提问前先从知识库检索相关信息 | 被动响应,无法执行任务,依赖知识库 |
| Agent | 赋予行动能力 | 通过“思考-行动”循环来使用工具、完成任务 | 复杂,不稳定,成本高 |
| 上下文工程 | 打造完美输入 | 系统性地收集、筛选、压缩、格式化所有信息,为模型提供最优上下文 | 是一个方法论/学科,而非具体系统,实现复杂 |
如今,用语言模型构建系统不再只是找对提示词里的句式和措辞,而是要回答一个更宏观的问题:什么样的上下文配置,最有可能让模型产出我们期望的行为?所谓“上下文”,是指在对大语言模型(LLM)进行采样时所包含的那组 tokens。手头的工程问题,是在 LLM 的固有约束之下,优化这些 tokens 的效用,以便稳定地得到预期结果。想要有效驾驭 LLM,往往需要“在上下文中思考”——也就是说:在任何一次调用时,都要审视 LLM 可见的整体状态,并预判这种状态可能诱发的行为。
上下文工程 vs. 提示工程
如上图所示,在现在前沿模型厂商的视角中,上下文工程是提示工程的自然演进。提示工程关注如何编写与组织 LLM 的指令以获得更优结果(例如系统提示的写法与结构化策略);而上下文工程则是在推理阶段,如何策划与维护“最优的信息集合(tokens)”,其中不仅包含提示本身,还包含其他会进入上下文窗口的一切信息。
在 LLM 工程的早期阶段,提示往往是主要工作,因为大多数用例(除日常聊天外)都需要针对单轮分类或文本生成做精调式的提示优化。顾名思义,提示工程的核心是“如何写出有效提示”,尤其是系统提示。然而,随着我们开始工程化地构建更强的智能体,它们在更长的时间范围内、跨多次推理轮次地工作,我们就需要能管理整个上下文状态的策略——其中包括系统指令、工具、MCP(Model Context Protocol)、外部数据、消息历史等。
一个循环运行的智能体,会不断产生下一轮推理可能相关的数据,这些信息必须被周期性地提炼。因此,上下文工程的“艺与术”,在于从持续扩张的“候选信息宇宙”中,甄别哪些内容应当进入有限的上下文窗口。
上下文工程为什么重要
目前的大模型会有这样的一个问题:上下文腐蚀(context rot)——随着上下文窗口中的 tokens 增加,模型从上下文中准确回忆信息的能力反而下降。不同模型的退化曲线或许更平滑,但这一特征几乎在所有模型上都会出现。因此,上下文必须被视作一种有限资源,且具有边际收益递减。就像人类有有限的工作记忆容量一样,LLM 也有一笔“注意力预算”。每新增一个 token,都会消耗这笔预算的一部分。
这种稀缺并非偶然,而是源自 LLM 的架构约束。Transformer 让每个 token 能够与上下文中的所有 token 建立关联,理论上形成 (n^2) 级别的两两注意力关系。随着上下文长度增长,模型对这些两两关系的建模能力会被“拉薄”,从而自然地产生“上下文规模”与“注意力集中度”的张力。此外,模型的注意力模式来源于训练数据分布——短序列通常比长序列更常见,因此模型对“全上下文依赖”的经验更少、专门参数也更少。
优秀的上下文工程目标是:用尽可能少、但高信号密度的 tokens,最大化获得期望结果的概率。
工程实践正在从“推理前一次性检索(embedding 检索)”逐步过渡到“及时(Just-in-time, JIT)上下文”。后者不再预先加载所有相关数据,而是维护轻量化引用(文件路径、存储查询、URL 等),在运行时通过工具动态加载所需数据。除了存储效率,引用的元数据本身也能帮助精化行为:目录层级、命名约定、时间戳等都在隐含地传达“目的与时效”。
需要权衡的是:运行时探索往往比预计算检索更慢,并且需要有“主见”的工程设计来确保模型拥有正确的工具与启发式。如果缺少引导,智能体可能会误用工具、追逐死胡同或错过关键信息,造成上下文浪费。在不少场景中,混合策略更有效:前置加载少量“高价值”上下文以保证速度,然后允许智能体按需继续自主探索。边界的选择取决于任务动态性与时效要求。在工程上,可以预先放入类似“项目约定说明(如 README/指南)”的文件,同时提供 glob、grep 等原语,让智能体即时检索具体文件,从而绕开过时索引与复杂语法树的沉没成本。
长时程任务要求智能体在超出上下文窗口的长序列行动中,仍能保持连贯性、上下文一致与目标导向。指望无限增大上下文窗口并不能根治“上下文污染”与相关性退化的问题,因此需要直接面向这些约束的工程手段:压缩整合(Compaction)、结构化笔记(Structured note-taking)与子代理架构(Sub-agent architectures)。
- 压缩整合(Compaction)
- 定义:当对话接近上下文上限时,对其进行高保真总结,并用该摘要重启一个新的上下文窗口,以维持长程连贯性。
- 实践:让模型压缩并保留架构性决策、未解决缺陷、实现细节,丢弃重复的工具输出与噪声;新窗口携带压缩摘要 + 最近少量高相关工件(如“最近访问的若干文件”)。
- 调参建议:先优化召回(确保不遗漏关键信息),再优化精确度(剔除冗余内容);一种安全的“轻触式”压缩是对“深历史中的工具调用与结果”进行清理。
- 结构化笔记(Structured note-taking)
- 定义:也称“智能体记忆”。智能体以固定频率将关键信息写入上下文外的持久化存储,在后续阶段按需拉回。
- 价值:以极低的上下文开销维持持久状态与依赖关系。例如维护 TODO 列表、项目 NOTES.md、关键结论/依赖/阻塞项的索引,跨数十次工具调用与多轮上下文重置仍能保持进度与一致性。
- 说明:在非编码场景中同样有效(如长期策略性任务、游戏/仿真中的目标管理与统计计数)。
- 子代理架构(Sub-agent architectures)
- 思想:由主代理负责高层规划与综合,多个专长子代理在“干净的上下文窗口”中各自深挖、调用工具并探索,最后仅回传凝练摘要(常见 1,000–2,000 tokens)。
- 好处:实现关注点分离。庞杂的搜索上下文留在子代理内部,主代理专注于整合与推理;适合需要并行探索的复杂研究/分析任务。
- 经验:公开的多智能体研究系统显示,该模式在复杂研究任务上相较单代理基线具有显著优势。
方法取舍可以遵循以下经验法则:
- 压缩整合:适合需要长对话连续性的任务,强调上下文的“接力”。
- 结构化笔记:适合有里程碑/阶段性成果的迭代式开发与研究。
- 子代理架构:适合复杂研究与分析,能从并行探索中获益。
3.2.2.上下文构建器
ContextBuilder 的设计理念是"简单高效",去除不必要的复杂性,统一以"相关性+新近性"的分数进行选择,符合 Agent 模块化与可维护性的工程取向。
一个优秀的上下文管理系统应该解决以下几个关键问题:
-
统一入口:将"获取(Gather)- 选择(Select)- 结构化(Structure)- 压缩(Compress)"抽象为可复用流水线,减少在 Agent 实现中的重复模板代码。这种统一的接口设计让开发者无需在每个 Agent 中重复编写上下文管理逻辑。
-
稳定形态:输出固定骨架的上下文模板,便于调试、A/B 测试与评估。我们采用了分区组织的模板结构:
[Role & Policies]:明确 Agent 的角色定位和行为准则[Task]:当前需要完成的具体任务[State]:Agent 的当前状态和上下文信息[Evidence]:从外部知识库检索的证据信息[Context]:历史对话和相关记忆[Output]:期望的输出格式和要求
-
预算守护:在 token 预算内尽量保留高价值信息,对超限上下文提供兜底压缩策略。这确保了即使在信息量巨大的场景下,系统也能稳定运行。
-
最小规则:不引入来源/优先级等分类维度,避免复杂度增长。实践表明,基于相关性和新近性的简单评分机制,在大多数场景下已经足够有效。
在实际应用 ContextBuilder 时,以下几点最佳实践值得注意:
- 动态调整 token 预算:根据任务复杂度动态调整
max_tokens,简单任务使用较小预算,复杂任务增加预算。 - 相关性计算优化:在生产环境中,将简单的关键词重叠替换为向量相似度计算,提升检索质量。
- 缓存机制:对于不变的系统指令和知识库内容,可以实现缓存机制,避免重复计算。
- 监控与日志:记录每次上下文构建的统计信息(选中信息数量、token 使用率等),便于后续优化。
- A/B 测试:对于关键参数(如相关性权重、新近性权重),通过 A/B 测试找到最优配置。
3.2.3.结构化笔记工具
顾名思义,NoteTool就是记笔记用的,可以用于长期项目追踪--记录当前的任务状态、关键结论、阻塞点、下一步计划;研究任务管理--记录每篇论文的核心观点、待深入调研的主题、重要参考文献。
每个笔记都是一个独立的 .md 文件,格式可以如下:
---
id: note_20250119_153000_0
title: 项目进展 - 第一阶段
type: task_state
tags: [refactoring, phase1, backend]
created_at: 2025-01-19T15:30:00
updated_at: 2025-01-19T15:30:00
---
# 项目进展 - 第一阶段
## 完成情况
已完成数据模型层的重构,主要改动包括:
1. 统一了实体类的命名规范
2. 引入了类型提示,提升代码可维护性
3. 优化了数据库查询性能
## 测试覆盖
- 单元测试覆盖率: 85%
- 集成测试覆盖率: 70%
## 下一步计划
1. 重构业务逻辑层
2. 解决依赖冲突问题
3. 提升集成测试覆盖率至85%
采用 Markdown (人类可读且格式丰富)+ YAML(机器可解析) 的混合格式,这种设计兼顾了结构化和可读性。
NoteTool 还维护一个 notes_index.json 文件,用于快速检索和管理笔记,用于:
- 快速检索:无需打开每个文件,直接从索引中查找
- 元数据管理:集中管理所有笔记的元数据
- 完整性校验:可以检测文件缺失或损坏
{
"note_20250119_153000_0": {
"id": "note_20250119_153000_0",
"title": "项目进展 - 第一阶段",
"type": "task_state",
"tags": ["refactoring", "phase1", "backend"],
"created_at": "2025-01-19T15:30:00",
"updated_at": "2025-01-19T15:30:00",
"file_path": "./notes/note_20250119_153000_0.md"
}
}
一个note的生命周期可以是:
- create(标题、内容、类型、标签列表、唯一ID、创建时间、更新时间)
- 构建id
- 构建元数据
- 构建markdown内容
- 保存
- 更新索引
- search
- read
- update
- list(列出所有笔记,按指定的tag或排序方式)
- count(笔记数量统计)/summary(笔记摘要)
- delete
在实际使用 NoteTool 时,以下最佳实践能帮助构建更强大的长时程智能体:
-
合理的笔记分类:
task_state:记录阶段性进展和状态conclusion:记录重要的结论和发现blocker:记录阻塞问题,优先级最高action:记录下一步行动计划reference:记录重要的参考资料
-
定期清理和归档:
- 对于已解决的 blocker,更新为 conclusion
- 对于过时的 action,及时删除或更新
- 使用 tags 进行版本管理,如
["v1.0", "completed"]
-
与 ContextBuilder 的配合:
- 在每轮对话前检索相关笔记
- 根据笔记类型设置不同的相关性分数(blocker > action > conclusion)
- 限制笔记数量,避免上下文过载
-
人机协作:
- 笔记是人类可读的 Markdown 格式,支持手动编辑
- 使用 Git 进行版本控制,追踪笔记的演化
- 在关键阶段,人工审核 Agent 生成的笔记
-
自动化工作流:
- 定期生成笔记摘要报告
- 基于笔记自动生成项目进度文档
- 将笔记内容同步到其他系统(如 Notion、Confluence)
3.2.4.终端文件访问工具
在许多实际场景中,智能体需要即时访问和探索文件系统——查看日志文件、分析代码库结构、检索配置文件等。这就是 TerminalTool 的用武之地。TerminalTool 为智能体提供了安全的命令行执行能力,支持常用的文件系统和文本处理命令,同时通过多层安全机制确保系统安全。这种设计实现了之前3.2.1中提到的"即时(Just-in-time, JIT)上下文"理念——智能体不需要预先加载所有文件,而是按需探索和检索。
其使用场景的特点是需要实时、轻量级的文件系统访问,而不是预先索引和向量化。
允许智能体执行命令是一个强大但危险的能力。TerminalTool 需要通过多层安全机制确保系统安全:
- 第一层:命令白名单,完全禁止任何可能修改系统的操作
- 第二层:工作目录限制(沙箱),只能访问指定的工作目录及其子目录,无法访问系统其他部分
- 第三层:超时控制,每个命令都有执行时间限制,防止无限循环或资源耗尽
- 第四层:输出大小限制,防止内存溢出
3.2.5.上下文挑战
上下文挑战主要存在四个方面,分别描述为:
- 上下文污染/毒化 - 当幻觉进入上下文时
- 嵌入错误信息,导致代理(agent)性能脱轨。这种情况会“毒化”关键部分,如目标或摘要,使得模型固执于不可能或无关的目标,导致重复的、无意义的的行为。
- 上下文干扰/分散 - 当上下文压倒了训练数据时
- 模型过度依赖历史细节,而忽略其预训练知识或生成新颖解决方案的能力。这会引发重复动作而非创造性问题解决,且性能在上下文窗口满载前就已下降。
- 上下文混淆 - 当多余的上下文影响响应时
- 无关或多余的信息(如冗余工具定义)被纳入上下文,迫使模型考虑它,从而产生次优响应。即使额外内容无害,也会稀释焦点并降低质量。在长上下文里,模型不光要找到相关信息,还要能分辨“哪个才是正确的 needle,哪个只是干扰项”。
- 上下文冲突 - 当上下文各部分不一致时
- 是混淆的更严重形式,指上下文中的信息相互冲突(如新工具或事实与现有内容矛盾),从而破坏推理,通常因为模型锁定在早期假设中。这比单纯无关更具破坏性,在多步交互中,早期的错误会传播,模型依赖于有缺陷的前提。
为解决上述挑战,上下文工程的策略主要分为四种:写入(存储)、选择、压缩和隔离。

3.3.智能体通信协议
在前面的章节中,我们构建了功能完备的单体智能体,它们具备推理、工具调用和记忆能力。然而,当我们尝试构建更复杂的 AI 系统时,自然会有疑问:如何让智能体与外部世界高效交互?如何让多个智能体相互协作?
这正是智能体通信协议要解决的核心问题。本章将为 HelloAgents 框架引入三种通信协议:MCP(Model Context Protocol)用于智能体与工具的标准化通信,A2A(Agent-to-Agent Protocol)用于智能体间的点对点协作,ANP(Agent Network Protocol)用于构建大规模智能体网络。这三种协议共同构成了智能体通信的基础设施层。
3.3.1.通信协议引言
对于一个简单的单智能体,如2.1.1节中的demo,这个智能体工作得很好,但它面临着三个根本性的限制。首先是工具集成的困境:每当需要访问新的外部服务(如 GitHub API、数据库、文件系统),我们都必须编写专门的 Tool 类。这不仅工作量大,而且不同开发者编写的工具无法互相兼容。其次是能力扩展的瓶颈:智能体的能力被限制在预先定义的工具集内,无法动态发现和使用新的服务。最后是协作的缺失:当任务复杂到需要多个专业智能体协作时(如研究员+撰写员+编辑),我们只能通过手动编排来协调它们的工作。这种方式存在明显的问题:代码重复(每个工具都要处理 HTTP 请求、错误处理、认证等),难以维护(API 变更需要修改所有相关工具),无法复用(其他开发者的工具无法直接使用),扩展性差(添加新服务需要大量编码工作)。
通信协议的核心价值正是解决这些问题。它提供了一套标准化的接口规范,让智能体能够以统一的方式访问各种外部服务,而无需为每个服务编写专门的适配器。这就像互联网的 TCP/IP 协议,它让不同的设备能够相互通信,而不需要为每种设备编写专门的通信代码。
有了通信协议,就可以把这样的繁杂代码
# 传统方式:手动集成每个服务
class GitHubTool(BaseTool):
"""需要手写GitHub API适配器"""
def run(self, repo_url):
# 大量的API调用代码...
pass
class DatabaseTool(BaseTool):
"""需要手写数据库适配器"""
def run(self, query):
# 数据库连接和查询代码...
pass
class WeatherTool(BaseTool):
"""需要手写天气API适配器"""
def run(self, location):
# 天气API调用代码...
pass
# 每个新服务都需要重复这个过程
agent.add_tool(GitHubTool())
agent.add_tool(DatabaseTool())
agent.add_tool(WeatherTool())
简化成:
from hello_agents.tools import MCPTool
# 连接到MCP服务器,自动获得所有工具
mcp_tool = MCPTool() # 内置服务器提供基础工具
# 或者连接到专业的MCP服务器
github_mcp = MCPTool(server_command=["npx", "-y", "@modelcontextprotocol/server-github"])
database_mcp = MCPTool(server_command=["python", "database_mcp_server.py"])
# 智能体自动获得所有能力,无需手写适配器
agent.add_tool(mcp_tool)
agent.add_tool(github_mcp)
agent.add_tool(database_mcp)
标准化接口让不同服务提供统一的访问方式,互操作性使得不同开发者的工具可以无缝集成,动态发现允许智能体在运行时发现新的服务和能力,可扩展性让系统能够轻松添加新的功能模块。
3.3.2.MCP协议
MCP(Model Context Protocol)由 Anthropic 团队提出,其核心设计理念是标准化智能体与外部工具/资源的通信方式。MCP 的设计哲学是"上下文共享"。它不仅仅是一个 RPC(远程过程调用)协议,更重要的是它允许智能体和工具之间共享丰富的上下文信息。例如当智能体访问一个代码仓库时,MCP 服务器不仅能提供文件内容,还能提供代码结构、依赖关系、提交历史等上下文信息,让智能体能够做出更智能的决策。
在 Agent/MCP 工具函数里最好写类型注释,因为它能帮助框架生成工具说明,也能帮助模型更准确地调用工具。不是“MCP 协议本身”看你的 Python 类型注释,而是 MCP 的 Python server 框架,比如 FastMCP,会读取你写的类型注解,然后自动生成工具的 schema。
MCP 概念介绍
MCP 就像 USB-C 统一了各种设备的连接方式一样,MCP 统一了智能体与外部工具的交互方式。无论你使用 Claude、GPT 还是其他模型,只要它们支持 MCP 协议,就能无缝访问相同的工具和资源。
MCP 协议采用 Host、Client、Servers 三层架构设计,三层架构的职责:
- Host(宿主层):Claude Desktop 作为 Host,负责接收用户提问并与 Claude 模型交互。Host 是用户直接交互的界面,它管理整个对话流程。
- Client(客户端层):当 Claude 模型决定需要访问文件系统时,Host 中内置的 MCP Client 被激活。Client 负责与适当的 MCP Server 建立连接,发送请求并接收响应。
- Server(服务器层):文件系统 MCP Server 被调用,执行实际的文件扫描操作,访问桌面目录,并返回找到的文档列表。
完整的交互流程:用户问题 → Claude Desktop(Host) → Claude 模型分析 → 需要文件信息 → MCP Client 连接 → 文件系统 MCP Server → 执行操作 → 返回结果 → Claude 生成回答 → 显示在 Claude Desktop 上
这种架构设计的优势在于关注点分离:Host 专注于用户体验,Client 专注于协议通信,Server 专注于具体功能实现。开发者只需专注于开发对应的 MCP Server,无需关心 Host 和 Client 的实现细节。
MCP 工作流程
当用户提出问题时,完整的工具选择流程如下:
- 工具发现阶段:MCP Client 连接到 Server 后,首先调用
list_tools()获取所有可用工具的描述信息(包括工具名称、功能说明、参数定义) - 上下文构建:Client 将工具列表转换为 LLM 能理解的格式,添加到系统提示词中。例如:
你可以使用以下工具: - read_file(path: str): 读取指定路径的文件内容 - search_code(query: str, language: str): 在代码库中搜索 - 模型推理:LLM 分析用户问题和可用工具,决定是否需要调用工具以及调用哪个工具。这个决策基于工具的描述和当前对话上下文
- 工具执行:如果 LLM 决定使用工具,Client 通过 MCP Server 执行所选工具,获取结果
- 结果整合:工具执行结果被送回给 LLM,LLM 结合结果生成最终回答
这个过程是完全自动化的,LLM 会根据工具描述的质量来决定是否使用以及如何使用工具。因此,编写清晰、准确的工具描述至关重要。
MCP 与 Function Calling 的差异
首先需要明确的是,Function Calling 与 MCP 并非竞争关系,而是相辅相成的。Function Calling 是大语言模型的一项核心能力,它体现了模型内在的智能,使模型能够理解何时需要调用函数,并精准生成相应的调用参数。相对地,MCP 则扮演着基础设施协议的角色,它在工程层面解决了工具与模型如何连接的问题,通过标准化的方式来描述和调用工具。
我们可以用一个简单的类比来理解:Function Calling 相当于你学会了“如何打电话”这项技能,包括何时拨号、如何与对方沟通、何时挂断。而 MCP 则是那个全球统一的“电话通信标准”,确保了任何一部电话都能顺利地拨通另一部。
Demo:使用MCP协议
首先来看一个使用functioncall的例子,然后在相同的场景下使用MCP作为对比。
"""
功能:
使用 OpenAI Chat Completions 的 function calling / tool calling 实现一个最小 Agent。
场景:
用户问:“请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。”
Agent 会让模型自己决定是否调用工具:
1. get_weather(city):查询演示天气数据
2. celsius_to_fahrenheit(celsius):摄氏温度转华氏温度
"""
# os 用于读取系统环境变量,例如 LLM_MODEL_ID、LLM_API_KEY。
import os
# json 用于把工具入参和工具结果在 Python dict 与 JSON 字符串之间转换。
import json
# typing 中的 Any 表示“任意类型”,Callable 表示“可调用对象”,用于类型标注。
from typing import Any, Callable
# dotenv 用于从项目根目录的 .env 文件读取环境变量。
from dotenv import load_dotenv
# OpenAI 是官方 Python SDK 中的客户端类;很多国产/本地模型服务也兼容这个接口。
from openai import OpenAI
# 加载 .env 文件中的配置;override=False 表示如果系统环境变量已存在,不强行覆盖。
load_dotenv(override=False)
class FunctionCallAgent:
"""
一个最小 function calling Agent。
这个类故意不使用 LangChain/LangGraph,目的是明确 function calling 的底层流程:
1. 把工具 JSON Schema 发给模型;
2. 模型返回 tool_calls;
3. Python 代码真正执行工具;
4. 把工具结果发回模型;
5. 模型生成最终自然语言回答。
"""
def __init__(
self,
model: str | None = None,
apiKey: str | None = None,
baseUrl: str | None = None,
timeout: int | None = None,
) -> None:
"""
初始化 Agent,并创建 OpenAI 兼容客户端。
参数说明:
model: 模型 ID;如果不传,就从环境变量 LLM_MODEL_ID 读取。
apiKey: API Key;如果不传,就从环境变量 LLM_API_KEY 读取。
baseUrl: OpenAI 兼容服务地址;如果不传,就从环境变量 LLM_BASE_URL 读取。
timeout: 超时时间;如果不传,就从环境变量 LLM_TIMEOUT 读取,默认 60 秒。
"""
# self.model 保存模型名称;or 表示左边为空时使用右边。
self.model = model or os.getenv("LLM_MODEL_ID")
# apiKey 保存 API 密钥;这里变量名沿用你给出的写法。
apiKey = apiKey or os.getenv("LLM_API_KEY")
# baseUrl 保存 OpenAI 兼容接口的基础地址,例如 https://api.openai.com/v1。
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
# timeout 优先使用参数,其次读取环境变量,最后使用 60 秒。
timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
# all([...]) 会检查列表中每个值是否都为真;任何一个为空就说明配置不完整。
if not all([self.model, apiKey, baseUrl]):
# raise ValueError 用于主动抛出参数错误,避免后续请求时才报难懂的错。
raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
# 创建 OpenAI 兼容客户端;base_url 允许你接入非 OpenAI 官方但兼容 OpenAI 协议的服务。
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
# 保存 Python 侧真实可执行的工具函数;key 是工具名,value 是函数对象。
# 把工具函数注册到一个字典里,是函数本身,并非可执行的函数结果。
# 因为 function calling 里,模型返回的通常是一个工具名,Agent 需要根据工具名找到真实函数。
# Callable[..., Any]表示一个可以被调用的对象;参数数量和参数类型不限;返回值类型也不限。
self.tool_functions: dict[str, Callable[..., Any]] = {
"get_weather": self.get_weather,
"celsius_to_fahrenheit": self.celsius_to_fahrenheit,
}
# 保存要发给模型看的工具定义;模型只看到 JSON Schema,不会直接看到 Python 函数源码。
self.tools = self.build_openai_tools_schema()
def get_weather(self, city: str) -> dict[str, Any]:
"""
演示用天气查询工具。
参数:
city: 城市名,例如“北京”“上海”“深圳”。
返回:
一个 dict,里面包含城市、天气、摄氏温度、湿度等信息。
注意:
这里不用真实天气 API,是为了让 demo 不依赖外部网络。
真实项目中可以把这里替换成数据库查询、HTTP API、RAG 检索等。
"""
# demo_weather_db 是一个假的天气数据库;key 是城市名,value 是天气信息。
demo_weather_db = {
"北京": {"condition": "晴", "temperature_c": 30.0, "humidity": "35%"},
"上海": {"condition": "小雨", "temperature_c": 27.0, "humidity": "78%"},
"深圳": {"condition": "多云", "temperature_c": 32.0, "humidity": "70%"},
"杭州": {"condition": "阴", "temperature_c": 28.0, "humidity": "65%"},
}
# dict.get(key, default) 表示如果 city 存在就取对应值,否则取 default 默认值。
weather = demo_weather_db.get(
city,
{"condition": "未知", "temperature_c": 26.0, "humidity": "未知"},
)
# 返回结构化结果;模型后续会根据这个 JSON 内容组织最终回答。
return {
"city": city,
"condition": weather["condition"],
"temperature_c": weather["temperature_c"],
"humidity": weather["humidity"],
"source": "demo_local_weather_db",
}
def celsius_to_fahrenheit(self, celsius: float) -> dict[str, float]:
"""
摄氏温度转华氏温度工具。
参数:
celsius: 摄氏温度,例如 30.0。
返回:
一个 dict,包含摄氏温度和换算后的华氏温度。
"""
# 摄氏转华氏公式:F = C * 9 / 5 + 32。
fahrenheit = celsius * 9 / 5 + 32
# round(x, 2) 表示保留两位小数,便于最终回答展示。
return {"celsius": celsius, "fahrenheit": round(fahrenheit, 2)}
def build_openai_tools_schema(self) -> list[dict[str, Any]]:
"""
构造 OpenAI tools 参数需要的 JSON Schema。
function calling 的关键点:
- Python 函数不能直接发给模型;
- 要把函数名、函数说明、参数结构写成 JSON Schema;
- 模型根据这个 schema 判断何时调用哪个工具,以及生成什么参数。
"""
# 返回一个列表;每个元素都是一个工具定义。
return [
{
# type=function 表示这是一个传统函数工具。
"type": "function",
# function 字段描述具体函数的名字、用途、参数格式。
"function": {
# name 必须与 self.tool_functions 中的 key 一致,否则 Python 侧找不到函数。
"name": "get_weather",
# description 会被模型读取,用来判断什么时候应该调用这个工具。
"description": "查询指定城市的演示天气信息,返回天气、摄氏温度和湿度。",
# parameters 是 JSON Schema,描述函数参数。
"parameters": {
# type=object 表示参数整体是一个 JSON 对象。
"type": "object",
# properties 描述对象中的字段。
"properties": {
"city": {
# city 字段类型是字符串。
"type": "string",
# 字段说明会影响模型填参质量。
"description": "城市名,例如:北京、上海、深圳、杭州。",
}
},
# required 表示 city 是必填参数。
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "celsius_to_fahrenheit",
"description": "把摄氏温度转换为华氏温度。",
"parameters": {
"type": "object",
"properties": {
"celsius": {
# number 表示整数或小数都可以。
"type": "number",
"description": "摄氏温度数值,例如 30.0。",
}
},
"required": ["celsius"],
},
},
},
]
def build_messages(self, user_question: str) -> list[dict[str, Any]]:
"""
构造对话消息。
参数:
user_question: 用户输入的问题。
返回:
messages 列表,符合 OpenAI Chat Completions 的消息格式。
"""
# system prompt 定义 Agent 行为边界:需要数据时必须调用工具,不能瞎编。
system_prompt = (
"你是一个天气助手 Agent。"
"当用户需要天气或温度换算时,你必须优先调用可用工具获取数据,"
"不要自己编造天气和温度。最后用中文给出简洁回答。"
)
# messages 是发送给模型的上下文;role=system 是系统指令,role=user 是用户问题。
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_question},
]
def call_llm(self, messages: list[dict[str, Any]]) -> Any:
"""
调用大模型。
参数:
messages: 当前完整对话上下文。
返回:
模型返回的 assistant message,可能包含 content,也可能包含 tool_calls。
"""
# self.client.chat.completions.create 是 Chat Completions API 的创建方法。
response = self.client.chat.completions.create(
# model 指定使用哪个模型。
model=self.model,
# messages 是当前对话历史。
messages=messages,
# tools 把可用工具的 schema 提供给模型。
tools=self.tools,
# tool_choice="auto" 表示让模型自己决定是否调用工具。
tool_choice="auto",
)
# choices[0].message 是第一条候选回答;一般 demo 只取第一条。
return response.choices[0].message
def run_tool_call(self, tool_call: Any) -> str:
"""
执行模型请求的某一次工具调用。
参数:
tool_call: 模型返回的工具调用对象,里面包含函数名和 JSON 字符串参数。
返回:
工具执行结果的 JSON 字符串,用于作为 role=tool 的消息发回模型。
"""
# tool_call.function.name 是模型想调用的函数名。
tool_name = tool_call.function.name
# tool_call.function.arguments 是 JSON 字符串,例如 '{"city":"北京"}'。
raw_arguments = tool_call.function.arguments
# json.loads 把 JSON 字符串转成 Python dict。
arguments = json.loads(raw_arguments)
# 根据函数名从工具注册表中取出真正的 Python 函数。
tool_function = self.tool_functions.get(tool_name)
# 如果模型调用了不存在的工具,返回错误信息,而不是让程序崩溃。
if tool_function is None:
return json.dumps({"error": f"未知工具:{tool_name}"}, ensure_ascii=False)
# **arguments 表示把 dict 展开成关键字参数,例如 {"city":"北京"} -> city="北京"。
result = tool_function(**arguments)
# json.dumps 把 Python dict 转成 JSON 字符串;ensure_ascii=False 保证中文不变成 Unicode 编码。
return json.dumps(result, ensure_ascii=False)
def run(self, user_question: str) -> str:
"""
运行完整 Agent 流程。
参数:
user_question: 用户问题。
返回:
模型最终生成的中文回答。
"""
# 第一步:构造初始 messages。
messages = self.build_messages(user_question)
# 最多循环 5 轮,防止模型反复调用工具导致死循环。
for _ in range(5):
# 第二步:把当前 messages 和工具 schema 发给模型。
assistant_message = self.call_llm(messages)
# 第三步:把 assistant 消息加入对话历史;model_dump 可把 SDK 对象转成普通 dict。
messages.append(assistant_message.model_dump(exclude_none=True))
# 第四步:如果没有 tool_calls,说明模型已经给出最终回答。
if not assistant_message.tool_calls:
# assistant_message.content 就是最终自然语言文本。
return assistant_message.content or ""
# 第五步:模型可能一次请求多个工具调用,所以这里逐个执行。
for tool_call in assistant_message.tool_calls:
# 真正执行 Python 工具函数,得到 JSON 字符串结果。
tool_result_json = self.run_tool_call(tool_call)
# 把工具结果作为 role=tool 的消息追加回上下文。
messages.append(
{
# role=tool 表示这条消息是工具执行结果,不是用户或模型说的话。
"role": "tool",
# tool_call_id 用于告诉模型:这个结果对应刚才哪一次工具调用。
"tool_call_id": tool_call.id,
# content 是工具返回内容,通常写 JSON 字符串。
"content": tool_result_json,
}
)
# 如果循环 5 次仍没有最终回答,返回一个保护性提示。
return "工具调用轮数过多,Agent 已停止。"
# Python 约定:只有直接运行本文件时,__name__ 才等于 "__main__"。
if __name__ == "__main__":
# 实例化 Agent;不传参数时,会自动读取 .env。
agent = FunctionCallAgent()
# 准备一个 demo 问题;你可以改成 input("请输入问题:") 做成命令行交互。
question = "请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。"
# 运行 Agent,拿到最终回答。
answer = agent.run(question)
# 打印最终回答。
print("\n===== Function Calling Agent 最终回答 =====")
print(answer)
===== Function Calling Agent 最终回答 =====
北京天气:晴,30°C,湿度 35%。
换算成华氏温度:86°F。
穿衣建议:天气较热,建议穿短袖、薄款透气衣物,注意防晒,多补水;如果长时间在户外,可以带帽子或太阳镜。
以下是相同的任务用LangChain封装的写法:
"""
功能:
使用 LangChain 框架 + @tool 装饰器实现一个最小 Agent。
场景:
用户问:“请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。”
Agent 会让模型自己决定是否调用工具:
1. get_weather(city):查询演示天气数据
2. celsius_to_fahrenheit(celsius):摄氏温度转华氏温度
安装依赖:
pip install langchain langchain-openai python-dotenv
.env 示例:
LLM_MODEL_ID=你的模型名
LLM_API_KEY=你的API密钥
LLM_BASE_URL=你的OpenAI兼容接口地址
LLM_TIMEOUT=60
"""
# os 用于读取系统环境变量,例如 LLM_MODEL_ID、LLM_API_KEY、LLM_BASE_URL。
import os
# json 用于把工具调用参数、工具返回结果在 Python 对象和 JSON 字符串之间转换。
import json
# Any 表示任意类型,用于类型注解。
from typing import Any
# dotenv 用于从当前项目目录下的 .env 文件中加载环境变量。
from dotenv import load_dotenv
# ChatOpenAI 是 LangChain 对 OpenAI Chat Model 的封装。
# 它不仅支持 OpenAI 官方接口,也支持很多 OpenAI-compatible 的模型服务。
from langchain_openai import ChatOpenAI
# @tool 是 LangChain 提供的工具注册装饰器。
# 用 @tool 包裹普通 Python 函数后,这个函数就会变成 LangChain Tool。
from langchain_core.tools import tool
# SystemMessage 表示系统提示词。
# HumanMessage 表示用户消息。
# ToolMessage 表示工具执行结果消息。
from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage
# load_dotenv 会读取 .env 文件,并把里面的配置加载到 os.environ 中。
# override=False 表示如果系统环境变量中已经存在同名变量,就不覆盖。
load_dotenv(override=False)
@tool
def get_weather(city: str) -> dict[str, Any]:
"""
查询指定城市的演示天气信息,返回天气、摄氏温度和湿度。
Args:
city: 城市名,例如:北京、上海、深圳、杭州。
"""
# demo_weather_db 是一个假的本地天气数据库。
# key 是城市名,value 是该城市的天气信息。
demo_weather_db = {
"北京": {"condition": "晴", "temperature_c": 30.0, "humidity": "35%"},
"上海": {"condition": "小雨", "temperature_c": 27.0, "humidity": "78%"},
"深圳": {"condition": "多云", "temperature_c": 32.0, "humidity": "70%"},
"杭州": {"condition": "阴", "temperature_c": 28.0, "humidity": "65%"},
}
# dict.get(key, default) 表示:
# 如果 city 在 demo_weather_db 中存在,就返回对应天气;
# 如果不存在,就返回后面的默认天气。
weather = demo_weather_db.get(
city,
{"condition": "未知", "temperature_c": 26.0, "humidity": "未知"},
)
# 返回结构化结果。
# LangChain 会把这个结果作为工具执行结果,再交还给模型。
return {
"city": city,
"condition": weather["condition"],
"temperature_c": weather["temperature_c"],
"humidity": weather["humidity"],
"source": "demo_local_weather_db",
}
@tool
def celsius_to_fahrenheit(celsius: float) -> dict[str, float]:
"""
把摄氏温度转换为华氏温度。
Args:
celsius: 摄氏温度数值,例如 30.0。
"""
# 摄氏转华氏公式:
# F = C * 9 / 5 + 32
fahrenheit = celsius * 9 / 5 + 32
# round(fahrenheit, 2) 表示把结果保留两位小数。
return {
"celsius": celsius,
"fahrenheit": round(fahrenheit, 2),
}
class LangChainToolAgent:
"""
一个最小 LangChain Tool Calling Agent。
和之前的 function calling 版本相比:
原来的写法:
1. 手写 tools JSON Schema;
2. 手写 self.tool_functions 字典;
3. 手动根据 tool name 找 Python 函数;
4. 手动 json.loads 解析参数;
5. 手动 tool_function(**arguments) 调用函数。
LangChain 写法:
1. 用 @tool 注册工具;
2. LangChain 根据函数名、类型注解、docstring 生成工具 schema;
3. 用 llm.bind_tools(tools) 把工具绑定到模型;
4. 模型返回 tool_calls;
5. 根据 tool name 找到对应 LangChain Tool;
6. 用 selected_tool.invoke(args) 执行工具。
"""
def __init__(
self,
model: str | None = None,
apiKey: str | None = None,
baseUrl: str | None = None,
timeout: int | None = None,
) -> None:
"""
初始化 Agent,并创建 LangChain 的 ChatOpenAI 客户端。
参数说明:
model: 模型 ID;如果不传,就从环境变量 LLM_MODEL_ID 读取。
apiKey: API Key;如果不传,就从环境变量 LLM_API_KEY 读取。
baseUrl: OpenAI 兼容服务地址;如果不传,就从环境变量 LLM_BASE_URL 读取。
timeout: 超时时间;如果不传,就从环境变量 LLM_TIMEOUT 读取,默认 60 秒。
"""
# self.model 保存模型名称。
# 如果构造函数传入了 model,就用传入值;
# 否则读取环境变量 LLM_MODEL_ID。
self.model = model or os.getenv("LLM_MODEL_ID")
# apiKey 保存 API 密钥。
# 如果构造函数传入了 apiKey,就用传入值;
# 否则读取环境变量 LLM_API_KEY。
apiKey = apiKey or os.getenv("LLM_API_KEY")
# baseUrl 保存 OpenAI-compatible 接口地址。
# 例如:
# https://api.openai.com/v1
# http://localhost:8000/v1
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
# timeout 保存请求超时时间。
# 优先使用构造函数参数;
# 其次读取环境变量 LLM_TIMEOUT;
# 如果都没有,就默认 60 秒。
timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
# all([...]) 会判断列表里的每个元素是否都为真。
# 如果 self.model、apiKey、baseUrl 任意一个为空,就说明配置不完整。
if not all([self.model, apiKey, baseUrl]):
# 主动抛出 ValueError,避免后面请求模型时才出现更难排查的错误。
raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
# 创建 LangChain ChatOpenAI 模型对象。
# model 指定模型名称。
# api_key 指定 API 密钥。
# base_url 指定 OpenAI-compatible 接口地址。
# timeout 指定请求超时时间。
self.llm = ChatOpenAI(
model=self.model,
api_key=apiKey,
base_url=baseUrl,
timeout=timeout,
)
# self.tools 保存所有 LangChain 工具对象。
# 注意:
# get_weather 和 celsius_to_fahrenheit 已经不是普通函数,
# 因为它们上面加了 @tool;
# 它们现在是 LangChain Tool 对象。
self.tools = [
get_weather,
celsius_to_fahrenheit,
]
# tool_map 是一个工具名字到工具对象的映射表。
# key 是工具名,例如 "get_weather";
# value 是 LangChain Tool 对象。
#
# 这和原来 self.tool_functions 的作用类似,
# 但这里保存的是 LangChain Tool,而不是原始 Python 函数。
self.tool_map = {
single_tool.name: single_tool
for single_tool in self.tools
}
# bind_tools 会把工具绑定到模型上。
# 绑定后,模型在 invoke 时就能“看到”这些工具的 schema。
#
# LangChain 会根据 @tool 函数的:
# 1. 函数名;
# 2. 参数类型注解;
# 3. docstring;
# 自动生成工具 schema。
self.llm_with_tools = self.llm.bind_tools(self.tools)
def build_messages(self, user_question: str) -> list[Any]:
"""
构造 LangChain 消息列表。
参数:
user_question: 用户输入的问题。
返回:
一个 LangChain Message 列表。
"""
# system_prompt 是系统提示词。
# 它告诉模型这个 Agent 的行为规则。
system_prompt = (
"你是一个天气助手 Agent。"
"当用户需要天气或温度换算时,你必须优先调用可用工具获取数据,"
"不要自己编造天气和温度。最后用中文给出简洁回答。"
)
# 返回消息列表。
# SystemMessage 对应 role=system。
# HumanMessage 对应 role=user。
return [
SystemMessage(content=system_prompt),
HumanMessage(content=user_question),
]
def print_tools_info(self) -> None:
"""
打印当前 Agent 可用的工具信息。
这一步不是 Agent 必须的,
只是为了更直观看到 @tool 注册后,LangChain 保存了哪些工具信息。
"""
# 打印标题。
print("\n===== LangChain 已注册工具 =====")
# 遍历 self.tools 中的每一个工具对象。
for single_tool in self.tools:
# single_tool.name 是工具名。
print(f"工具名: {single_tool.name}")
# single_tool.description 是工具描述。
# 这个描述通常来自函数 docstring。
print(f"工具说明: {single_tool.description}")
# single_tool.args 是工具参数 schema。
# LangChain 根据函数参数类型注解自动生成。
print(f"工具参数: {single_tool.args}")
# 打印分隔线。
print("-" * 40)
def run_one_tool_call(self, tool_call: dict[str, Any]) -> ToolMessage:
"""
执行模型返回的一次工具调用。
参数:
tool_call: 模型返回的工具调用信息。
LangChain 中通常是一个 dict,
里面包含 name、args、id 等字段。
返回:
ToolMessage,表示工具执行结果消息。
"""
# tool_call["name"] 是模型想要调用的工具名。
tool_name = tool_call["name"]
# tool_call["args"] 是模型为工具生成的参数。
# 例如:
# {"city": "北京"}
# {"celsius": 30.0}
tool_args = tool_call["args"]
# tool_call["id"] 是这次工具调用的唯一 ID。
# ToolMessage 需要带上这个 ID,
# 这样模型才能知道工具结果对应刚才哪一次 tool call。
# tool_call["id"] 是这一次工具调用的 ID,不是工具函数本身的 ID。
tool_call_id = tool_call["id"]
# 根据工具名,从 tool_map 中找到对应的 LangChain Tool。
selected_tool = self.tool_map.get(tool_name)
# 如果模型调用了不存在的工具,就构造一个错误结果。
if selected_tool is None:
# 把错误信息转成 JSON 字符串。
error_content = json.dumps(
{"error": f"未知工具:{tool_name}"},
ensure_ascii=False,
)
# 返回 ToolMessage。
return ToolMessage(
content=error_content,
tool_call_id=tool_call_id,
)
# selected_tool.invoke(tool_args) 会真正执行工具。
# 对于 get_weather,相当于执行:
# get_weather.invoke({"city": "北京"})
#
# 对于 celsius_to_fahrenheit,相当于执行:
# celsius_to_fahrenheit.invoke({"celsius": 30.0})
result = selected_tool.invoke(tool_args)
# 工具结果可能是 dict,也可能是 str、int、float 等。
# 为了统一传回模型,这里转成 JSON 字符串。
result_json = json.dumps(result, ensure_ascii=False)
# 构造 ToolMessage。
# content 是工具执行结果。
# tool_call_id 用于对应模型刚才的工具调用 ID。
return ToolMessage(
content=result_json,
tool_call_id=tool_call_id,
)
def run(self, user_question: str) -> str:
"""
运行完整 Agent 流程。
参数:
user_question: 用户问题。
返回:
模型最终生成的中文回答。
"""
# 第一步:构造初始消息。
messages = self.build_messages(user_question)
# 最多循环 5 轮。
# 这是为了防止模型一直调用工具,导致死循环。
for _ in range(5):
# 第二步:调用绑定了工具的模型。
# 如果模型认为需要工具,它会返回 tool_calls。
# 如果模型认为不需要工具,它会直接返回 content。
assistant_message = self.llm_with_tools.invoke(messages)
# 第三步:把模型消息加入历史。
# 这一步非常重要:
# 后续工具结果必须接在这个 assistant_message 后面,
# 模型才能理解上下文。
messages.append(assistant_message)
# 第四步:检查模型是否请求了工具调用。
# 在 LangChain 中,assistant_message.tool_calls 通常是一个 list。
# 如果为空,说明模型已经给出最终回答。
if not assistant_message.tool_calls:
# assistant_message.content 就是模型最终回答。
return str(assistant_message.content or "")
# 第五步:模型可能一次性请求多个工具调用,所以这里逐个执行。
for tool_call in assistant_message.tool_calls:
# 执行单个工具调用,并得到 ToolMessage。
tool_message = self.run_one_tool_call(tool_call)
# 把工具结果加入消息历史。
# 下一轮模型调用时,模型会看到工具结果,并决定继续调用工具或生成最终回答。
messages.append(tool_message)
# 如果 5 轮后仍然没有得到最终回答,就返回保护性提示。
return "工具调用轮数过多,Agent 已停止。"
if __name__ == "__main__":
# 创建 Agent 实例。
# 不传参数时,会自动从 .env 文件或系统环境变量读取配置。
agent = LangChainToolAgent()
# 打印 @tool 注册后的工具信息。
# 这一步方便观察 LangChain 根据函数注解和 docstring 生成了什么工具信息。
agent.print_tools_info()
# 准备一个 demo 问题。
question = "请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。"
# 运行 Agent,得到最终回答。
answer = agent.run(question)
# 打印最终回答。
print("\n===== LangChain Tool Calling Agent 最终回答 =====")
print(answer)
===== LangChain 已注册工具 =====
工具名: get_weather
工具说明: 查询指定城市的演示天气信息,返回天气、摄氏温度和湿度。
Args:
city: 城市名,例如:北京、上海、深圳、杭州。
工具参数: {'city': {'title': 'City', 'type': 'string'}}
----------------------------------------
工具名: celsius_to_fahrenheit
工具说明: 把摄氏温度转换为华氏温度。
Args:
celsius: 摄氏温度数值,例如 30.0。
工具参数: {'celsius': {'title': 'Celsius', 'type': 'number'}}
----------------------------------------
===== LangChain Tool Calling Agent 最终回答 =====
北京天气:晴,30°C,湿度35%
换算后:86°F
穿衣建议:天气偏热,建议穿轻薄短袖、透气衣物,注意防晒,外出可带水;如果长时间在户外,建议备一件薄外套防空调。
以下是使用MCP连接工具服务器的代码(把server和client放进了同一个文件,其实应该分开更直观):
"""
功能:
使用 MCP(Model Context Protocol)实现与 function_call_agent.py 相同的 Agent 功能。
场景:
用户问:“请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。”
Agent 通过 MCP 连接工具服务器,发现工具,再让模型选择并调用工具。
为什么这个文件既是 MCP server 又是 MCP client:
本文件使用命令行参数区分两种模式:
python mcp_agent.py --server 启动 MCP 工具服务器
python mcp_agent.py 启动 MCP Agent 客户端,并自动用子进程拉起 server
注意:
MCP 负责“工具服务器协议化、工具发现、工具调用”。
LLM 仍然负责“根据用户问题决定调用哪个工具”。
"""
# asyncio 用于运行异步 MCP client,因为 MCP 的连接和调用都是异步接口。
import asyncio
# os 用于读取 .env 或系统环境变量。
import os
# sys 用于读取命令行参数,也用于拿到当前 Python 解释器路径。
import sys
# json 用于解析模型生成的工具参数,以及序列化工具结果。
import json
# typing.Any 表示任意类型,主要用于函数返回值和消息结构的类型标注。
from typing import Any
# dotenv 用于读取 .env 文件中的 LLM_MODEL_ID、LLM_API_KEY、LLM_BASE_URL 等配置。
from dotenv import load_dotenv
# OpenAI 是 OpenAI Python SDK 客户端,也可连接 OpenAI 兼容服务。
from openai import OpenAI
# ClientSession 是 MCP 客户端会话对象;StdioServerParameters 描述如何通过 stdio 启动/连接 MCP server。
from mcp import ClientSession, StdioServerParameters, types
# stdio_client 是 MCP 的 stdio 传输客户端;它会启动子进程并通过标准输入输出通信。
from mcp.client.stdio import stdio_client
# FastMCP 是 MCP Python SDK 提供的快速 server 开发封装,可用装饰器暴露 tools/resources/prompts。
from mcp.server.fastmcp import FastMCP
# 用了 官方 MCP Python SDK 里内置的 FastMCP
# 加载 .env 文件;override=False 表示不覆盖已经存在的系统环境变量。
load_dotenv(override=False)
# 创建 MCP server 对象;名字 WeatherMCPDemo 会出现在 MCP 客户端或 Inspector 中。
# json_response=True 让工具更倾向于返回结构化 JSON,便于客户端解析。
mcp = FastMCP("WeatherMCPDemo", json_response=True)
@mcp.tool()
def get_weather(city: str) -> dict[str, Any]:
"""
MCP 工具:查询指定城市的演示天气信息。
参数:
city: 城市名,例如“北京”“上海”“深圳”“杭州”。
返回:
dict 格式的天气信息。
说明:
@mcp.tool() 会把这个普通 Python 函数注册为 MCP 工具。
MCP server 会根据函数签名 city: str 和 docstring 自动生成工具 schema。
"""
# 这里使用本地假数据,避免 demo 依赖真实天气 API。
demo_weather_db = {
"北京": {"condition": "晴", "temperature_c": 30.0, "humidity": "35%"},
"上海": {"condition": "小雨", "temperature_c": 27.0, "humidity": "78%"},
"深圳": {"condition": "多云", "temperature_c": 32.0, "humidity": "70%"},
"杭州": {"condition": "阴", "temperature_c": 28.0, "humidity": "65%"},
}
# 如果 city 不在假数据库中,就返回一个默认天气,保证 demo 不会报 KeyError。
weather = demo_weather_db.get(
city,
{"condition": "未知", "temperature_c": 26.0, "humidity": "未知"},
)
# 返回结构化 dict;MCP 会把它包装成 CallToolResult 返回给 client。
return {
"city": city,
"condition": weather["condition"],
"temperature_c": weather["temperature_c"],
"humidity": weather["humidity"],
"source": "demo_mcp_weather_server",
}
@mcp.tool()
def celsius_to_fahrenheit(celsius: float) -> dict[str, float]:
"""
MCP 工具:把摄氏温度转换为华氏温度。
参数:
celsius: 摄氏温度。
返回:
dict,包含原摄氏温度和华氏温度。
"""
# 摄氏转华氏公式:F = C * 9 / 5 + 32。
fahrenheit = celsius * 9 / 5 + 32
# 返回结构化结果;round(..., 2) 用于保留两位小数。
return {"celsius": celsius, "fahrenheit": round(fahrenheit, 2)}
@mcp.prompt()
def weather_agent_prompt(user_question: str) -> str:
"""
MCP Prompt:返回一个可复用的天气 Agent 提示模板。
参数:
user_question: 用户原始问题。
返回:
一段提示词文本。
说明:
@mcp.prompt() 和 @mcp.tool() 类似,但它暴露的不是可执行函数,
而是“提示模板”。MCP client 可以先发现 prompt,再按需获取 prompt 内容。
"""
# 这里返回系统提示词;client 会把它放进 OpenAI messages 的 system 位置。
return (
"你是一个天气助手 Agent。"
"你可以使用 MCP server 提供的工具查询天气和换算温度。"
"当用户需要天气或温度换算时,必须优先调用工具,不要编造数据。"
"最后用中文回答,并给出简洁穿衣建议。\n\n"
f"用户问题:{user_question}"
)
class MCPAgent:
"""
一个最小 MCP Agent。
它和 function calling 版的核心区别:
- function calling 版:工具函数和工具 schema 都写在 Agent 代码内部;
- MCP 版:工具函数放在 MCP server 中,Agent 先连接 server,再动态发现工具。
也就是说,MCP 把“工具提供者”和“Agent 使用者”解耦了。
"""
def __init__(
self,
model: str | None = None,
apiKey: str | None = None,
baseUrl: str | None = None,
timeout: int | None = None,
) -> None:
"""
初始化 MCP Agent,并创建 OpenAI 兼容客户端。
参数说明和 function_call_agent.py 完全一致。
"""
# 读取模型 ID;优先使用参数,其次使用环境变量。
self.model = model or os.getenv("LLM_MODEL_ID")
# 读取 API Key;变量名 apiKey 沿用你提供的写法。
apiKey = apiKey or os.getenv("LLM_API_KEY")
# 读取 OpenAI 兼容服务地址。
baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
# 读取超时时间;没有配置时默认 60 秒。
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 build_server_params(self) -> StdioServerParameters:
"""
构造 MCP stdio server 启动参数。
返回:
StdioServerParameters 对象,告诉 MCP client 如何启动 server。
"""
# sys.executable 是当前 Python 解释器路径,保证 client 和 server 使用同一个虚拟环境。
python_executable = sys.executable
# __file__ 是当前这个 mcp_agent.py 文件路径。
current_file = __file__
# StdioServerParameters 描述一个通过标准输入/输出通信的 MCP server 子进程。
return StdioServerParameters(
# command 是要执行的命令,这里就是当前 Python 解释器。
command=python_executable,
# args 是命令参数;相当于运行:python mcp_agent.py --server。
args=[current_file, "--server"],
# env 把当前环境变量传给子进程,保证 server 也能读取必要配置。
env=os.environ.copy(),
)
async def print_mcp_capabilities(self, session: ClientSession) -> None:
"""
打印 MCP server 暴露的 prompts 和 tools。
参数:
session: 已初始化的 MCP ClientSession。
"""
# list_prompts 会向 MCP server 请求可用 prompt 列表。
prompts_response = await session.list_prompts()
# 打印 prompt 列表,方便观察 MCP 的“发现能力”。
print("\n===== MCP Server 可用 Prompts =====")
for prompt in prompts_response.prompts:
# prompt.name 是 prompt 名称;prompt.description 是 docstring 生成的说明。
print(f"- {prompt.name}: {prompt.description}")
# list_tools 会向 MCP server 请求可用 tool 列表。
tools_response = await session.list_tools()
# 打印工具名称、描述和输入 schema,帮助看清 MCP 如何描述工具。
print("\n===== MCP Server 可用 Tools =====")
for tool in tools_response.tools:
print(f"- name: {tool.name}")
print(f" description: {tool.description}")
print(f" inputSchema: {json.dumps(tool.inputSchema, ensure_ascii=False)}")
async def get_prompt_from_mcp(self, session: ClientSession, user_question: str) -> str:
"""
从 MCP server 获取 prompt 模板。
参数:
session: MCP 客户端会话。
user_question: 用户问题。
返回:
prompt 文本,用于放入 OpenAI messages。
"""
# get_prompt 调用 MCP prompt,并传入 prompt 所需参数。
prompt_result = await session.get_prompt(
# 这里的名字必须与 @mcp.prompt() 装饰的函数名一致。
"weather_agent_prompt",
# arguments 是 prompt 参数字典。
arguments={"user_question": user_question},
)
# prompt_result.messages 是 MCP 返回的一组 prompt message。
first_message = prompt_result.messages[0]
# first_message.content 通常是 TextContent,其中 text 字段是真正文本。
content = first_message.content
# isinstance 用于判断 content 是否为 MCP TextContent 类型。
if isinstance(content, types.TextContent):
return content.text
# 兜底:如果 SDK 返回了其他内容类型,就转成字符串。
return str(content)
async def convert_mcp_tools_to_openai_tools(self, session: ClientSession) -> list[dict[str, Any]]:
"""
把 MCP tools 转换成 OpenAI Chat Completions tools 格式。
参数:
session: MCP 客户端会话。
返回:
OpenAI tools 列表。
为什么需要转换:
MCP server 能发现工具,但 OpenAI Chat Completions API 需要的是自己的 tools JSON 格式。
因此 Agent 客户端要做一层适配。
"""
# 从 MCP server 动态获取工具列表。
tools_response = await session.list_tools()
# 准备一个空列表,用来保存转换后的 OpenAI tool schema。
openai_tools: list[dict[str, Any]] = []
# 遍历 MCP server 暴露的每一个工具。
for tool in tools_response.tools:
# MCP v1 中工具入参 schema 字段通常叫 inputSchema。
input_schema = tool.inputSchema
# 按 OpenAI Chat Completions 的格式组织 function tool。
openai_tools.append(
{
"type": "function",
"function": {
# 工具名沿用 MCP tool.name。
"name": tool.name,
# 工具说明沿用 MCP tool.description;如果为空则给空字符串。
"description": tool.description or "",
# 参数 schema 直接使用 MCP 生成的 inputSchema。
"parameters": input_schema,
},
}
)
# 返回可传给 OpenAI API 的工具定义。
return openai_tools
def call_llm(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> Any:
"""
调用 LLM,让模型基于 MCP 工具 schema 决定是否 tool call。
参数:
messages: 对话历史。
tools: 从 MCP tools 转换来的 OpenAI tools。
返回:
assistant message,可能包含自然语言 content,也可能包含 tool_calls。
"""
# 调用 Chat Completions API。
response = self.client.chat.completions.create(
# 指定模型。
model=self.model,
# 传入当前上下文。
messages=messages,
# 传入从 MCP server 动态发现并转换来的工具定义。
tools=tools,
# 让模型自动决定是否调用工具。
tool_choice="auto",
)
# 取第一条候选输出。
return response.choices[0].message
async def call_mcp_tool(self, session: ClientSession, tool_name: str, arguments: dict[str, Any]) -> str:
"""
调用 MCP server 上的某个工具。
参数:
session: MCP 客户端会话。
tool_name: 工具名,例如 get_weather。
arguments: 工具参数,例如 {"city": "北京"}。
返回:
JSON 字符串,用作 OpenAI role=tool 的消息内容。
"""
# session.call_tool 会通过 MCP 协议把工具名和参数发给 server 执行。
result = await session.call_tool(tool_name, arguments=arguments)
# 如果工具执行报错,isError 通常会为 True;这里转成 JSON 返回给模型。
if getattr(result, "isError", False):
return json.dumps({"error": self.extract_mcp_text_result(result)}, ensure_ascii=False)
# MCP v1 的结构化结果字段通常是 structuredContent。
structured_content = getattr(result, "structuredContent", None)
# 如果存在结构化结果,优先返回结构化 JSON。
if structured_content:
return json.dumps(structured_content, ensure_ascii=False)
# 兜底:如果没有 structuredContent,就从 content 中提取文本。
return json.dumps({"text": self.extract_mcp_text_result(result)}, ensure_ascii=False)
def extract_mcp_text_result(self, result: Any) -> str:
"""
从 MCP CallToolResult 中提取文本内容。
参数:
result: MCP 工具调用结果。
返回:
拼接后的文本。
"""
# 准备一个列表保存所有文本片段。
texts: list[str] = []
# result.content 是 MCP 内容块列表,可能包含文本、图片、资源等不同类型。
for block in result.content:
# 如果内容块是 TextContent,就取 block.text。
if isinstance(block, types.TextContent):
texts.append(block.text)
# 如果是其他类型,这里简单转成字符串,真实项目中可按类型分别处理。
else:
texts.append(str(block))
# 用换行拼接多个文本片段。
return "\n".join(texts)
async def run(self, user_question: str) -> str:
"""
运行完整 MCP Agent。
参数:
user_question: 用户问题。
返回:
最终中文回答。
"""
# 构造 stdio server 参数,准备启动 MCP server 子进程。
server_params = self.build_server_params()
# stdio_client 会启动 MCP server 子进程,并返回读写流。
async with stdio_client(server_params) as (read_stream, write_stream):
# ClientSession 封装了 MCP 初始化、工具发现、工具调用等能力。
async with ClientSession(read_stream, write_stream) as session:
# 初始化 MCP 连接;没有这一步,后续 list_tools/call_tool 不能正常工作。
await session.initialize()
# 打印 MCP server 暴露的工具和 prompt,方便你观察 MCP 的发现过程。
await self.print_mcp_capabilities(session)
# 从 MCP server 获取 prompt 模板。
system_prompt = await self.get_prompt_from_mcp(session, user_question)
# 把 MCP tools 转成 OpenAI tools,供模型选择调用。
openai_tools = await self.convert_mcp_tools_to_openai_tools(session)
# 构造初始对话历史。
messages: list[dict[str, Any]] = [
# system 消息放 MCP prompt 返回的提示词。
{"role": "system", "content": system_prompt},
# user 消息放用户原始问题。
{"role": "user", "content": user_question},
]
# 最多循环 5 轮,防止模型无限调用工具。
for _ in range(5):
# 把 messages 和 MCP 转换出的 tools 发给模型。
assistant_message = self.call_llm(messages, openai_tools)
# 将 assistant message 加入上下文,保留模型发起的 tool_calls。
messages.append(assistant_message.model_dump(exclude_none=True))
# 如果没有 tool_calls,说明模型给出了最终回答。
if not assistant_message.tool_calls:
return assistant_message.content or ""
# 遍历模型请求的每个工具调用。
for tool_call in assistant_message.tool_calls:
# 读取模型想调用的工具名。
tool_name = tool_call.function.name
# 解析模型生成的 JSON 参数字符串。
arguments = json.loads(tool_call.function.arguments)
# 打印调用信息,方便你观察“模型决定调用什么工具”。
print("\n===== LLM 请求调用 MCP Tool =====")
print(f"tool_name = {tool_name}")
print(f"arguments = {arguments}")
# 通过 MCP 协议调用 server 上的工具。
tool_result_json = await self.call_mcp_tool(session, tool_name, arguments)
# 打印 MCP 工具返回值,方便调试。
print("===== MCP Tool 返回结果 =====")
print(tool_result_json)
# 把 MCP 工具结果作为 role=tool 消息发回模型。
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result_json,
}
)
# 如果超过最大轮数,返回保护性提示。
return "工具调用轮数过多,Agent 已停止。"
# 只有直接运行本文件时,才进入这个分支。
if __name__ == "__main__":
# 如果命令行参数中包含 --server,说明当前进程要作为 MCP server 运行。
if "--server" in sys.argv:
# 使用 stdio transport 启动 MCP server;client 会通过标准输入输出与它通信。
mcp.run(transport="stdio")
else:
# 否则当前进程作为 MCP Agent client 运行。
agent = MCPAgent()
# 准备 demo 问题;可以改成 input("请输入问题:")。
question = "请查询北京天气,并把摄氏温度换算成华氏温度,最后给出穿衣建议。"
# asyncio.run 用于运行异步 main 流程。
answer = asyncio.run(agent.run(question))
# 打印最终回答。
print("\n===== MCP Agent 最终回答 =====")
print(answer)
===== MCP Server 可用 Prompts =====
- weather_agent_prompt:
MCP Prompt:返回一个可复用的天气 Agent 提示模板。
参数:
user_question: 用户原始问题。
返回:
一段提示词文本。
说明:
@mcp.prompt() 和 @mcp.tool() 类似,但它暴露的不是可执行函数,
而是“提示模板”。MCP client 可以先发现 prompt,再按需获取 prompt 内容。
===== MCP Server 可用 Tools =====
- name: get_weather
description:
MCP 工具:查询指定城市的演示天气信息。
参数:
city: 城市名,例如“北京”“上海”“深圳”“杭州”。
返回:
dict 格式的天气信息。
说明:
@mcp.tool() 会把这个普通 Python 函数注册为 MCP 工具。
MCP server 会根据函数签名 city: str 和 docstring 自动生成工具 schema。
inputSchema: {"properties": {"city": {"title": "City", "type": "string"}}, "required": ["city"], "title": "get_weatherArguments", "type": "object"}
- name: celsius_to_fahrenheit
description:
MCP 工具:把摄氏温度转换为华氏温度。
参数:
celsius: 摄氏温度。
返回:
dict,包含原摄氏温度和华氏温度。
inputSchema: {"properties": {"celsius": {"title": "Celsius", "type": "number"}}, "required": ["celsius"], "title": "celsius_to_fahrenheitArguments", "type": "object"}
[07/06/26 14:36:57] INFO HTTP Request: POST _client.py:1025
https://your-llm-provider.com/v1/chat/completions
"HTTP/1.1 200 OK"
===== LLM 请求调用 MCP Tool =====
tool_name = get_weather
arguments = {'city': '北京'}
===== MCP Tool 返回结果 =====
{"city": "北京", "condition": "晴", "temperature_c": 30.0, "humidity": "35%", "source": "demo_mcp_weather_server"}
[07/06/26 14:36:58] INFO HTTP Request: POST _client.py:1025
https://your-llm-provider.com/v1/chat/completions
"HTTP/1.1 200 OK"
===== LLM 请求调用 MCP Tool =====
tool_name = celsius_to_fahrenheit
arguments = {'celsius': 30}
===== MCP Tool 返回结果 =====
{"celsius": 30.0, "fahrenheit": 86.0}
[07/06/26 14:37:01] INFO HTTP Request: POST _client.py:1025
https://your-llm-provider.com/v1/chat/completions
"HTTP/1.1 200 OK"
===== MCP Agent 最终回答 =====
北京当前天气:晴,气温 30°C,湿度 35%。
换算成华氏温度:86°F。
穿衣建议:天气较热,建议穿轻薄、透气的夏装,如短袖、短裤/薄长裤;注意防晒,外出可带太阳镜和帽子,并及时补水。
以上流程说明完整链路跑通:
用户问题
↓
LLM 判断需要天气工具
↓
调用 MCP tool: get_weather
↓
拿到北京天气
↓
LLM 判断需要温度换算工具
↓
调用 MCP tool: celsius_to_fahrenheit
↓
拿到华氏温度
↓
LLM 生成最终中文回答
用了MCP后,不再“在 Agent 里手写和维护 tool 的实现与 schema”,而是“MCP server 暴露 tools → Agent 动态发现 tools → bind/传给 LLM → LLM 返回 tool_call → Agent 通过 MCP 调 server”。LLM 仍然需要看到 tool 的 name、description、input schema,否则它无法决定调用哪个工具以及如何填参数。
MCP仍然需要写工具函数,工具函数总得实现,其优势是:工具函数一旦写成 MCP server,之后可以被多个 Agent、多个框架、多个模型客户端复用(直接复用接口,而不需要再自行维护工具)。即项目结构可以是:
weather_mcp_server.py
├── get_weather()
└── celsius_to_fahrenheit()
openai_agent.py
└── 不写 get_weather,只连接 MCP server
langchain_agent.py
└── 不写 get_weather,只连接 MCP server
langgraph_agent.py
└── 不写 get_weather,只连接 MCP server
claude_desktop_config.json
└── 也可以连接同一个 MCP server
而不是
function_call_agent.py
├── get_weather()
└── celsius_to_fahrenheit()
mcp_agent.py
├── get_weather()
└── celsius_to_fahrenheit()
MCP 客户端支持多种连接方式,最常用的是 Stdio 模式(通过标准输入输出与本地进程通信)。连接成功后,第一步通常是查询服务器提供了哪些工具。调用工具时,只需提供工具名称和符合 JSON Schema 的参数。
(个人理解:mcp的作用相当于tool的docker,tool注册在mcp server上并对外提供接口,agent通过其暴露在client端的接口来访问)
MCP 社区生态
MCP 协议的一个巨大优势是丰富的社区生态。Anthropic 和社区开发者已经创建了大量现成的 MCP 服务器,涵盖文件系统、数据库、API 服务等各种场景。这意味着不需要从零开始编写工具适配器,可以直接使用这些经过验证的服务器。
这里给出 MCP 社区的四个资源库:
- Awesome MCP Servers (https://github.com/punkpeye/awesome-mcp-servers)
- 社区维护的 MCP 服务器精选列表
- 包含各种第三方服务器
- 按功能分类,易于查找
- MCP Servers Website (Awesome MCP Servers)
- 官方 MCP 服务器目录网站
- 提供搜索和筛选功能
- 包含使用说明和示例
- Official MCP Servers (https://github.com/modelcontextprotocol/servers)
- Anthropic 官方维护的服务器
- 质量最高、文档最完善
- 包含常用服务的实现
- Smithery(https://smithery.ai/)类似于 Python 的 PyPI 或 Node.js 的 npm。通过 Smithery,用户可以:
- 发现和搜索 MCP 服务器
- 一键安装 MCP 服务器
- 查看服务器的使用统计和评价
- 自动获取服务器更新
常用官方 MCP 服务器
社区热门 MCP 服务器
MCP有以下传输方式:
如果是 stdio 模式(上面的Demo样例):通常不需要先手动运行 server,这种方式下client 自动用当前 Python 解释器启动 server.py;如果是 HTTP / SSE / Streamable HTTP 模式(MCP server 是网络服务):需要先启动 server,然后另开一个终端运行 client,这种模式类似普通 Web API。
3.3.3.A2A协议
MCP 协议解决了智能体与工具的交互,而 A2A 协议则解决智能体之间的协作问题。在一个需要多智能体(如研究员、撰写员、编辑)协作的任务中,它们需要通信、委托任务、协商能力和同步状态。
A2A(Agent-to-Agent Protocol)协议由 Google 团队提出,其核心设计理念是实现智能体之间的点对点通信。与 MCP 关注智能体与工具的通信不同,A2A 关注的是智能体之间如何相互协作。这种设计让智能体能够像人类团队一样进行对话、协商和协作。
A2A 的设计哲学是"对等通信"。在 A2A 网络中,每个智能体既是服务提供者,也是服务消费者。智能体可以主动发起请求,也可以响应其他智能体的请求。这种对等的设计避免了中心化协调器的瓶颈,让智能体网络更加灵活和可扩展。
A2A 请求生命周期是一个序列,详细说明了请求遵循的四个主要步骤:代理发现、身份验证、发送消息 API 和发送消息流 API。
由于A2A和ANP的社区目前还不是很完善,并且个人项目用不太到这两个协议(招聘JD中对这两个协议的要求也不多),故这里就不准备Demo了。Hello-Agent教程用他们的框架模拟了这两个协议作为Demo,教程链接。
3.3.4.ANP协议
ANP(Agent Network Protocol)是一个概念性的协议框架,目前由开源社区维护,还没有成熟的生态,其核心设计理念是构建大规模智能体网络的基础设施。如果说 MCP 解决的是"如何访问工具",A2A 解决的是"如何与其他智能体对话",那么 ANP 解决的是"如何在大规模网络中发现和连接智能体"。
ANP 的设计哲学是"去中心化服务发现"。在一个包含成百上千个智能体的网络中,如何让智能体能够找到它需要的服务?ANP 提供了服务注册、发现和路由机制,让智能体能够动态地发现网络中的其他服务,而不需要预先配置所有的连接关系。
3.3.5.三种协议比较
目前MCP的生态相对成熟,不过各种工具的时效性取决于维护者,选择协议的关键在于开发需求本身:
- 如果你的智能体需要访问外部服务(文件、数据库、API),选择MCP
- 如果你需要多个智能体相互协作完成任务,选择A2A
- 如果你要构建大规模的智能体生态系统,考虑ANP
3.4.Agentic-RL
智能体处理更复杂的任务时表现不佳,自然会有疑问:如何让智能体具备更强的推理能力?如何让智能体学会更好地使用工具?如何让智能体能够自我改进?本章引入强化学习训练能力,将从 LLM 训练的基础知识开始,逐步深入到监督微调(Supervised Fine-Tuning,SFT)、群组相对策略优化(Group Relative Policy Optimization, GRPO)等实用技术,最终构建一个完整的智能体训练 pipeline。(该章做为概念学习等辅助作用,不会涉及特别复杂的数学公式,对Hello-Agent中的概念有些许删减)
3.4.1.从LLM训练到Agentic-RL
强化学习(Reinforcement Learning, RL)是一种专注于解决序贯决策问题的学习范式,它通过智能体与环境的直接交互,在"试错"中学习如何最大化长期收益。
传统的监督学习方法存在三个核心局限:一是数据质量完全决定训练质量,模型只能模仿训练数据,难以超越;二是缺乏探索能力,只能被动学习人类提供的路径;三是难以优化长期目标,无法精确优化多步推理的中间过程。强化学习提供了新的可能性。通过让智能体自主生成多个候选答案并根据正确性获得奖励,它可以学习哪些推理路径更优、哪些步骤是关键,甚至发现比人类标注更好的解题方法。这就是 Agentic RL 的核心思想:将 LLM 作为可学习策略,嵌入智能体的感知-决策-执行循环,通过强化学习优化多步任务表现。
- LLM 是策略:policy: state → action(当前上下文 → 下一步文本 / 下一步工具调用)
- LLM 是可学习的:
- 理论上可以更新 LLM 参数(用 RLHF / PPO / DPO / GRPO 等方法微调 LLM)
- 也可以优化 Agent 的行为策略(用奖励模型优化,检索/反思/多步等策略)
- Agentic RL 是把 Agent 的多步行为放进强化学习框架里优化。
我们可以将多步推理任务(例如数学问题)映射到强化学习框架:
- 智能体:基于 LLM 的推理系统
- 环境:数学问题和验证系统
- 状态:当前的问题描述和已有的推理步骤
- 行动:生成下一步推理或最终答案
- 奖励:答案是否正确(正确+1,错误 0)
一个强大的 LLM(如 GPT、Claude、Qwen)的诞生,通常要经历两个主要阶段:预训练(Pretraining)和后训练(Post-training)。这两个阶段构成了 LLM 从"语言模型"到"对话助手"的完整演化路径。

各个阶段的简要介绍可以看我之前的博客论文串读--OpenAI-GPT系列1.2.3.4--大语言模型标准架构和预训练流程-CSDN博客和论文精读--《Training LMs to follow instructions with human feedback》--InstructGPT:大模型SFT+RLFH(PPO)详解,含代码_instructgpt论文-CSDN博客
Agentic RL核心理念
传统的后训练(我们称之为 PBRFT: Preference-Based Reinforcement Fine-Tuning)主要关注单轮对话的质量优化:给定一个用户问题,模型生成一个回答,然后根据回答的质量获得奖励。这种方式适合优化对话助手,但对于需要多步推理、工具使用、长期规划的智能体任务来说,就显得力不从心了。Agentic RL则是一种新的范式,它将 LLM 视为一个可学习的策略,嵌入在一个顺序决策循环中。在这个框架下,智能体需要在动态环境中与外部世界交互,执行多步行动来完成复杂任务,获得中间反馈来指导后续决策,优化长期累积奖励而非单步奖励。
通过一个具体例子来理解这个区别。在 PBRFT 场景中,用户问"请解释什么是强化学习",模型生成完整回答,然后根据回答质量直接给分。而在 Agentic RL 场景中,用户请求"帮我分析这个 GitHub 仓库的代码质量",智能体需要经历多个步骤:首先调用 GitHub API 获取仓库信息,成功获得仓库结构和文件列表,得到+0.1 的奖励;然后读取主要代码文件,成功获得代码内容,得到+0.1 的奖励;接着分析代码质量合理,得到+0.2 的奖励;最后生成分析报告质量高,得到+0.6 的奖励。总奖励是所有步骤的累积:1.0。可以看到,Agentic RL 的关键特征是多步交互、每一步的行动都会改变环境状态、每一步都可以获得反馈、优化整个任务的完成质量。


PBRFT 思维关注"如何让模型生成更好的单个回答",优化回答质量,关注语言表达,进行单步决策。而 Agentic RL 思维关注"如何让智能体完成复杂任务",优化任务完成度,关注行动策略,进行多步规划。这种转变使得 LLM 从"对话助手"进化为"自主智能体",能够主动寻找信息、知道何时、如何使用外部工具、为了最终目标,愿意执行看似"绕路"的中间步骤、从错误学习。

- 推理(Reasoning)是指从给定信息中逻辑地得出结论的过程,是智能体的核心能力。传统的 CoT 提示方法依赖少样本示例,泛化能力有限;SFT 只能模仿训练数据中的推理模式,难以创新。强化学习的优势在于通过试错学习有效的推理策略,发现训练数据中没有的推理路径,学会何时需要深度思考、何时可以快速回答。推理任务可以建模为序列决策问题,给定问题 q,智能体需要生成推理链 c=(c1,c2,...,cn) 和最终答案 a。奖励函数通常设计为 r(q,c,a)=1 if a=a∗ else 0,训练目标是 maxθEq,(c,a)∼πθ[r(q,c,a)]。通过这种方式,模型学会生成高质量的推理链,而不仅仅是记忆答案。
- 工具使用(Tool Use)是指智能体调用外部工具来完成任务的能力。在工具使用任务中,行动空间扩展为 at∈{atthink,attool},其中 atthink 是生成思考过程,attool=(tool_name,arguments) 是调用工具。强化学习让智能体学会何时需要使用工具、选择哪个工具、如何组合多个工具。例如,在解决数学问题时,智能体需要学会何时使用计算器、何时使用代码解释器、何时直接推理。
- 记忆(Memory)是指智能体保持和重用过去信息的能力,对于长期任务至关重要。LLM 的上下文窗口有限,静态检索策略(如 RAG)无法针对任务优化。强化学习让智能体学会记忆管理策略:决定哪些信息值得记住、何时更新记忆、何时删除过时信息。这类似于人类的工作记忆,我们会主动管理大脑中的信息,保留重要的、遗忘无关的。
- 规划(Planning)是指制定行动序列以达成目标的能力。传统的 CoT 是线性思考,无法回溯;提示工程使用静态规划模板,难以适应新情况。强化学习让智能体学会动态规划:通过试错发现有效的行动序列,学会权衡短期和长期收益。例如,在多步任务中,智能体可能需要先执行一些看似"绕路"的步骤,例如收集信息,才能最终完成任务。
- 自我改进(Self-Improvement)是指智能体回顾自身输出、纠正错误并优化策略的能力。强化学习让智能体学会自我反思:识别自己的错误、分析失败原因、调整策略。这种能力使得智能体能够在没有人工干预的情况下持续改进,类似于人类的"从错误中学习"。
- 感知(Perception)是指理解多模态信息的能力。例如,强化学习可以提升视觉推理能力,让模型学会使用视觉工具,学会视觉规划。这使得智能体不仅能理解文本,还能理解和操作视觉世界。
3.4.2.SFT

在开始强化学习之前,需要先进行 SFT 训练。这是因为预训练模型虽然具备强大的语言能力,但它并不知道如何完成特定任务。预训练模型的训练目标是预测下一个词,而不是解决数学问题或使用工具。预训练模型的输出格式是自由文本,而我们需要结构化的输出(如"Step 1: ..., Step 2: ..., Final Answer: ...")。预训练模型没有见过任务相关的数据,不知道什么是"好的"推理过程。
SFT 的作用是教会模型任务的基本规则。首先,学习输出格式,让模型知道如何组织答案(如使用"Step 1", "Final Answer"等标记)。其次,学习推理模式,通过示例学习如何分解问题、逐步推导。再次,建立基线能力,为后续的强化学习提供一个合理的起点。最后,减少探索空间,强化学习不需要从零开始,可以在 SFT 的基础上优化。
什么时候需要用到强化学习,什么时候SFT就足够了
RL前通常都需要SFT。
- SFT 适合:你知道“标准答案/标准行为”长什么样(教模型“别人是怎么做的”,普通问答、改写)。
- RL 适合:你不容易写出标准答案,但能判断结果好不好(让模型“自己试,做得好就奖励”,多步复杂规划)。
也可以这样判断(来自xhs):
- 大模型能做对,但你想让小模型/私有模型也学会:→ SFT 很合适。
- 大模型偶尔做对,但不稳定:→ 先收集高质量轨迹做 SFT;如果还是不稳定,再考虑 RL。
- 大模型也很难直接做对,但结果可以自动验证:→ 可以考虑 RL 或 search + verifier + RL。
- 大模型能生成很多候选,你能判断哪个更好,但很难写标准答案:→ DPO / RLHF / RL 更合适。
LoRA
直接微调整个模型需要大量的计算资源和显存。对于 Qwen3-0.6B(0.6B 参数),全量微调需要约 12GB 显存(FP16)或 24GB 显存(FP32)。对于更大的模型(如 7B、13B),全量微调几乎不可能在消费级 GPU 上进行。
LoRA(Low-Rank Adaptation)是一种参数高效微调方法,它只训练少量的额外参数,而保持原模型参数冻结。LoRA 的核心思想是:模型微调时的参数变化可以用低秩矩阵表示。具体介绍和公式原理可以看我之前的这篇博客大模型微调、优化与评估(什么是大模型,知识外挂RAG,前缀微调prefix-tuning,低秩适应方法LoRA、QLora,微调方法对比总结,迁移学习,领域适应,评估指标BLEU、ROUGE)_rag评估bleu-CSDN博客

LoRA 的关键超参数包括:
- 秩(rank,r),控制 LoRA 矩阵的秩,越大表达能力越强,但参数量也越多,典型值为 4-64,默认 8;
- Alpha(α),LoRA 的缩放因子,实际更新为 ΔW=α/r*BA,控制 LoRA 的影响强度,典型值等于 rank;
- 目标模块(target_modules),指定哪些层应用 LoRA,通常选择注意力层(q_proj, k_proj, v_proj, o_proj),也可以包括 MLP 层(gate_proj, up_proj, down_proj)。
- 可以把 Transformer 里一层大致看成两块:Attention 部分负责“看上下文里哪些 token 重要”;MLP / FFN 部分:负责“对信息做进一步加工和变换”
q是 query,意思是“我要找什么信息”(比如模型正在生成某个词,它会用q_proj生成一个查询向量:“我现在需要什么上下文信息?”)。k是 key,意思是“我这里有什么信息”。(每个 token 会通过k_proj生成自己的 key:“我这个 token 可以提供什么线索?”)v是 value,意思是“真正要取走的信息”。(v 决定“从被看的 token 里拿什么信息”)o是 output projection。(o_proj 负责把 attention 的结果整理后送回模型主干。)up_proj会把 hidden size 升到更大的中间维度,方便做复杂变换。gate_proj是门控层,它决定哪些信息应该通过、哪些信息应该被抑制。down_proj把扩大的中间维度再压回 hidden size(主干需要的维度)。- LoRA通常就加在这些“大量参与模型能力表达”的线性层上。Attention 决定模型如何利用上下文(第一优先级,尤其q、v)。MLP 更像模型内部的“知识加工器”和“模式变换器”(其次优先级)。
可以采用Llama-Factory现成的框架做微调,它提供web-cli,不用自己写代码只用填入参数。
3.4.3.PPO、DPO、GRPO
在强化学习领域,PPO(Proximal Policy Optimization)是最经典的算法之一。PPO 通过限制策略更新的幅度,保证训练的稳定性。但是,PPO 在 LLM 训练中存在一些问题:需要训练 Value Model(价值模型),增加了训练复杂度和显存占用;需要同时维护四个模型(Policy Model、Reference Model、Value Model、Reward Model),工程实现复杂;训练不稳定,容易出现奖励崩塌或策略退化。
除了 PPO 之外,DPO(Direct Preference Optimization)也是 LLM 对齐训练中常见的方法。DPO 不再显式训练 Reward Model,也不需要像 PPO 那样通过复杂的强化学习循环更新策略,而是直接使用成对偏好数据进行训练,例如给定同一个问题下的较优回答和较差回答,让模型提高生成较优回答的概率、降低生成较差回答的概率。相比 PPO,DPO 的工程实现更简单、训练更稳定,适合偏好数据较充足的场景;但它主要依赖离线偏好样本,缺少 PPO/GRPO 这类方法在在线采样和可验证任务奖励优化方面的灵活性。
GRPO(Group Relative Policy Optimization)是一种简化的 PPO 变体,专门为 LLM 设计。GRPO 的核心思想是:不需要 Value Model,使用组内相对奖励代替绝对奖励;简化训练流程,只需要 Policy Model 和 Reference Model;提高训练稳定性,减少奖励崩塌的风险。
Llama-Factory也提供PPO和DPO微调的功能。




3.5.Skills
3.5.1.什么是Agent Skills
Agent Skills 是一种标准化的程序性知识封装格式。如果说 MCP 为智能体提供了"手"来操作工具,那么 Skills 就提供了"操作手册"或"SOP(标准作业程序)",教导智能体如何正确使用这些工具。
这种设计理念源于一个简单但深刻的洞察:连接性(Connectivity)与能力(Capability)应该分离。MCP 专注于前者,Skills 专注于后者。这种职责分离带来了清晰的架构优势:
- MCP 的职责:提供标准化的访问接口,让智能体能够"够得着"外部世界的数据和工具
- Skills 的职责:提供领域专业知识,告诉智能体在特定场景下"如何组合使用这些工具"
Agent Skills vs MCP:本质区别与协作关系
用一个类比来理解:MCP 像是 USB 接口或驱动程序,它定义了设备如何连接;而 Skills 像是软件应用程序,它定义了如何使用这些连接的设备来完成具体任务。你可以拥有一个功能完善的打印机驱动(MCP),但如果没有告诉你如何在 Word 里设置页边距和双面打印(Skill),你仍然无法高效地完成打印任务。
- MCP:解决“Agent 能不能连接外部工具 / API / 数据源”
- Skills:解决“Agent 知不知道该如何正确使用这些能力”
假设你要构建一个智能体来帮助团队进行代码审查:
MCP 让智能体"能够"访问 GitHub,能够调用这些 API。但它不知道"应该"做什么。
# MCP 提供对 GitHub 的标准化访问
github_mcp = MCPTool(server_command=["npx", "-y", "@modelcontextprotocol/server-github"])
# MCP 暴露的工具(简化示例):
# - list_pull_requests(repo, state)
# - get_pull_request_details(pr_number)
# - list_pr_comments(pr_number)
# - create_pr_comment(pr_number, body)
# - get_file_content(repo, path, ref)
# - list_pr_files(pr_number)
Skills 告诉智能体"应该"做什么、如何组织审查流程、需要关注哪些公司特定的规范。它是领域知识和最佳实践的容器:
---
name: code-review-workflow
description: 执行标准的代码审查流程,包括检查代码风格、安全问题、测试覆盖率等
---
# 代码审查工作流
## 审查清单
当执行代码审查时,按以下步骤进行:
1. **获取 PR 信息**:调用 `get_pull_request_details` 了解变更背景
2. **分析变更文件**:调用 `list_pr_files` 获取文件列表
3. **逐文件审查**:
- 对于 `.py` 文件:检查是否符合 PEP 8,是否有明显的性能问题
- 对于 `.js/.ts` 文件:检查是否有未处理的 Promise,是否使用了废弃的 API
- 对于测试文件:验证是否覆盖了新增的代码路径
4. **安全检查**:
- 是否硬编码了敏感信息(密钥、密码)
- 是否有 SQL 注入或 XSS 风险
5. **提供反馈**:
- 严重问题:使用 `create_pr_comment` 直接评论
- 建议改进:在总结中提出
## 公司特定规范
- 所有数据库查询必须使用参数化查询
- API 端点必须有权限验证装饰器
- 新功能必须附带单元测试(覆盖率 > 80%)
## 示例评论模板
**严重问题**:
⚠️ 安全风险:第 45 行直接拼接 SQL 字符串,存在注入风险。
建议改用参数化查询:`cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))`
在上下文管理策略上也有本质差异:
- Agent Skills 最核心的创新是渐进式披露(Progressive Disclosure)机制。这种机制将技能信息分为三个层次,智能体按需逐步加载,既确保必要时不遗漏细节,又避免一次性将过多内容塞入上下文窗口。
Agent Skills 渐进式披露三层架构:
- 第一层:元数据(Metadata)
- 在 Skills 的设计中,每个技能都存放在一个独立的文件夹中,核心是一个名为
SKILL.md的 Markdown 文件。这个文件必须以 YAML 格式的 Frontmatter 开头,定义技能的基本信息。 - 当智能体启动时,它会扫描所有已安装的技能文件夹,仅读取每个
SKILL.md的 Frontmatter 部分,将这些元数据加载到系统提示词中。根据实测数据,每个技能的元数据仅消耗约 100 个 token。即使你安装了 50 个技能,初始的上下文消耗也只有约 5,000 个 token。 - 这与 MCP 的工作方式形成了鲜明对比。在典型的 MCP 实现中,当客户端连接到一个服务器时,通常会通过
tools/list请求获取所有可用工具的完整 JSON Schema,可能立即消耗数万个 token。
- 在 Skills 的设计中,每个技能都存放在一个独立的文件夹中,核心是一个名为
- 第二层:技能主体(Instructions)
- 当智能体通过分析用户请求,判断某个技能与当前任务高度相关时,它会进入第二层加载。此时,智能体会读取该技能的完整
SKILL.md文件内容,将详细的指令、注意事项、示例等加载到上下文中。 - 此时,智能体获得了完成任务所需的全部上下文:数据库结构、查询模式、注意事项等。这部分内容的 token 消耗取决于指令的复杂度,通常在 1,000 到 5,000 个 token 之间。
- 当智能体通过分析用户请求,判断某个技能与当前任务高度相关时,它会进入第二层加载。此时,智能体会读取该技能的完整
- 第三层:附加资源(Scripts & References)
- 对于更复杂的技能,
SKILL.md可以引用同一文件夹下的其他文件:脚本、配置文件、参考文档等。智能体仅在需要时才加载这些资源。
- 对于更复杂的技能,
这种设计有三个关键优势:
- 无限的知识容量:通过脚本和外部文件,技能可以"携带"远超上下文限制的知识。例如,一个数据分析技能可以附带一个 1GB 的数据文件和一个查询脚本,智能体通过执行脚本来访问数据,而无需将整个数据集加载到上下文中。
- 确定性执行:复杂的计算、数据转换、格式解析等任务交给代码执行,避免了 LLM 生成过程中的不确定性和幻觉问题。
- Token消耗量大幅减少:这种架构不仅大幅降低了初始成本,还使得对话过程中的上下文管理更加精准和高效。
Skills+MCP应该混合使用:
典型工作流:
- 用户问:"分析公司内部谁的话语权最高"
- Skills 层识别这是一个数据分析任务,加载
mysql-employees-analysis技能 - Skills 层根据技能指令,将任务分解为子步骤:查询管理关系、薪资对比、任职时长等
- MCP 层执行具体的 SQL 查询,返回结果
- Skills 层根据技能中的领域知识,解读数据并生成综合分析
- 返回结构化的答案给用户
这种架构的优势是:
- 关注点分离:MCP 专注于"能力",Skills 专注于"智慧"
- 成本优化:渐进式加载大幅降低 token 消耗
- 可维护性:业务逻辑(Skills)与基础设施(MCP)解耦
- 复用性:同一个 MCP 服务器可以被多个 Skills 使用
3.5.2.如何创建和使用 Skills
SKILL.md 文件的标准结构:
---
# === 必需字段 ===
name: skill-name
# 技能的唯一标识符,使用 kebab-case 命名
description: >
简洁但精确的描述,说明:
1. 这个技能做什么
2. 什么时候应该使用它
3. 它的核心价值是什么
# 注意:description 是智能体选择技能的唯一依据,必须写清楚!
# === 可选字段 ===
version: 1.0.0
# 语义化版本号
allowed_tools: [tool1, tool2]
# 此技能可以调用的工具列表(白名单)
required_context: [context_item1]
# 此技能需要的上下文信息
license: MIT
# 许可协议
author: Your Name <email@example.com>
# 作者信息
tags: [database, analysis, sql]
# 便于分类和搜索的标签
---
# 技能标题
## 概述
(对技能的详细介绍,包括使用场景、技术背景等)
## 前置条件
(使用此技能需要的环境配置、依赖项等)
## 工作流程
(详细的步骤说明,告诉智能体如何执行任务)
## 最佳实践
(经验总结、注意事项、常见陷阱等)
## 示例
(具体的使用案例,帮助智能体理解)
## 故障排查
(常见问题和解决方案)
根据 Anthropic 官方文档和社区最佳实践,编写有效的 Skills 需要遵循以下原则:
1. 精准的 Description。description 是智能体决策的关键。它应该:
- 精确定义适用范围:避免模糊的描述如"帮助处理数据"
- 包含触发关键词:让智能体能够匹配用户意图
- 说明独特价值:与其他技能区分开来
- 例如对于数据库查询,不应只写“处理数据库查询”,而要改为“ 将中文业务问题转换为 SQL 查询并分析 MySQL employees 示例数据库。 适用于员工信息查询、薪资统计、部门分析、职位变动历史等场景。 当用户询问关于员工、薪资、部门的数据时使用此技能。”
2. 模块化与单一职责。一个 Skill 应该专注于一个明确的领域或任务类型。如果一个 Skill 试图做太多事情,会导致:
- Description 过于宽泛,匹配精度下降
- 指令内容过长,浪费上下文
- 难以维护和更新
- 例如对于数据库查询,与其创建一个"通用数据分析"技能,不如创建多个专门的技能:
mysql-employees-analysis:专门分析 employees 数据库sales-data-analysis:专门分析销售数据user-behavior-analysis:专门分析用户行为数据
- 例如对于数据库查询,与其创建一个"通用数据分析"技能,不如创建多个专门的技能:
3. 确定性优先原则
- 对于复杂的、需要精确执行的任务,优先使用脚本而不是依赖 LLM 生成。
- 例如,在数据导出场景中,与其让 LLM 生成 Excel 二进制内容(容易出错),不如编写一个专门的脚本来处理这个任务,SKILL.md 中只需要指导智能体何时调用这个脚本即可。
4. 渐进式披露策略。合理利用三层结构,将信息按重要性和使用频率分层:
- SKILL.md 主体:放置核心工作流、常用模式
- 附加文档(如
advanced.md):放置高级用法、边缘情况 - 数据文件:放置大型参考数据,通过脚本按需查询
Demo及Skill的使用流程
Skill的使用流程一般是:
用户输入任务
↓
SkillLoader 读取所有 SKILL.md 的 frontmatter
↓
LLM 根据 description 选择 skill
↓
Harness 加载被选中 skill 的 SKILL.md 正文
↓
Harness 根据 allowed_tools 暴露工具
↓
LLM 判断是否需要调用工具
↓
ToolExecutor 检查工具是否在白名单中
↓
执行工具
↓
工具结果返回给 LLM
↓
LLM 继续推理或输出最终答案
以下的demo是自己想的,只是为了尝试Skills具体的使用,故没有很具体的应用。
包含两个skills:
- web-research:用 Tavily 做联网检索和资料整理。
- structured-writing:用于文本改写、润色、整理。
项目目录
skills_agent_demo/
├── .env
├── requirements.txt
├── main.py
├── agent/
│ ├── __init__.py
│ ├── llm.py
│ ├── skill_loader.py
│ ├── tools.py
│ └── harness.py
└── skills/
├── web-research/
│ ├── SKILL.md
│ ├── advanced.md
│ └── data/
│ └── source_policy.json
└── structured-writing/
├── SKILL.md
└── advanced.md
完整流程图
┌────────────────────┐
│ 用户输入任务 │
└─────────┬──────────┘
↓
┌────────────────────┐
│ SkillLoader │
│ 读取所有 SKILL.md │
│ 的 frontmatter │
└─────────┬──────────┘
↓
┌────────────────────┐
│ Skill Selector LLM │
│ 只看 name + │
│ description │
└─────────┬──────────┘
↓
┌────────────────────┐
│ 选中某个 Skill │
│ 例如 web-research │
└─────────┬──────────┘
↓
┌────────────────────┐
│ Harness 加载该 │
│ Skill 的正文 body │
└─────────┬──────────┘
↓
┌────────────────────┐
│ Harness 读取 │
│ allowed_tools │
│ 并暴露工具 schema │
└─────────┬──────────┘
↓
┌────────────────────┐
│ LLM 根据 Skill │
│ 决定下一步 │
└─────────┬──────────┘
↓
┌────────────────────────────┐
│ 可能 1:直接回答 │
│ 可能 2:调用 tavily_search │
│ 可能 3:读取 advanced.md │
│ 可能 4:查询 data 文件 │
└─────────┬──────────────────┘
↓
┌────────────────────┐
│ ToolExecutor 执行 │
│ 工具并返回结果 │
└─────────┬──────────┘
↓
┌────────────────────┐
│ 工具结果重新放回 │
│ messages │
└─────────┬──────────┘
↓
┌────────────────────┐
│ LLM 基于观察结果 │
│ 继续推理或最终回答 │
└────────────────────┘
Agent/harness.py
from __future__ import annotations
import json
import re
from typing import Any
from agent.llm import ChatLLM
from agent.skill_loader import Skill, SkillLoader
from agent.tools import ToolExecutor
class SkillAgentHarness:
"""
Skills Agent 的工程外壳。
你可以把它理解成:
LLM + Harness = Agent
LLM 负责:
1. 选择 skill。
2. 根据上下文生成回答。
3. 决定是否调用工具。
Harness 负责:
1. 加载 skills。
2. 控制 skill 选择流程。
3. 把选中的 SKILL.md 正文放进上下文。
4. 根据 allowed_tools 暴露工具。
5. 执行工具。
6. 把工具结果塞回消息列表。
7. 控制最大工具调用轮数。
"""
def __init__(
self,
llm: ChatLLM,
skill_loader: SkillLoader,
max_tool_steps: int = 6,
verbose: bool = True,
):
"""
初始化 Agent Harness。
参数:
llm:
ChatLLM 实例。
skill_loader:
SkillLoader 实例。
max_tool_steps:
最大工具调用轮数。
防止模型无限循环调用工具。
verbose:
是否打印中间过程。
"""
self.llm = llm
self.skill_loader = skill_loader
self.tool_executor = ToolExecutor(skill_loader)
self.max_tool_steps = max_tool_steps
self.verbose = verbose
def run(self, user_task: str) -> str:
"""
运行 Agent。
完整流程:
1. 根据用户任务选择 skill。
2. 打印被选中的 skill。
3. 使用被选中的 skill 执行任务。
4. 返回最终答案。
"""
selected_skill, reason = self._select_skill(user_task)
if self.verbose:
print("\n========== Skill Selection ==========")
print(f"Selected skill: {selected_skill.name}")
print(f"Reason: {reason}")
print("Allowed tools:", selected_skill.allowed_tools)
print("Available resources:", self.skill_loader.list_resources(selected_skill.name))
return self._run_with_skill(user_task, selected_skill)
def _select_skill(self, user_task: str) -> tuple[Skill, str]:
"""
选择最合适的 skill。
关键点:
选择阶段只给 LLM 看:
1. skill.name
2. skill.description
不给它看:
1. SKILL.md 正文
2. advanced.md
3. data 文件
这样可以模拟真实 Skills 系统中的第一层渐进式披露。
"""
skill_candidates = self.skill_loader.list_for_selection()
selector_messages = [
{
"role": "system",
"content": (
"你是一个 skill selector。\n"
"你必须只根据每个 skill 的 description 判断应该选择哪个 skill。\n"
"name 只作为返回用的唯一标识,不作为语义判断依据。\n"
"请返回严格 JSON,不要输出多余文字。\n"
"JSON 格式:{\"skill_name\": \"...\", \"reason\": \"...\"}"
),
},
{
"role": "user",
"content": json.dumps(
{
"user_task": user_task,
"skills": skill_candidates,
},
ensure_ascii=False,
indent=2,
),
},
]
# 这里不传 tools,因为 skill 选择只是一个分类任务
response = self.llm.chat(selector_messages, temperature=0.0)
content = response.choices[0].message.content or ""
# 尝试解析模型返回的 JSON
data = self._parse_json_object(content)
skill_name = data.get("skill_name")
reason = data.get("reason", "")
# 如果模型返回了合法 skill name,就使用它
if skill_name in self.skill_loader.skills:
return self.skill_loader.get(skill_name), reason
# 如果模型输出格式错误,使用一个简单 fallback,防止 demo 中断
fallback = self._fallback_select_skill(user_task)
return fallback, "LLM 返回的 skill_name 无效,使用关键词 fallback。"
def _fallback_select_skill(self, user_task: str) -> Skill:
"""
简单的关键词 fallback。
真实工程里可以替换成:
1. embedding 检索
2. BM25
3. 规则系统
4. 小模型分类器
这里保留 fallback 是为了让 demo 更稳。
"""
text = user_task.lower()
# 这些关键词更像联网研究任务
if any(
keyword in text
for keyword in [
"搜索",
"查找",
"最新",
"新闻",
"联网",
"资料",
"来源",
"调研",
"research",
"tavily",
"官网",
]
):
return self.skill_loader.get("web-research")
# 默认使用写作 skill
return self.skill_loader.get("structured-writing")
def _run_with_skill(self, user_task: str, skill: Skill) -> str:
"""
使用指定 skill 处理用户任务。
这个函数是 Agent 的核心 loop。
流程:
1. 读取当前 skill 可用资源列表。
2. 构造 system prompt。
3. 根据 allowed_tools 暴露工具 schema。
4. 调用 LLM。
5. 如果 LLM 返回 tool_calls,就执行工具。
6. 把工具结果追加到 messages。
7. 再次调用 LLM。
8. 如果 LLM 不再调用工具,就返回最终答案。
"""
# 只列出资源路径,不读取资源内容
resources = self.skill_loader.list_resources(skill.name)
# 选中 skill 后,才把 SKILL.md 主体放进上下文
system_prompt = f"""
你是一个支持 Skills 的 Agent。
当前已经触发的 Skill:
# Skill Name
{skill.name}
# Skill Description
{skill.description}
# Skill Body
{skill.body}
# 当前 Skill 允许使用的工具白名单
{json.dumps(skill.allowed_tools, ensure_ascii=False)}
# 当前 Skill 可按需读取的资源
{json.dumps(resources, ensure_ascii=False, indent=2)}
重要规则:
1. 你必须遵循当前 Skill 的工作流程。
2. 你只能调用 allowed_tools 中列出的工具。
3. 你不能假装已经读取 advanced.md 或 data 文件。
4. 如果需要高级策略、边缘情况或数据文件,必须调用 load_skill_resource 或 query_skill_data。
5. 如果工具返回错误,要先根据错误调整下一步,不要编造工具结果。
6. 最终回答使用中文,结构清晰。
""".strip()
messages: list[dict[str, Any]] = [
{
"role": "system",
"content": system_prompt,
},
{
"role": "user",
"content": user_task,
},
]
# 根据当前 skill 的 allowed_tools 获取工具 schema
# 注意:这里不会暴露所有工具,只暴露白名单内工具
tool_schemas = self.tool_executor.schemas_for(skill.allowed_tools)
# 最多循环 max_tool_steps 轮,防止无限调用工具
for step in range(1, self.max_tool_steps + 1):
# 调用 LLM
response = self.llm.chat(
messages=messages,
tools=tool_schemas,
tool_choice="auto" if tool_schemas else None,
temperature=0.2,
)
assistant_message = response.choices[0].message
# 如果模型返回 tool_calls,说明它想调用工具
tool_calls = assistant_message.tool_calls or []
# 如果没有 tool call,说明模型已经准备好最终回答
if not tool_calls:
return assistant_message.content or ""
# 把 assistant 的 tool call 消息追加到 messages
messages.append(assistant_message.model_dump(exclude_none=True))
# 逐个执行工具调用
for tool_call in tool_calls:
tool_name = tool_call.function.name
try:
arguments = json.loads(tool_call.function.arguments or "{}")
except json.JSONDecodeError:
arguments = {}
if self.verbose:
print(f"\n[Tool Step {step}] {tool_name}")
print(json.dumps(arguments, ensure_ascii=False, indent=2))
# 通过 ToolExecutor 执行工具
# ToolExecutor 会再次检查 allowed_tools 白名单
tool_result = self.tool_executor.execute(
skill=skill,
tool_name=tool_name,
arguments=arguments,
)
if self.verbose:
preview = tool_result[:1000]
print("[Tool Result Preview]")
print(preview + ("..." if len(tool_result) > 1000 else ""))
# 把工具结果以 role=tool 的形式加入 messages
# 下一轮 LLM 会基于这个观察结果继续推理
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result,
}
)
# 如果达到最大工具轮数还没结束,强制让模型总结已有信息
messages.append(
{
"role": "system",
"content": (
"工具调用次数已达到上限。"
"请基于已有信息给出最终答案。"
"如果信息不足,请明确说明不足之处。"
),
}
)
final_response = self.llm.chat(messages=messages, temperature=0.2)
return final_response.choices[0].message.content or ""
def _parse_json_object(self, text: str) -> dict[str, Any]:
"""
从模型输出中解析 JSON 对象。
理想情况下,模型会直接输出:
{"skill_name": "...", "reason": "..."}
但模型偶尔可能加上一些解释文字。
所以这里做两步解析:
1. 先尝试直接 json.loads。
2. 如果失败,再用正则提取第一个 {...}。
"""
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
pass
return {}
Agent/llm.py
import os
from typing import Any
from openai import OpenAI
class ChatLLM:
"""
OpenAI Chat Completions 的简单封装。
这个类只负责一件事:
调用 OpenAI 的 chat.completions.create 接口。
注意:
这里故意没有使用 LangChain。
这样可以更清楚地看到:
1. LLM 只是负责生成回复或 tool call。
2. Agent 的 skill 选择、tool 执行、循环控制,都是 Harness 在做。
"""
def __init__(self, model: str | None = None):
"""
初始化 LLM 客户端。
参数:
model:
可选模型名。
如果不传,则读取环境变量 LLM_MODEL_ID。
如果环境变量也没有,则默认使用 gpt-4.1-mini。
"""
# 从环境变量中读取 LLM_API_KEY
self.client = OpenAI(api_key=os.getenv("LLM_API_KEY"))
# 优先使用传入模型,其次使用环境变量,最后使用默认模型
self.model = model or os.getenv("LLM_MODEL_ID", "gpt-4.1-mini")
def chat(
self,
messages: list[dict[str, Any]],
tools: list[dict[str, Any]] | None = None,
tool_choice: str | dict[str, Any] | None = None,
temperature: float = 0.2,
):
"""
调用 OpenAI Chat Completions。
参数:
messages:
对话消息列表。
格式类似:
[
{"role": "system", "content": "..."},
{"role": "user", "content": "..."}
]
tools:
可选工具 schema 列表。
如果传入 tools,模型就可以返回 tool_calls。
tool_choice:
工具选择策略。
常用值:
- "auto":模型自己决定是否调用工具
- "none":禁止调用工具
- 指定某个工具:强制调用
temperature:
采样温度。
越低越稳定,越高越发散。
返回:
OpenAI SDK 返回的 ChatCompletion 对象。
"""
kwargs: dict[str, Any] = {
"model": self.model,
"messages": messages,
"temperature": temperature,
}
# 只有在存在工具时才传 tools 参数
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = tool_choice or "auto"
return self.client.chat.completions.create(**kwargs)
Agent/skill_loader.py
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import json
import yaml
@dataclass
class Skill:
"""
Skill 数据结构。
一个 Skill 对应一个 skills/<skill-name>/SKILL.md 文件。
字段说明:
name:
skill 的唯一标识符。
description:
skill 的简洁描述。
在这个 demo 中,description 是 LLM 选择 skill 的唯一语义依据。
version:
skill 版本号,可选。
allowed_tools:
当前 skill 允许调用的工具白名单。
required_context:
当前 skill 需要的上下文说明。
这个 demo 中暂时只展示,不做强校验。
license:
许可证信息。
author:
作者信息。
tags:
标签,用于分类。
注意:在本 demo 的 skill 选择阶段,不使用 tags 作为语义判断依据。
directory:
skill 所在目录。
body:
SKILL.md 中 frontmatter 后面的正文。
只有 skill 被选中后,才会放进 Agent 上下文。
"""
name: str
description: str
version: str | None
allowed_tools: list[str]
required_context: list[str]
license: str | None
author: str | None
tags: list[str]
directory: Path
body: str
def metadata_for_selection(self) -> dict[str, str]:
"""
返回给 skill selector 的最小信息。
注意:
这里故意只返回 name 和 description。
原因:
1. name 用于标识最终选择哪个 skill。
2. description 是选择 skill 的唯一语义依据。
3. 不把 SKILL.md 正文、advanced.md、data 文件提前塞给模型。
"""
return {
"name": self.name,
"description": self.description,
}
class SkillLoader:
"""
Skill 加载器。
主要职责:
1. 扫描 skills 目录。
2. 读取每个 SKILL.md。
3. 解析 YAML frontmatter。
4. 保存 skill 元信息和正文。
5. 提供资源读取能力,用于渐进式披露。
"""
def __init__(self, skills_root: Path):
"""
初始化 SkillLoader。
参数:
skills_root:
skills 根目录,例如 skills_agent_demo/skills。
"""
self.skills_root = skills_root
# key 是 skill.name,value 是 Skill 对象
self.skills: dict[str, Skill] = {}
def discover(self) -> dict[str, Skill]:
"""
发现所有 skills。
它会扫描:
skills/*/SKILL.md
返回:
skill name 到 Skill 对象的映射。
异常:
如果 skills 目录不存在,会报错。
如果没有发现任何 SKILL.md,会报错。
如果 skill name 重复,会报错。
"""
if not self.skills_root.exists():
raise FileNotFoundError(f"skills 目录不存在:{self.skills_root}")
# 遍历所有子目录里的 SKILL.md
for skill_md in self.skills_root.glob("*/SKILL.md"):
skill = self._load_skill_metadata_and_body(skill_md)
if skill.name in self.skills:
raise ValueError(f"重复的 skill name:{skill.name}")
self.skills[skill.name] = skill
if not self.skills:
raise RuntimeError(f"没有发现任何 SKILL.md:{self.skills_root}")
return self.skills
def list_for_selection(self) -> list[dict[str, str]]:
"""
返回用于选择 skill 的候选列表。
只包含:
1. name
2. description
不包含:
1. SKILL.md 正文
2. advanced.md
3. data 文件
4. references 文件
"""
return [skill.metadata_for_selection() for skill in self.skills.values()]
def get(self, skill_name: str) -> Skill:
"""
根据 skill name 获取 Skill 对象。
参数:
skill_name:
skill 的唯一名称。
返回:
Skill 对象。
"""
if skill_name not in self.skills:
raise KeyError(f"未知 skill:{skill_name}")
return self.skills[skill_name]
def list_resources(self, skill_name: str) -> list[str]:
"""
列出某个 skill 目录下的附加资源。
这也是渐进式披露的一部分。
它只告诉模型:
当前 skill 有哪些资源可用。
但不会直接读取资源内容。
例如可能返回:
[
"advanced.md",
"data/source_policy.json"
]
"""
skill = self.get(skill_name)
resources: list[str] = []
# 遍历 skill 目录下所有文件
for path in skill.directory.rglob("*"):
if path.is_file() and path.name != "SKILL.md":
resources.append(path.relative_to(skill.directory).as_posix())
return sorted(resources)
def read_resource(self, skill_name: str, relative_path: str) -> str:
"""
读取某个 skill 目录下的资源文件。
参数:
skill_name:
当前 skill 名称。
relative_path:
相对于该 skill 目录的资源路径。
例如:
- advanced.md
- data/source_policy.json
安全限制:
只能读取当前 skill 目录内的文件。
禁止通过 ../ 逃逸到其他目录。
"""
skill = self.get(skill_name)
# 计算目标文件绝对路径
target = (skill.directory / relative_path).resolve()
# 当前 skill 目录绝对路径
skill_dir = skill.directory.resolve()
# 防止路径逃逸
if not str(target).startswith(str(skill_dir)):
raise ValueError("非法资源路径:禁止访问 skill 目录之外的文件")
if not target.exists() or not target.is_file():
raise FileNotFoundError(f"资源不存在:{relative_path}")
return target.read_text(encoding="utf-8")
def query_json_resource(
self,
skill_name: str,
relative_path: str,
query: str = "",
limit: int = 5,
) -> str:
"""
查询当前 skill 目录下的 JSON 数据文件。
这个函数用于演示:
大型数据文件不直接塞进 prompt,
而是通过工具按需查询。
参数:
skill_name:
当前 skill 名称。
relative_path:
JSON 文件相对路径。
query:
简单关键词过滤。
为空时返回前 limit 条。
limit:
最多返回多少条。
返回:
JSON 字符串。
"""
# 先读取 JSON 文件内容
raw = self.read_resource(skill_name, relative_path)
# 解析 JSON
data = json.loads(raw)
query_lower = query.lower().strip()
# 把 dict/list 统一转换成列表,方便过滤
if isinstance(data, dict):
items = [{"key": k, "value": v} for k, v in data.items()]
elif isinstance(data, list):
items = data
else:
items = [{"value": data}]
# 如果提供了 query,就做简单字符串匹配
if query_lower:
filtered = []
for item in items:
text = json.dumps(item, ensure_ascii=False).lower()
if query_lower in text:
filtered.append(item)
items = filtered
# 返回前 limit 条
return json.dumps(items[:limit], ensure_ascii=False, indent=2)
def _load_skill_metadata_and_body(self, skill_md: Path) -> Skill:
"""
读取并解析单个 SKILL.md 文件。
SKILL.md 格式:
---
name: xxx
description: >
xxx
allowed_tools: [...]
---
# 正文
返回:
Skill 对象。
"""
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---"):
raise ValueError(f"{skill_md} 缺少 YAML frontmatter")
try:
# 分割:
# 第一个 --- 前面为空
# 第二部分是 YAML frontmatter
# 第三部分是 Markdown 正文
_, frontmatter_text, body = text.split("---", 2)
except ValueError as exc:
raise ValueError(f"{skill_md} frontmatter 格式错误") from exc
# 解析 YAML
meta: dict[str, Any] = yaml.safe_load(frontmatter_text) or {}
# 检查必需字段
required_fields = ["name", "description"]
for field in required_fields:
if field not in meta:
raise ValueError(f"{skill_md} 缺少必需字段:{field}")
# 构造 Skill 对象
return Skill(
name=str(meta["name"]),
description=str(meta["description"]).strip(),
version=meta.get("version"),
allowed_tools=list(meta.get("allowed_tools", [])),
required_context=list(meta.get("required_context", [])),
license=meta.get("license"),
author=meta.get("author"),
tags=list(meta.get("tags", [])),
directory=skill_md.parent,
body=body.strip(),
)
Agent/tools.py
from __future__ import annotations
import json
import os
from typing import Any, Callable
from tavily import TavilyClient
from agent.skill_loader import Skill, SkillLoader
def tavily_search(query: str, max_results: int = 5, topic: str = "general") -> str:
"""
使用 Tavily 搜索网页。
这个工具用于 web-research skill。
参数:
query:
搜索查询词。
max_results:
最多返回多少条结果。
为了避免上下文太长,这里限制在 1 到 10 之间。
topic:
Tavily 的搜索类型。
常见值:
- general
- news
- finance
返回:
JSON 字符串,包含搜索结果列表。
"""
api_key = os.getenv("TAVILY_API_KEY")
if not api_key:
return json.dumps(
{
"error": "缺少 TAVILY_API_KEY,请在 .env 文件中配置。",
},
ensure_ascii=False,
)
# 限制 max_results 范围,避免一次返回太多内容
max_results = max(1, min(int(max_results), 10))
# 初始化 Tavily 客户端
client = TavilyClient(api_key=api_key)
# 调用 Tavily 搜索
response = client.search(
query=query,
max_results=max_results,
topic=topic,
include_answer=False,
)
# 压缩搜索结果,只保留 Agent 最常用的字段
compact_results = []
for item in response.get("results", []):
compact_results.append(
{
"title": item.get("title"),
"url": item.get("url"),
"content": item.get("content"),
"score": item.get("score"),
}
)
return json.dumps(
{
"query": query,
"results": compact_results,
},
ensure_ascii=False,
indent=2,
)
class ToolExecutor:
"""
工具执行器。
主要职责:
1. 注册所有可用工具。
2. 根据 skill.allowed_tools 暴露工具 schema。
3. 执行工具前检查当前 skill 是否允许调用该工具。
4. 执行工具并返回结果。
"""
def __init__(self, skill_loader: SkillLoader):
"""
初始化 ToolExecutor。
参数:
skill_loader:
SkillLoader 实例。
用于读取 skill 的 advanced.md、data 文件等资源。
"""
self.skill_loader = skill_loader
# 注册所有工具
self.registry: dict[str, dict[str, Any]] = self._build_registry()
def schemas_for(self, allowed_tool_names: list[str]) -> list[dict[str, Any]]:
"""
根据 allowed_tools 返回工具 schema。
参数:
allowed_tool_names:
当前 skill 允许调用的工具名称列表。
返回:
OpenAI Chat Completions tools 参数所需的 schema 列表。
"""
schemas = []
for name in allowed_tool_names:
if name in self.registry:
schemas.append(self.registry[name]["schema"])
return schemas
def execute(self, skill: Skill, tool_name: str, arguments: dict[str, Any]) -> str:
"""
执行某个工具。
参数:
skill:
当前被激活的 Skill。
tool_name:
模型想调用的工具名称。
arguments:
模型生成的工具参数。
关键安全点:
即使模型请求调用某个工具,
Harness 也不会直接执行,
而是先检查 tool_name 是否在当前 skill.allowed_tools 中。
"""
# 白名单检查:防止模型越权调用工具
if tool_name not in skill.allowed_tools:
return json.dumps(
{
"error": f"工具 {tool_name} 不在当前 skill 的 allowed_tools 白名单中。",
"allowed_tools": skill.allowed_tools,
},
ensure_ascii=False,
)
# 工具是否存在
if tool_name not in self.registry:
return json.dumps(
{
"error": f"未知工具:{tool_name}",
},
ensure_ascii=False,
)
try:
# load_skill_resource 是特殊工具:
# 它不直接对应一个普通 Python 函数,
# 而是调用 SkillLoader 读取当前 skill 目录下的资源。
if tool_name == "load_skill_resource":
relative_path = arguments["relative_path"]
content = self.skill_loader.read_resource(
skill_name=skill.name,
relative_path=relative_path,
)
return json.dumps(
{
"skill": skill.name,
"resource": relative_path,
"content": content,
},
ensure_ascii=False,
indent=2,
)
# query_skill_data 也是特殊工具:
# 它用于查询当前 skill 目录下的 JSON 数据文件。
if tool_name == "query_skill_data":
relative_path = arguments["relative_path"]
query = arguments.get("query", "")
limit = int(arguments.get("limit", 5))
content = self.skill_loader.query_json_resource(
skill_name=skill.name,
relative_path=relative_path,
query=query,
limit=limit,
)
return json.dumps(
{
"skill": skill.name,
"resource": relative_path,
"query": query,
"matches": json.loads(content),
},
ensure_ascii=False,
indent=2,
)
# 普通工具直接从 registry 中取函数执行
func: Callable[..., str] = self.registry[tool_name]["func"]
return func(**arguments)
except Exception as exc:
# 工具执行失败时,把错误返回给模型
# 让模型有机会调整下一步
return json.dumps(
{
"error": str(exc),
"tool": tool_name,
},
ensure_ascii=False,
)
def _build_registry(self) -> dict[str, dict[str, Any]]:
"""
注册所有工具。
每个工具包含两部分:
1. func:
Python 函数。
2. schema:
给 OpenAI tool calling 使用的 JSON schema。
注意:
这里只是“注册工具”。
某个 skill 能不能用这些工具,
取决于该 skill 的 allowed_tools。
"""
return {
"tavily_search": {
"func": tavily_search,
"schema": {
"type": "function",
"function": {
"name": "tavily_search",
"description": "使用 Tavily 搜索网页,适合查找最新信息、多来源资料、事实核查。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询词,应该简洁明确。",
},
"max_results": {
"type": "integer",
"description": "返回结果数量,建议 3 到 5。",
"default": 5,
},
"topic": {
"type": "string",
"enum": ["general", "news", "finance"],
"description": "搜索类别。",
"default": "general",
},
},
"required": ["query"],
},
},
},
},
"load_skill_resource": {
"func": None,
"schema": {
"type": "function",
"function": {
"name": "load_skill_resource",
"description": "读取当前 skill 目录下的附加文档,例如 advanced.md。只能读取当前 skill 内部资源。",
"parameters": {
"type": "object",
"properties": {
"relative_path": {
"type": "string",
"description": "相对于当前 skill 目录的资源路径,例如 advanced.md。",
}
},
"required": ["relative_path"],
},
},
},
},
"query_skill_data": {
"func": None,
"schema": {
"type": "function",
"function": {
"name": "query_skill_data",
"description": "查询当前 skill 目录下的 JSON 数据文件,只返回匹配片段,避免把大文件全部放进上下文。",
"parameters": {
"type": "object",
"properties": {
"relative_path": {
"type": "string",
"description": "相对于当前 skill 目录的数据文件路径,例如 data/source_policy.json。",
},
"query": {
"type": "string",
"description": "过滤关键词。为空时返回前几条。",
"default": "",
},
"limit": {
"type": "integer",
"description": "最多返回多少条。",
"default": 5,
},
},
"required": ["relative_path"],
},
},
},
},
}
skills/structured-writing/advanced.md
# Structured Writing Advanced Guide
## 正式语气
适合:
- 工作邮件
- 项目汇报
- 对外说明
- 课程作业
- 正式通知
常用表达:
- “目前”
- “预计”
- “建议”
- “请确认”
- “后续将”
- “为确保……”
避免表达:
- “反正”
- “随便”
- “差不多”
- “搞一下”
- “应该还行”
## 委婉语气
适合:
- 拒绝
- 延期
- 催促
- 说明问题
- 请求配合
可用表达:
- “可能需要更多时间”
- “为了确保质量”
- “是否方便在……前确认”
- “我们建议优先……”
- “如果方便的话,请……”
## 强硬但礼貌
适合:
- 明确边界
- 提醒风险
- 强调截止时间
- 要求对方行动
可用表达:
- “该事项需要在……前完成”
- “如果未能按时确认,可能会影响……”
- “为避免后续风险,请……”
- “请在……前完成确认”
## 营销风格
结构建议:
1. 先讲痛点
2. 再讲解决方案
3. 最后讲收益
示例结构:
> 你是否遇到过……
> 现在,通过……
> 你可以更轻松地……
skills/structured-writing/SKILL.md
---
name: structured-writing
description: >
用于改写、润色、整理、扩写或压缩用户提供的文本,包括邮件、报告段落、说明文、社交媒体文案和学习笔记。
当用户提供一段草稿,并希望它更清晰、更专业、更自然、更有结构、更正式、更委婉或更简洁时,应该使用此技能。
核心价值是把粗糙表达转化成可直接使用的高质量文本,同时保留用户原意。
version: 1.0.0
allowed_tools: [load_skill_resource]
required_context: [user_draft, target_audience, tone]
license: MIT
author: Demo Author <demo@example.com>
tags: [writing, editing, polish, structure]
---
# Structured Writing Skill
## 概述
这个技能用于文本改写和结构化表达。
它不负责事实检索。
如果用户需要最新信息、事实核查或资料来源,应使用 `web-research` skill。
适用场景包括:
- 改写一句话
- 润色邮件
- 整理学习笔记
- 优化报告段落
- 改写社交媒体文案
- 把口语表达改得更正式
- 把冗长表达改得更简洁
## 前置条件
用户最好提供:
- 原始草稿
- 目标读者
- 语气要求
- 长度要求
如果缺少这些信息,可以根据上下文做合理默认:
- 目标读者:普通读者
- 语气:清晰、专业、自然
- 长度:中等
## 工作流程
1. 识别用户想要的改写目标:
- 更专业
- 更口语
- 更简短
- 更正式
- 更委婉
- 更有逻辑
2. 提取原文核心意思。
3. 保留用户原意,不新增未经提供的事实。
4. 优先改善:
- 结构
- 逻辑顺序
- 表达清晰度
- 语气一致性
5. 如果用户要求非常正式、委婉、强硬或营销风格,可以读取:
- `load_skill_resource("advanced.md")`
6. 输出最终可直接使用的版本。
## 最佳实践
- 不要过度改写导致意思变化。
- 用户没有要求多版本时,默认给一个最佳版本。
- 如果用户要求“只要结果”,不要解释。
- 如果原文有明显歧义,可以在最终答案后简短说明。
- 不要为了“高级”而堆砌空话。
- 不要添加原文没有的承诺、数据、事实、时间点。
## 示例
用户:
> 帮我把这句话改得专业一点:这个功能现在还没做完,可能要晚点。
输出:
> 该功能目前仍在开发中,预计需要更多时间完成。我们会在进展明确后及时同步更新。
## 故障排查
- 如果用户要求“不要改变意思”,严格保留语义。
- 如果用户要求“更短”,优先删除重复信息。
- 如果用户要求“更强硬”,提高明确性,但避免攻击性表达。
- 如果用户要求事实补充,但没有提供资料,应提醒需要使用检索类能力。
web-research\data\source_policy.json
[
{
"source_type": "official_docs",
"trust_level": "high",
"use_for": ["API usage", "configuration", "version behavior", "installation"],
"caution": "官方文档适合确认 API、配置、版本行为,但可能不覆盖真实用户体验。"
},
{
"source_type": "github",
"trust_level": "high",
"use_for": ["source code", "issues", "release notes", "examples"],
"caution": "GitHub issue 中的信息可能只是个别案例,需要注意 issue 状态和时间。"
},
{
"source_type": "academic_paper",
"trust_level": "high",
"use_for": ["method", "experiment", "formal definition"],
"caution": "论文结果可能不代表工业落地效果。"
},
{
"source_type": "news_media",
"trust_level": "medium",
"use_for": ["recent events", "public announcements", "market movement"],
"caution": "注意发布时间、信息来源和媒体立场。"
},
{
"source_type": "personal_blog",
"trust_level": "medium",
"use_for": ["practice experience", "tutorial", "engineering tricks"],
"caution": "需要和官方资料交叉验证。"
},
{
"source_type": "forum_or_social_media",
"trust_level": "low",
"use_for": ["user sentiment", "bug symptoms", "anecdotal evidence"],
"caution": "不要把个别用户发言当成事实结论。"
}
]
web-research\advanced.md
# Web Research Advanced Guide
## 查询改写策略
### 技术框架类问题
用户问:
> X 框架现在怎么用?
可以搜索:
- `X official docs`
- `X GitHub examples`
- `X agent framework tutorial`
- `X changelog`
### 最新动态类问题
用户问:
> 最近 X 有什么变化?
可以搜索:
- `X latest update`
- `X changelog`
- `X release notes`
- `X news`
### 事实核查类问题
至少搜索两个不同角度:
- 官方来源
- 独立媒体或第三方资料
## 来源优先级
一般优先级:
1. 官方文档
2. 官方 GitHub / changelog
3. 论文 / 标准文档
4. 主流媒体 / 专业机构
5. 个人博客
6. 论坛、社交媒体、二手转载
## 什么时候需要更多搜索
以下情况应该追加搜索:
- 搜索结果互相矛盾
- 用户要求“最新”
- 用户要求“准确引用来源”
- 涉及价格、政策、版本、API、法规
- 搜索结果明显不够权威
web-research\SKILL.md
---
name: web-research
description: >
用于回答需要联网检索、最新资料、多来源事实核查、资料综述或来源引用的问题。
当用户的问题涉及最近变化、实时信息、外部网页资料、新闻、产品、框架、论文、技术动态或需要查证的信息时,应该使用此技能。
核心价值是把开放网络信息检索、来源筛选、结果整合和不确定性说明组织成可靠的研究流程。
version: 1.0.0
allowed_tools: [tavily_search, load_skill_resource, query_skill_data]
required_context: [user_question]
license: MIT
author: Demo Author <demo@example.com>
tags: [web, research, tavily, fact-checking]
---
# Web Research Skill
## 概述
这个技能用于处理需要外部网页信息的问题。
它的重点不是“搜到一个结果就回答”,而是让 Agent:
1. 先理解用户真正想查什么。
2. 再设计合适的搜索 query。
3. 然后比较多个来源。
4. 最后综合回答,并说明不确定性。
适用场景包括:
- 最新技术动态
- 框架、工具、库的当前用法
- 产品、政策、价格、版本变化
- 需要来源支撑的事实核查
- 需要多来源综述的问题
## 前置条件
需要环境变量:
- `TAVILY_API_KEY`
如果没有这个 key,`tavily_search` 工具会返回错误信息。
## 工作流程
1. 判断用户问题是否需要最新信息或外部来源。
2. 将用户问题改写成简洁搜索 query。
3. 调用 `tavily_search` 获取 3 到 5 个结果。
4. 阅读每个结果的:
- title
- url
- content
- score
5. 优先使用更权威、更直接、更近期的来源。
6. 如果问题涉及来源可靠性、边缘情况或搜索策略,可以调用:
- `load_skill_resource("advanced.md")`
7. 如果需要判断不同来源类型的可信度,可以调用:
- `query_skill_data("data/source_policy.json", query="...")`
8. 最终回答必须:
- 先给结论
- 再给依据
- 明确哪些信息来自搜索结果
- 对不确定处说明“不确定”或“需要进一步验证”
## 最佳实践
- 查询词应短,不要把用户的长问题原样丢给搜索引擎。
- 技术问题优先找官方文档、GitHub、论文、主流技术博客。
- 新闻问题优先找多个独立媒体或官方发布。
- 不要把搜索结果里的营销话术直接当事实。
- 如果搜索结果不足,不要编造。
- 如果结果互相矛盾,应说明冲突,而不是强行给单一结论。
## 示例
用户:
> 请查一下 LangGraph 现在主要适合用来做什么?
推荐流程:
1. 搜索 `LangGraph current use cases agents`
2. 对比官方文档、GitHub、技术博客
3. 总结 LangGraph 的主要使用场景
4. 说明依据来自搜索结果
## 故障排查
- 如果 Tavily 返回结果很少,换更短 query。
- 如果结果质量差,加入 `official docs`、`GitHub`、`paper` 等限定词。
- 如果用户问的是观点类问题,不要只搜索一个来源。
- 如果用户要求“最新”,搜索词里可以加入年份或 `latest`。
在这个demo中,如何知道用哪个skill:harness.py 的 _select_skill(),模型会返回一个json格式的 skill_name ,然后 Harness 根据 skill_name 取出完整 Skill(即选择skill的过程是让一个LLM根据用户query、skill-name和description做分类识别任务);找到skill后如何根据 skill 找 tool:每个 SKILL.md 的 frontmatter 里写了 allowed_tools,Harness 读取 allowed_tools,ToolExecutor 只暴露这些工具的 schema(从全局工具注册表 registry 中拿到这些工具的 schema)。如何根据 skill 找 advanced.md:目录扫描(Harness会调用一个函数扫描当前 skill 目录下除了 SKILL.md 以外的文件,然后把这些资源路径放进 system prompt--只是告诉agent可用)
以上最小 demo 为了简单,做的是单 skill 路由,如果一个任务需要多个 skill,就要升级成多 skill 编排。
- 顺序式 Pipeline(适合任务天然有先后顺序/步骤)
- 并行式 Fan-out(适合多个 skill 可以各自独立完成一部分,然后再合并)
- 动态式 Planner(适合复杂任务,开始时不确定要用哪些 skill,个人感觉类似于react,先执行一个skill后看结果决定下一步,直到完成)
代码中,需要让 selector 返回列表,并将每一步只暴露当前 skill 的 allowed_tools。
3.6.Harness
Harness 可以理解为“模型外面的工程外壳”。LangChain 的说法很直接:Agent = Model + Harness,如果不是模型本身,那基本都属于 Harness;它包括 system prompt、tools、skills、MCP、文件系统、沙箱、浏览器、编排逻辑、middleware、日志等。顾名思义,缰绳,简单一些理解就是负责规范大模型行为的判断、是否、限制、记录等。
3.7.Loop Engineering
Loop Engineering 是一个比较新的说法。Addy Osmani 对它的解释是:你不再亲自一轮一轮提示 Agent,而是设计一个系统,由这个系统持续提示、分配任务、检查结果、记录状态、决定下一步。Oracle 把 agent loop 定义为 Harness 在一次 Agent run 中反复执行的循环:组装上下文、调用模型、行动、把轨迹追加回上下文,并一直重复到停止条件满足。
Loop Engineering 和 ReAct 的区别
- ReAct:一种模型推理范式,强调 Thought / Action / Observation
- Loop Engineering:一种工程设计方法,强调谁来驱动循环、如何验证、何时停止、如何恢复、如何记录经验
也就是说,ReAct 是 loop 里面的一种“思考-行动模式”;Loop Engineering 是你在 Harness 层设计整个闭环系统。
4.1.智能体性能评估
在构建智能体系统时,我们还需要解决一个核心问题:如何客观地评估智能体的性能? 具体来说,我们需要回答以下问题:
- 智能体是否具备预期的能力?
- 在不同任务上的表现如何?
- 与其他智能体相比处于什么水平?
4.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教程主要介绍以下三个场景:
4.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 ...
4.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}
4.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 可以自动化评估题目质量,但对于数学题目这种需要严格逻辑推理的内容,人工验证仍然是不可或缺的。特别是在评估答案生成质量时,需要人类专家验证答案的准确性、解答步骤的完整性和数学推理的严密性。此外,人工验证还可以发现自动化评估可能遗漏的问题,如题目的创新性、趣味性等主观因素。
更多推荐

所有评论(0)