第四章 智能体经典范式构建

本章任务:

  • Cli:构建一个基本的客户端,能够成功调用 LLM ,并解析 LLM 流式返回的结果

  • ReAct(Reasoning and Action):着重于实现如何让模型在执行过程不断根据当前状况动态地更新行动计划;

  • Plan-and-Solve:着重于构建模型的规划能力;

  • Reflection:模型的反思能力。

基本 Cli

前置库:

1
pip install openai python-dotenv

仔细阅读一下 HelloAgentLLM.py 具体干了什么。

初始化

1
2
3
4
5
6
7
8
9
10
11
def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
# 1.初始化客户端。优先使用传入参数,如果未提供,则从环境变量加载。
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文件中定义。")

# 2.构建一个 OpenAI 规范的 LLM 调用接口
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)

没什么好说的,直接根据 .env 的参数构建一个 OpenAI 规范的 LLM 接口。

think 函数

main 函数中,通过构造函数构建一个接口后,调用了 think 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
print("请输入文本...")
userPrompt = input()

exampleMessages = [
{"role": "system", "content": "You are a helpful assistant that writes Python code."},
{"role": "user", "content": userPrompt}
]

print("--- 调用LLM ---")
responseText = llmClient.think(exampleMessages)
if responseText:
print("\n\n--- 完整模型响应 ---")
print(responseText)

传入的 Dict 字典中,包含了系统提示词和用户提示词。

think 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
"""
调用大语言模型进行思考,并返回其响应。
"""
print(f"🧠 正在调用 {self.model} 模型...")
try:
# 1. 通过LLM接口调用大模型,response为流式响应(stream=True)
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
)

# 2. 处理流式响应
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

stream 输出模式,每次都只返回一个增量 chunk ,每次都要打印这个 chunk,并拼接到最终输出。

ReAct:工具

本节需要在 Cli 内实现 Reasoning and Action,即强制让模型按照 思考-行动-观察 的流程,这样模型更容易激活模型的工具意图,从而主动调用工具。

此外,ReAct 模式最重要的是工具,激活工具意图只是让模型拥有调用工具的能力,而工具调用是否可靠,还是需要自行定义。所以本节还涉及单个工具的定义,多个工具的注册发现、调用。

准备工作

前置库:

1
pip install google-search-results

.env 添加:

1
SERPAPI_API_KEY = ""

search 分三步:1.初始化;2.构建 serpapi 接口并调用;3.智能化筛选搜索结果。

第三步中,会检查是否存在 answer_box(Google的答案摘要框)或 knowledge_graph(知识图谱)等信息,如果存在,就直接返回这些最精确的答案。如果不存在,它才会退而求其次,返回前三个常规搜索结果的摘要。这种“智能解析”能为LLM提供质量更高的信息输入。

测试一下:

1
2
if __name__ == '__main__':
print(search("英伟达的历史最高股价是多少"))

工具调用结果:

1
2
3
4
5
6
7
8
9
🔍 正在执行 [SerpApi] 网页搜索: 英伟达的历史最高股价是多少
[1] 英偉達(NVDA) 過往股價及數據
成交量. 2026年6月26日, 193.12, 195.55, 191.22, 192.53, 192.53, 178,906,300. 2026年6月25日, 200.08, 200.80, 192.13, 195.74, 195.74, 150,205,600.

[2] 5万亿美元!英伟达市值究竟有多惊人:几组对比告诉你……
①英伟达公司上周进一步创造了历史,成为全球首家市值突破5万亿美元的企业; ②这一里程碑式的壮举,或许也预示着这家全球顶尖的芯片公司,对世界经济和金融市场 ...

[3] NVIDIA股票图表— NASDAQ:NVDA股票价格
NVDA于2026年5月13日达到历史最高价,价格为236.54 USD,其历史最低价为0.03 USD,于1999年4月25日达到。在NVDA图表上查看更多价格动态。 查看其它股票达到的最高和最低价格

ReAct:工具执行类 ToolExecutor

如果存在很多个工具,我们构建一个执行类 ToolExecutor,统一注册工具、执行工具。

初始化

构造工具对象时,其内部存入了一个 Dict 对象:

1
2
def __init__(self):
self.tools: Dict[str, Dict[str, Any]] = {}

顾名思义,是用来存储自身工具的注册表。其结构案例为:

1
2
3
4
5
6
7
8
9
10
{                                                                                              
"Search": {
"description": "一个网页搜索引擎。当你需要回答关于时事...",
"func": search.search # search.py 中的 search 函数
}
"other": {
"description": "another tools needed when...",
"func": other.other
}
}

也就是说,一个注册表应当包含哪些东西:什么时候用这个工具?(自然语言描述) 和 这个工具在哪?(结构化语言)。

注册工具

工具注册,本质是采用 callable 传入工具函数的指针:

1
2
3
4
5
6
7
8
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}' 已注册。")

暴露工具列表

1
2
3
4
5
6
7
8
def getAvailableTools(self) -> str:
"""
获取所有可用工具的格式化描述字符串。
"""
return "\n".join([
f"- {name}: {info['description']}"
for name, info in self.tools.items()
])

提取工具

返回 callable

1
2
3
4
5
def getTool(self, name: str) -> callable:
"""
根据名称获取一个工具的执行函数。
"""
return self.tools.get(name, {}).get("func")

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# --- 工具初始化与使用示例 ---
if __name__ == '__main__':
# 1. 初始化工具执行器
toolExecutor = ToolExecutor()

# 2. 注册我们的实战搜索工具
search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
toolExecutor.registerTool("Search", search_description, search.search)

# 3. 打印可用的工具
print("\n--- 可用的工具 ---")
print(toolExecutor.getAvailableTools())

# 4. 智能体的Action调用,这次我们问一个实时性的问题
print("\n--- 执行 Action: Search['英伟达最新的GPU型号是什么'] ---")
tool_name = "Search"
tool_input = "英伟达最新的GPU型号是什么"

tool_function = toolExecutor.getTool(tool_name)
if tool_function:
observation = tool_function(tool_input)
print("--- 观察 (Observation) ---")
print(observation)
else:
print(f"错误:未找到名为 '{tool_name}' 的工具。")

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
工具 'Search' 已注册。

--- 可用的工具 ---
- Search: 一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。

--- 执行 Action: Search['英伟达最新的GPU型号是什么'] ---
🔍 正在执行 [SerpApi] 网页搜索: 英伟达最新的GPU型号是什么
--- 观察 (Observation) ---
[1] 比较GeForce 系列最新一代显卡和前代显卡| NVIDIA
比较最新一代RTX 30 系列显卡和前代的RTX 20 系列、GTX 10 和900 系列显卡。查看规格、功能、技术支持等内容。

[2] GeForce RTX 50 系列显卡| NVIDIA
GeForce RTX™ 50 系列GPU 搭载NVIDIA Blackwell 架构,为游戏玩家和创作者带来全新玩法。RTX 50 系列具备强大的AI 算力,带来升级体验和更逼真的画面。

[3] 英伟达
GeForce RTX 50 系列GPU 为游戏玩家和创作者带来全新玩法。借助NVIDIA DLSS 4 实现性能倍增,以更快速度生成图像,并通过NVIDIA Studio 平台释放你的创造力。

ReAct:ReActAgent

prompt

我们刚刚定义好了工具,也跑通了工具调用,接下来我们要让 LLM 主动调用工具。

我们需要在系统提示词中进行激活(ReActPrompt.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。

现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

ReActAgent 指导思想

我们之前定义的 HelloAgentLLM 类,通过 think 函数就可以进行单一提问单一回答。而ReAct 是 think 函数的反复调用者,每次得到 Observation 写入历史后,重新调用 think 函数,直到不再调用。

所以 ReActAgent 和 HelloAgentLLM 的关系是什么呢?ReActAgent 内部包含 HelloAgentLLM 对象,用于运行最原子化的 LLM 调用流程。

另外,ReActAgent 类也整合了前面定义的 ToolExecutor 和所有 Tool。

然后,仔细阅读 ReActAgent 怎么写的。

初始化

1
2
3
4
5
def __init__(self, llm_client: HelloAgentLLM, tool_executor: ToolExecutor, max_steps: int = 5):
self.llm_client = llm_client
self.tool_executor = tool_executor
self.max_steps = max_steps
self.history = []

构造函数参数:一个 HelloAgentLLM 对象、ToolExecutor 对象(需要注册工具)、步数用来防止无限返回

history = [] 用来存储上下文。

run

run 是外部调用接口,开启 ReAct 流程。

第一步:拼接提示词。从 ToolExecutor 获取工具列表、自身上下文历史(history)、用户输入的问题,一同加入 ReActPrompt,完成拼接。

1
2
3
4
5
6
7
8
# 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
)

第二步:调用 LLM 执行。使用 HelloAgentLLM 的 think 函数调用 LLM 输入拼接后提示词。

1
2
3
4
5
6
7
# 2. 调用LLM进行思考
messages = [{"role": "user", "content": prompt}]
response_text = self.llm_client.think(messages=messages)

if not response_text:
print("错误:LLM未能返回有效响应。")
break

第三步:使用自身成员函数 _parse_output 解析出 thought 和 action。这个函数就是简单地匹配关键字。

1
2
3
4
5
6
7
8
9
# 3. 解析LLM的输出
thought, action = self._parse_output(response_text)

if thought:
print(f"思考: {thought}")

if not action:
print("警告:未能解析出有效的Action,流程终止。")
break

第四步:行动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. 执行Action
if action.startswith("Finish"):
# 如果是Finish指令,提取最终答案并结束
final_answer = re.match(r"Finish\[(.*)\]", action).group(1)
print(f"🎉 最终答案: {final_answer}")
return final_answer

tool_name, tool_input = self._parse_action(action)
if not tool_name or not tool_input:
# ... 处理无效Action格式 ...
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) # 调用真实工具

Action 阶段中,包含了解析 LLM 响应的 Action ,根据解析结果调用工具,调用工具的结果就是 Observation。

第五步:写入历史

1
2
3
4
5
6
# 5. 写入 history
print(f"👀 观察: {observation}")

# 将本轮的Action和Observation添加到历史记录中
self.history.append(f"Action: {action}")
self.history.append(f"Observation: {observation}")

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if __name__ == '__main__':

# 1.构建 agent
toolExecutor = ToolExecutor()
search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
toolExecutor.registerTool("Search", search_description, search.search)

agent = ReActAgent(
HelloAgentLLM(),
toolExecutor,
5
)

# 2.提问
print("请输入文本: ")
userPrompt = input()

agent.run(userPrompt)

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
工具 'Search' 已注册。
请输入文本:
英伟达股价多少
--- 第 1 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 用户询问英伟达的股价。我需要获取最新的股价信息。我将使用搜索引擎搜索“英伟达 股价”或“NVIDIA stock price”。
🎬 行动: Search[NVIDIA stock price today]
🔍 正在执行 [SerpApi] 网页搜索: NVIDIA stock price today
👀 观察: 英伟达,创立于1993年1月,是美国一家以设计和销售图形处理器为主的无厂半导体公司,总部设在加利福尼亚州的圣克拉拉,位于硅谷的中心位置。NVIDIA亦会设计游戏机图形处理器,例如第一代Xbox、PlayStation 3及任天堂Switch和任天堂Switch 2。
--- 第 2 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 用户询问英伟达的股价,属于实时信息。历史中的搜索未返回股价,需要重新搜索。我将直接用股票代码“NVDA”搜索最新价格。
🎬 行动: Search[NVDA 股价 今天]
🔍 正在执行 [SerpApi] 网页搜索: NVDA 股价 今天
👀 观察: 英伟达,创立于1993年1月,是美国一家以设计和销售图形处理器为主的无厂半导体公司,总部设在加利福尼亚州的圣克拉拉,位于硅谷的中心位置。NVIDIA亦会设计游戏机图形处理器,例如第一代Xbox、PlayStation 3及任天堂Switch和任天堂Switch 2。
--- 第 3 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 之前的搜索只返回了公司简介,没有提供股价。我需要尝试更精确的查询来获取英伟达今天的股票价格,例如包括股票代码和“today”等关键词。
🎬 行动: Search[NVDA stock price today per share]
🔍 正在执行 [SerpApi] 网页搜索: NVDA stock price today per share
👀 观察: 英伟达,创立于1993年1月,是美国一家以设计和销售图形处理器为主的无厂半导体公司,总部设在加利福尼亚州的圣克拉拉,位于硅谷的中心位置。NVIDIA亦会设计游戏机图形处理器,例如第一代Xbox、PlayStation 3及任天堂Switch和任天堂Switch 2。
--- 第 4 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 前三次搜索返回的都是英伟达公司的简介,没有得到具体的股价信息。可能是搜索引擎返回的结果被截断了,或者当前时间(2025年4月9日)的实时股价数据没有被正确抓取。我需要尝试更精确地搜索,比如直接搜索股票代码和具体日期,或者搜索财经网站。
🎬 行动: Search[NVDA 股价 2025-04-09]
🔍 正在执行 [SerpApi] 网页搜索: NVDA 股价 2025-04-09
👀 观察: [1] 英偉達(NVDA) 過往股價及數據
英偉達(NVDA) ; 2025年9月9日, 169.09, 170.98, 166.74, 170.76 ; 2025年9月8日, 167.55, 170.96, 167.35, 168.31 ...

[2] 英伟达公司(NVDA)_美股行情_今日股价与走势图_新浪财经
新浪财经为您提供英伟达公司(NVDA)实时行情数据。包括最新股价、今日涨跌幅、技术走势图、历史数据查询及最新相关新闻资讯,助力您把握美股市场投资机会。

[3] 英伟达(NVDA)股票股价_股价行情_讨论
雪球为您提供英伟达(NVDA)股票股价,实时行情,热门讨论,最新资讯,最新公告,公司资料,财务数据,财务指标分析等与英伟达(NVDA)股票相关的信息与服务.
--- 第 5 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 我需要获取英伟达(NVDA)的最新股价。虽然之前的搜索没有得到实时数据,但最后一条搜索返回了新浪财经和雪球的链接,可能包含行情。为了提供准确信息,我将再次搜索“NVDA 最新股价”或直接点击链接。由于无法直接点击链接,我将尝试搜索“NVDA 股价 2025-04-09 实时”来获取当日价格。
🎬 行动: Search[NVDA 股价 2025-04-09 实时]
🔍 正在执行 [SerpApi] 网页搜索: NVDA 股价 2025-04-09 实时
👀 观察: [1] 英伟达公司(NVDA)_美股行情_今日股价与走势图_新浪财经
新浪财经为您提供英伟达公司(NVDA)实时行情数据。包括最新股价、今日涨跌幅、技术走势图、历史数据查询及最新相关新闻资讯,助力您把握美股市场投资机会。

[2] 英伟达(NVDA)最新价格_行情_走势图
手机东方财富网是东方财富网的手机门户网站,为亿万用户打造一个手机联通世界的超级平台,提供7*24小时全面及时的财经中文资讯,内容覆盖国内外突发事件、股市资讯、全球新闻

[3] 英偉達(NVDA) 股價、新聞、報價和記錄
前收市價 195.74 · 開市 193.10 · 買入價 192.00 x 100 · 賣出價 191.85 x 200 · 今日波幅 191.22 - 195.55 · 52 週波幅 151.49 - 236.54 · 成交量 177,757,849 · 平均成交量 ...
已达到最大步数,流程终止。

像这种涉及到精确数字的问题,往往只能在特定网站上搜索,比如各种交易所官网,大模型大概率没法得知,所以会不停地循环,试图寻找答案。

再测试一个比较简单的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
工具 'Search' 已注册。
请输入文本:
苹果公司全线涨价是怎么回事
--- 第 1 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 用户询问苹果公司全线涨价的原因,这涉及到近期的时事新闻,我需要使用搜索工具来获取最新信息。
🎬 行动: Search[苹果公司全线涨价 原因]
🔍 正在执行 [SerpApi] 网页搜索: 苹果公司全线涨价 原因
👀 观察: [1] 苹果涨价——压垮AI存储泡沫的最后一根稻草?
苹果涨价之所以能触发韩股熔断,恰恰因为它们击中了AI存储叙事最脆弱的一环:不是说AI不需要存储了,而是市场开始重新定价"AI需要多少存储、有人愿意为此付 ...

[2] 苹果全线涨价,iPhone也不远了库克:这是百年一遇的“洪水”
苹果发言人在涨价声明中表示:“AI数据中心的快速扩张,引发了对内存及存储芯片需求的急剧攀升。我们从未经历过零部件价格如此迅猛、涨幅如此之大的情况。

[3] 苹果突然涨价致果链集体大跌,门店员工也未获提前通知
美东时间6月25日,苹果官宣全球范围内上调Mac、iPad全系硬件定价。这场由AI存储芯片紧缺催生的终端调价风暴,不仅使得苹果股价大跌,也影响了果链在二级市场 ...
--- 第 2 步 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

思考: 根据搜索结果,苹果公司全线涨价的主要原因是AI数据中心快速扩张导致存储芯片(内存及存储)需求急剧攀升,零部件价格暴涨,迫使苹果上调Mac、iPad等产品的全球定价。此次涨价未提前通知门店,引发股价下跌和供应链震荡。我需要将这些信息整合成一个清晰的答案。
🎉 最终答案: 苹果公司突然宣布全线涨价,主要涉及Mac和iPad全系产品。涨价的核心原因是AI(人工智能)数据中心快速扩张,导致对内存及存储芯片的需求急剧攀升,零部件供应紧张且价格暴涨。苹果在声明中表示,从未经历过如此迅猛的零部件涨价情况。此次调价不仅引发了苹果自身股价大跌,也连带导致供应链相关企业股价集体下挫,甚至部分市场出现剧烈波动。此外,有消息称许多门店员工也未提前获得通知,涨价决定来得非常突然。

可以看到,这种比较简单的只涉及文字的问题,全网到处都有关于这个话题的讨论,大模型只搜索一次就完成了。

Plan-and-Resolve:Planner

LLM 如何才能拥有 将复杂问题拆分成若干子问题 的能力?这一点与 ReAct 是有着显著区别的。

我们构建的 ReAct 倾向于反复调用 LLM,试图通过上下文的迭代来命中预期,也就是说前 n-1 次调用 LLM 相当于只是给第 n 次调用 LLM 做准备工作。

而 plan-and-resolve 模式中,我们力求的是分为两个阶段解决问题:

  1. 第一次让 LLM 进行 plan,选定所有需要的工具和顺序,并将其作为结构化输出封装成 PLAN;

  2. 第二次拿着封装好的 PLAN,根据工作流循环让 LLM 调用工具。

这种方式每次循环都解决一个简单的子问题。

此外,采用这种模式将规划流程标准化,即使 LLM 本身就有着很强的规划能力,也大概率能够在此基础上进一步最终执行的准确度。

planner prompt

提示词应该怎么写?在前面的 ReAct 的 prompt 中,我们明文规定了 LLM 必须输出 Thought => Action => Observation 的思维链,我们在 plan-and-resolve 中也需要明文规定:

1
2
3
4
5
6
7
8
9
10
11
12
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。

问题: {question}

请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
\```
"""

Planner 指导思想

定义 Planner 类对 prompt 进行封装。我们前面定义的 ReActAgent 本质上也是对 ReActPrompt 的封装:我们强制定义了 LLM 的输出格式,然后我们根据这个格式进行输出解析,完成业务。在 Planner 中,我们自然也需要对前面提到的 ["步骤1", "步骤2", "步骤3", ...] 进行解析。

另外,Planner 同样应当调用 HelloAgentLLM。

接下来解读一下 Planner.py 干了什么

初始化

这里要传入一个 HelloAgentLLM 实例。

plan

plan 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def plan(self, question: str) -> list[str]:
"""
根据用户问题生成一个行动计划。
"""
prompt = PLANNER_PROMPT_TEMPLATE.format(question=question)

# 为了生成计划,我们构建一个简单的消息列表
messages = [{"role": "user", "content": prompt}]

print("--- 正在生成计划 ---")
# 使用流式输出来获取完整的计划
response_text = self.llm_client.think(messages=messages) or ""

print(f"✅ 计划已生成:\n{response_text}")

# 解析LLM输出的列表字符串
try:
# 找到```python和```之间的内容
plan_str = response_text.split("```python")[1].split("```")[0].strip()
# 使用ast.literal_eval来安全地执行字符串,将其转换为Python列表
plan = ast.literal_eval(plan_str)
return plan if isinstance(plan, list) else []
except (ValueError, SyntaxError, IndexError) as e:
print(f"❌ 解析计划时出错: {e}")
print(f"原始响应: {response_text}")
return []
except Exception as e:
print(f"❌ 解析计划时发生未知错误: {e}")
return []

测试 plan

定义 main 函数进行测试:

1
2
3
4
5
6
7
if __name__ == '__main__':
# 1. 构建 planner
planner = Planner(HelloAgentLLM())

# 2. 提问
print("请输入问题: ")
planner.plan(input())

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请输入问题:
怎么构建一个快速排序算法?
--- 正在生成计划 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 计划已生成:
```python
[
"理解快速排序算法的基本原理:选择一个基准元素,将数组分为小于基准和大于基准的两部分,然后递归地对两部分排序。",
"定义快速排序函数,明确输入参数(待排序数组、起始索引和结束索引)和递归终止条件(当起始索引不小于结束索引时返回)。",
"实现分区函数:选取基准元素(例如最后一个元素),遍历数组将小于基准的元素移到左边,记录分区点索引,最后将基准放到正确位置并返回该索引。",
"在快速排序函数中调用分区函数,获取分区点索引,然后对左右两个子数组递归调用自身。",
"编写辅助函数或对外接口,简化调用方式(例如只接受数组作为参数,内部调用主递归函数)。",
"使用不同测试用例(空数组、单元素、已排序、逆序、含重复元素、随机数组)验证算法正确性和稳定性。",
"分析算法性能,讨论平均时间复杂度O(n log n)、最坏情况O(n²)以及空间复杂度,并考虑优化策略(如随机化基准选择、三数取中法等)。"
]
\```

可以看到,LLM 确实将一个复杂的任务拆解成了若干子问题,并且做出了排序,方便后续 LLM 执行。

Plan-and-Resolve:Executor

我们前面已经实现了 Planner,LLM 能够成功将一个复杂问题分解成若干子问题,并且进行了排序,接下来要做的就是根据排序好的子问题执行。

我们需要定义 Executor,流程与 Planner 类似,都是要传入 HelloAgentLLM ,并且使用提示词进行激活。

executor prompt

Planner 调用 LLM ,结果输出的是 {plan},也即一个 str[],将其作为 Executor 的输入提示词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决“当前步骤”,并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

# 历史步骤与结果:
{history}

# 当前步骤:
{current_step}

请仅输出针对“当前步骤”的回答:
"""

解读

构造函数传入 HelloAgentLLM。

execute() 函数接收了 plan 阶段获得的 str[] ,.forEach 调用 LLM 执行,采用 history 字符串作为记忆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Executor:
def __init__(self, llm_client:HelloAgentLLM):
self.llm_client = llm_client

def execute(self, question: str, plan: list[str]) -> str:
"""
根据计划,逐步执行并解决问题。
"""
history = "" # 用于存储历史步骤和结果的字符串

print("\n--- 正在执行计划 ---")

for i, step in enumerate(plan):
print(f"\n-> 正在执行步骤 {i+1}/{len(plan)}: {step}")

prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question,
plan=plan,
history=history if history else "无", # 如果是第一步,则历史为空
current_step=step
)

messages = [{"role": "user", "content": prompt}]

response_text = self.llm_client.think(messages=messages) or ""

# 更新历史记录,为下一步做准备
history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n"

print(f"✅ 步骤 {i+1} 已完成,结果: {response_text}")

# 循环结束后,最后一步的响应就是最终答案
final_answer = response_text
return final_answer

Plan-and-Resolve:联合

Planner 与 Executor 都已完成定义,二者之间的关系非常明了:Planner 获取 plan,Executor 根据 plan 执行。

接下来需要定义一个 PlanAndExecuteAgent 类,将二者的工作流程进行封装。

测试

定义一个主函数测试一下:

1
2
3
4
5
6
7
8
if __name__ == '__main__':

# 1. 构造 agent
agent = PlanAndExecuteAgent(HelloAgentLLM())

# 2. 执行任务
print("请输入问题:")
agent.run(input())

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
请输入问题:
怎么写三路快速排序

--- 开始处理问题 ---
问题: 怎么写三路快速排序
--- 正在生成计划 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 计划已生成:
``python
["步骤1:理解三路快速排序的核心思想——通过一次分区将数组划分为小于枢轴、等于枢轴、大于枢轴三个区域,避免重复元素带来的退化。",
"步骤2:定义排序函数(例如 `three_way_quicksort(arr, low, high)`),明确参数为待排数组及当前处理的起止下标。",
"步骤3:在函数内实现枢轴选择逻辑,通常选取 `arr[low]` 作为枢轴,也可加入随机选取来优化性能。",
"步骤4:实现三路分区逻辑,初始化指针 `lt = low`(小于区右边界)、`gt = high`(大于区左边界)、`i = low + 1`(扫描指针),遍历过程中根据与枢轴的比较结果交换元素并移动指针。",
"步骤5:编写循环分区细节:当 `arr[i] < pivot` 时交换 `arr[lt+1]` 与 `arr[i]`,然后移动 `lt` 和 `i`;当 `arr[i] > pivot` 时交换 `arr[gt]` 与 `arr[i]`,仅移动 `gt`;相等时仅移动 `i`,直到 `i > gt` 结束。",
"步骤6:分区结束后将枢轴交换到正确位置,例如交换 `arr[low]` 与 `arr[lt]`,并调整边界。",
"步骤7:递归地对小于区(`low` 到 `lt-1`)和大于区(`gt+1` 到 `high`)进行排序,中间等于区已自动就位。",
"步骤8:添加递归终止条件:当 `low >= high` 时直接返回,避免无限递归。",
"步骤9:编写对外接口函数(如 `sort(arr)`),内部调用 `three_way_quicksort(arr, 0, len(arr)-1)`,方便用户使用。",
"步骤10:设计多组测试用例,包括随机数组、全等元素、已排序数组、单元素数组和空数组,验证排序结果的正确性和稳定性。"]
``

--- 正在执行计划 ---

-> 正在执行步骤 1/10: 步骤1:理解三路快速排序的核心思想——通过一次分区将数组划分为小于枢轴、等于枢轴、大于枢轴三个区域,避免重复元素带来的退化。
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 步骤 1 已完成,结果: 三路快速排序的核心思想是:在每次分区操作中,选取一个枢轴元素,将数组重新排列为三个连续区域——所有小于枢轴的元素位于左侧,等于枢轴的元素居中,大于枢轴的元素位于右侧。这样,等于枢轴的部分在后续递归中无需再次处理,从而在包含大量重复元素的数组上避免性能退化到O(n²),保持平均O(n log n)的时间复杂度。

-> 正在执行步骤 2/10:
······(步骤2——10省略)

--- 任务完成 ---
最终答案: ``python
def test_three_way_quicksort():
# 辅助函数:检查列表是否升序
def is_sorted(arr):
return all(arr[i] <= arr[i+1] for i in range(len(arr)-1))

# 1. 随机数组
import random
arr1 = [random.randint(0, 100) for _ in range(20)]
expected1 = sorted(arr1)
sort(arr1)
assert arr1 == expected1, f"随机数组测试失败: {arr1}"

# 2. 全等元素
arr2 = [5] * 10
expected2 = sorted(arr2)
sort(arr2)
assert arr2 == expected2, f"全等元素测试失败: {arr2}"

# 3. 已排序数组
arr3 = list(range(20))
expected3 = sorted(arr3)
sort(arr3)
assert arr3 == expected3, f"已排序数组测试失败: {arr3}"

# 4. 逆序数组
arr4 = list(range(20, 0, -1))
expected4 = sorted(arr4)
sort(arr4)
assert arr4 == expected4, f"逆序数组测试失败: {arr4}"

# 5. 单元素数组
arr5 = [42]
expected5 = sorted(arr5)
sort(arr5)
assert arr5 == expected5, f"单元素数组测试失败: {arr5}"

# 6. 空数组
arr6 = []
expected6 = sorted(arr6)
sort(arr6)
assert arr6 == expected6, f"空数组测试失败: {arr6}"

# 7. 包含大量重复元素的数组
arr7 = [3, 1, 2, 3, 1, 3, 2, 1, 3, 1]
expected7 = sorted(arr7)
sort(arr7)
assert arr7 == expected7, f"大量重复元素测试失败: {arr7}"

print("所有测试用例通过!")

# 运行测试
test_three_way_quicksort()
``

可以看到,LLM 确实经过了 10 步构建,这是符合流程的。然而有一个瑕疵是,LLM 在最终输出环境并没有给出 sort 函数,反而是给出了其测试函数,这也是 LLM 不可控的地方,需要用户在 question 环节就给出明确说明。

我们再次测试,在提示词内就给出完全的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
请输入问题:
使用typescript,写一个三路快排,最终输出只给出three_way_quicksort(nums:number[])

--- 开始处理问题 ---
问题: 使用typescript,写一个三路快排,最终输出只给出three_way_quicksort(nums:number[])
--- 正在生成计划 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 计划已生成:
``python
[
"理解三路快排算法:将数组划分为小于基准值、等于基准值和大于基准值三个区域,以避免重复元素导致的性能下降。",
"定义TypeScript函数 three_way_quicksort,接收 number[] 并返回 number[],内部实现递归排序逻辑。",
"选择合适的基准值策略(如随机选取或三数取中),并在函数内实现三向切分(partition),返回小于和大于区域的起止索引或直接拼接三个子数组。",
"处理递归终止条件:当数组长度小于等于1时直接返回。",
"实现分区逻辑:使用三个指针(lt, i, gt)进行原地分区,将数组整理为 [ <pivot, =pivot, >pivot] 的状态,然后递归排序小于和大于部分。",
"将分区后的三部分(小于、等于、大于)分别递归调用 three_way_quicksort,并将结果合并后返回。",
"添加边缘情况处理:空数组、单元素数组、所有元素相等的数组,确保不出现无限递归或数组越界。",
"编写单元测试用例,覆盖普通数组、重复元素、已排序数组、逆序数组等,以验证正确性。",
"整理代码,移除多余辅助函数导出,确保最终只暴露 three_way_quicksort 函数并符合题目要求。"
]
``

--- 正在执行计划 ---

-> 正在执行步骤 1/9: 理解三路快排算法:将数组划分为小于基准值、等于基准值和大于基准值三个区域,以避免重复元素导致的性能下降。
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 步骤 1 已完成,结果: 三路快排通过一次遍历将数组划分为三个区域:小于基准值的元素放在左侧,等于基准值的元素放在中间,大于基准值的元素放在右侧。相比传统二路快排,它能有效处理大量重复元素的情况,避免递归树深度增加和额外比较,从而提升性能。分区时通常使用三个指针 lt、i、gt:lt 指向小于区域的右边界,gt 指向大于区域的左边界,i 为当前扫描指针。扫描过程中,若当前元素小于基准值,则交换到小于区域并移动 lt 和 i;若等于基准值,则仅移动 i;若大于基准值,则交换到大于区域并移动 gt,i 不动。扫描结束时,数组被划分为 [lo, lt-1] 小于区,[lt, gt] 等于区,[gt+1, hi] 大于区,然后对小于区和大于区递归排序,等于区已经就位。

-> 正在执行步骤 2/9: 定义TypeScript函数 three_way_quicksort,接收 number[] 并返回 number[],内部实现递归排序逻辑。
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

✅ 步骤 2 已完成,结果: ``typescript
function three_way_quicksort(nums: number[]): number[] {
// 边缘情况:空数组或单元素数组直接返回副本
if (nums.length <= 1) return nums.slice();

const arr = nums.slice(); // 复制数组,以便进行原地分区排序
sort(arr, 0, arr.length - 1);
return arr;

/**
* 递归排序函数,对 arr 的 [lo, hi] 区间进行三路快排
*/
function sort(arr: number[], lo: number, hi: number): void {
if (lo >= hi) return;

// 三向切分,后续步骤将实现具体的分区逻辑
const [lt, gt] = partition(arr, lo, hi);

// 递归排序小于和大于基准值的部分
sort(arr, lo, lt - 1);
sort(arr, gt + 1, hi);
}

/**
* 占位分区函数,将在后续步骤中实现完整的三向切分逻辑
* 预期返回:[lt, gt]——小于区域的右边界和大于区域的左边界
*/
function partition(arr: number[], lo: number, hi: number): [number, number] {
// TODO: 实现三向切分
return [lo, hi];
}
}
``

-> 正在执行步骤 3/9: ······(步骤3---9省略)

--- 任务完成 ---
······(此处省略)
``

这次给出我们想要的结果了。

Reflection

Memory

为什么需要 Reflection?就是赋予大模型反思自己的回答有无纰漏,从而进行改进的能力。我们之前构建的 Agent,不管是基于 ReAct 模式还是 plan-and-resolve 模式,都是一旦完成就退出进程了,对于此前的解答并没有进行检查。

问题是,如何获取之前的解答呢?我们之前定义的两个 Agent,都是通过一个 run 函数直接调用的,特别是 PlanAndResolveAgent 的 run 函数,里面甚至有局部变量 history 作为上下文,一旦函数结束了,上下文就释放了。

所以,对于一个合格的 Reflection 机制,最先需要实现的功能就是:全局的 Memory 记忆,他必须能够跨会话地读取到上下文,才能进行后续的反思。

构建也很简单,实则就是一个 Stack,其核心是自身的成员变量 self.records: List[Dict[str, Any]]

Prompt

官方在此次设计了三套提示词,分别对应 执行=>反思=>优化 的流程。

Reflection 的核心就是执行之后的反思,重点不在于如何执行,所以官方在设计执行的提示词时,只使用了最简单的设计:完全没有任何规范流程,允许 LLM 自由发挥:

1
2
3
4
5
6
7
8
INITIAL_PROMPT_TEMPLATE = """
你是一位资深的程序员。请根据以下要求,编写一个函数。
你的代码必须包含完整的函数签名、文档字符串。

要求: {task}

请直接输出代码,不要包含任何额外的解释。
"""

调用 LLM 执行得到结果后,LLM 需要进行反思,这一步也采用标准的提示词进行激活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
REFLECT_PROMPT_TEMPLATE = """
你是一位极其严格的代码评审专家和资深算法工程师,对代码的性能有极致的要求。
你的任务是审查以下代码,并专注于找出其在<strong>算法效率</strong>上的主要瓶颈。

# 原始任务:
{task}

# 待审查的代码:
{code}

请分析该代码的时间复杂度,并思考是否存在一种<strong>算法上更优</strong>的解决方案来显著提升性能。
如果存在,请清晰地指出当前算法的不足,并提出具体的、可行的改进算法建议(例如,使用筛法替代试除法)。
如果代码在算法层面已经达到最优,才能回答“无需改进”。

请直接输出你的反馈,不要包含任何额外的解释。
"""

反思完毕后,LLM 输出反馈,随后再次进入 LLM 对之前的代码进行优化,提示词被设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
REFINE_PROMPT_TEMPLATE = """
你是一位资深的程序员。你正在根据一位代码评审专家的反馈来优化你的代码。

# 原始任务:
{task}

# 你上一轮尝试的代码:
{last_code_attempt}
评审员的反馈:
{feedback}

请根据评审员的反馈,生成一个优化后的新版本代码。
你的代码必须包含完整的函数签名、文档字符串。
请直接输出优化后的代码,不要包含任何额外的解释。
"""

ReflectionAgent

我们还是调用 HelloAgentLLM 类的 think 函数来调用大模型,构建 ReflectionAgent 类来处理提示词、响应和历史上下文。

我们来解读一下。

run 函数是调用接口,整体来说是一个关于 iteration 数值的循环,超出循环,或者反思意见认为已经趋于完美则停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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)
print("初始执行效果:", initial_code)
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)
print("该轮反思反馈意见:", feedback)
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)
print("该轮优化代码:", refined_code)
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

编写函数进行一个测试:

1
2
3
4
5
6
7
if __name__ == '__main__':
# 1. 构建 agent
agent = ReflectionAgent(HelloAgentLLM())

# 2. 调用 agent
print("请输入问题: ")
agent.run(input())

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
请输入问题:
用typescript写一个归并排序mergeSort,然后写一个测试函数testSort,包含五组待排序数组,在控制台打印排序效果,你最终的输出仅包含mergeSort和testSort

--- 开始处理任务 ---
任务: 用typescript写一个归并排序mergeSort,然后写一个测试函数testSort,包含五组待排序数组,在控制台打印排序效果,你最终的输出仅包含mergeSort和testSort

--- 正在进行初始尝试 ---
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

初始执行效果: ``typescript
/**
* 归并排序算法
* @param arr - 待排序的数值型数组
* @returns 排序后的新数组(原数组不会被修改)
*/
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) {
return arr;
}

const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));

return merge(left, right);
}
······(此处省略)
``
📝 记忆已更新,新增一条 'execution' 记录。

--- 第 1/3 轮迭代 ---

-> 正在进行反思...
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

该轮反思反馈意见: 该实现的时间复杂度为 O(n log n),已属比较排序的渐进最优。但在具体操作上存在显著性能瓶颈:`mergeSort` 中大量使用 `arr.slice()` 创建新数组,每层递归都会完整复制数据;`merge` 结束时又通过 `concat` 与 `slice` 多次分配并复制剩余元素,导致内存分配次数和元素复制量均为 O(n log n)。这使得实际运行效率远低于理论上界,GC 压力极大。

**改进建议**:采用单辅助数组的原地归并策略。可改写为自底向上的迭代归并,或保持自顶向下递归但全程复用同一个长度为 n 的临时数组,仅通过索引划分子区间,避免 `slice`/`concat` 的任何额外复制。这样可将元素复制总量降至严格的 O(n log n)(仅合并时复制),内存分配降至 O(1)(复用固定缓冲区),大幅提升性能。示例优化方案如下:

``typescript
·····(此处省略)
``

该版本递归时在 `target` 和 `work` 之间交替,避免了任何切片操作,所有合并直接写入目标数组,仅分配一次辅助空间,性能与内存效率均得到质的提升。
📝 记忆已更新,新增一条 'reflection' 记录。

-> 正在进行优化...
🧠 正在调用 deepseek-v4-pro 模型...
✅ 大语言模型响应成功:

该轮优化代码: ``typescript
······(此处省略)
``
📝 记忆已更新,新增一条 'execution' 记录。

--- 第 2/3 轮迭代 ---

······(第二轮第三轮省略)

📝 记忆已更新,新增一条 'execution' 记录。

--- 任务完成 ---
最终生成的代码:
``typescript
······(此处省略)
``

这个结果算是符合我们的预期

总结

本章构建了三种经典 Agent 范式。

我们通过先构建一个符合 OpenAI 规范的基本 LLM 调用接口 HelloAgentLLM,实现了对 LLM 进行调用以及处理 LLM 的流式响应。

通过赋予 LLM 使用工具从而与外界交互的能力,我们构建起了 ReAct 模式 Agent,这种模式更专注于通过观察与外界的交互从而决定下一步的行动,所以如何为 LLM 构建使用工具的环境也是其中一个核心课题。适合需要探索性的任务

随后,我们实现了一个 Plan-and-Resolve 模式 Agent,解决了实现让 LLM 将复杂问题拆解 这一难题。通过分别注入标准化的提示词,强制让 LLM 分别执行 plan 与 execute 两个阶段,让 LLM 的思路更加结构化和稳定化,适合需要经过逻辑分析、内部推理密集的任务

最后,基于 赋予 LLM 反思能力,我们构建了 Reflect 模式的 Agent,该种模式着重于让 LLM 拆解自己以前的回答,发现问题并尝试优化,能够显著提升解答质量。适合需要高质量和高精度的解答任务

本章代码

HelloAgentLLM.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import os
from openai import OpenAI
from dotenv import load_dotenv
from typing import List, Dict

# 加载 .env 文件中的环境变量
load_dotenv()

class HelloAgentLLM:
"""
为本书 "Hello Agents" 定制的LLM客户端。
它用于调用任何兼容OpenAI接口的服务,并默认使用流式响应。
"""
def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
# 1.初始化客户端。优先使用传入参数,如果未提供,则从环境变量加载。
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文件中定义。")

# 2.构建一个 OpenAI 规范的 LLM 调用接口
self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)

def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
"""
调用大语言模型进行思考,并返回其响应。
"""
print(f"🧠 正在调用 {self.model} 模型...")
try:
# 通过LLM接口调用大模型,response为流式响应(stream=True)
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

# --- 客户端使用示例 ---
if __name__ == '__main__':
try:
llmClient = HelloAgentLLM()

print("请输入文本...")
userPrompt = input()

exampleMessages = [
{"role": "system", "content": "You are a helpful assistant that writes Python code."},
{"role": "user", "content": userPrompt}
]

print("--- 调用LLM ---")
responseText = llmClient.think(exampleMessages)
if responseText:
print("\n\n--- 完整模型响应 ---")
print(responseText)

except ValueError as e:
print(e)

search.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from serpapi import SerpApiClient
import os
from dotenv import load_dotenv
load_dotenv()

def search(query: str) -> str:
"""
一个基于SerpApi的实战网页搜索引擎工具。
它会智能地解析搜索结果,优先返回直接答案或知识图谱信息。
"""
print(f"🔍 正在执行 [SerpApi] 网页搜索: {query}")
try:

# 1. 初始化 search 的 api
api_key = os.getenv("SERPAPI_API_KEY")
if not api_key:
return "错误:SERPAPI_API_KEY 未在 .env 文件中配置。"


# 2. 构建 Serpapi 接口,并调用
params = {
"engine": "google",
"q": query,
"api_key": api_key,
"gl": "cn", # 国家代码
"hl": "zh-cn", # 语言代码
}
client = SerpApiClient(params)
results = client.get_dict()

# 3.智能解析:优先寻找最直接的答案
if "answer_box_list" in results:
return "\n".join(results["answer_box_list"])
if "answer_box" in results and "answer" in results["answer_box"]:
return results["answer_box"]["answer"]
if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
return results["knowledge_graph"]["description"]
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}"

if __name__ == '__main__':
print(search("英伟达的历史最高股价是多少"))

ToolExecutor.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from typing import Dict, Any
import search
class ToolExecutor:
"""
一个工具执行器,负责管理和执行工具。
"""
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()
])

# --- 工具初始化与使用示例 ---
if __name__ == '__main__':
# 1. 初始化工具执行器
toolExecutor = ToolExecutor()

# 2. 注册我们的实战搜索工具
search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
toolExecutor.registerTool("Search", search_description, search.search)

# 3. 打印可用的工具
print("\n--- 可用的工具 ---")
print(toolExecutor.getAvailableTools())

# 4. 智能体的Action调用,这次我们问一个实时性的问题
print("\n--- 执行 Action: Search['英伟达最新的GPU型号是什么'] ---")
tool_name = "Search"
tool_input = "英伟达最新的GPU型号是什么"

tool_function = toolExecutor.getTool(tool_name)
if tool_function:
observation = tool_function(tool_input)
print("--- 观察 (Observation) ---")
print(observation)
else:
print(f"错误:未找到名为 '{tool_name}' 的工具。")

ReActPrompt.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。

现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

ReActAgent.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from HelloAgentLLM import HelloAgentLLM
from ToolExecutor import ToolExecutor
from ReActPrompt import REACT_PROMPT_TEMPLATE
import re
from typing import Dict, Any

import search

class ReActAgent:
def __init__(self, llm_client: HelloAgentLLM, tool_executor: ToolExecutor, max_steps: int = 5):
self.llm_client = llm_client
self.tool_executor = tool_executor
self.max_steps = max_steps
self.history = []

def _parse_output(self, text: str):
"""解析LLM的输出,提取Thought和Action。
"""
# Thought: 匹配到 Action: 或文本末尾
thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
# Action: 匹配到文本末尾
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字符串,提取工具名称和输入。
"""
match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
if match:
return match.group(1), match.group(2)
return None, None

def run(self, question: str):
"""
运行ReAct智能体来回答一个问题。
"""
self.history = [] # 每次运行时重置历史记录
current_step = 0

while current_step < self.max_steps:
current_step += 1
print(f"--- 第 {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 = self._parse_output(response_text)

if thought:
print(f"思考: {thought}")

if not action:
print("警告:未能解析出有效的Action,流程终止。")
break

# 4. 执行Action
if action.startswith("Finish"):
# 如果是Finish指令,提取最终答案并结束
final_answer = re.match(r"Finish\[(.*)\]", action).group(1)
print(f"🎉 最终答案: {final_answer}")
return final_answer

tool_name, tool_input = self._parse_action(action)
if not tool_name or not tool_input:
# ... 处理无效Action格式 ...
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) # 调用真实工具

# 5. 将 Action 和 Observation 写入 history
print(f"👀 观察: {observation}")

# 将本轮的Action和Observation添加到历史记录中
self.history.append(f"Action: {action}")
self.history.append(f"Observation: {observation}")

# 循环结束
print("已达到最大步数,流程终止。")
return None

if __name__ == '__main__':
# 1.构建 agent
toolExecutor = ToolExecutor()
search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
toolExecutor.registerTool("Search", search_description, search.search)

agent = ReActAgent(
HelloAgentLLM(),
toolExecutor,
5
)

# 2.提问
print("请输入文本: ")
userPrompt = input()
agent.run(userPrompt)

.env

1
2
3
4
5
LLM_MODEL_ID = ""
LLM_API_KEY = ""
LLM_BASE_URL = ""

SERPAPI_API_KEY = ""

Planner.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 假定 llm_client.py 中的 HelloAgentsLLM 类已经定义好
# from llm_client import HelloAgentsLLM
from HelloAgentLLM import HelloAgentLLM
from PlannerPrompt import PLANNER_PROMPT_TEMPLATE
import ast

class Planner:
def __init__(self, llm_client:HelloAgentLLM):
self.llm_client = llm_client

def plan(self, question: str) -> list[str]:
"""
根据用户问题生成一个行动计划。
"""
prompt = PLANNER_PROMPT_TEMPLATE.format(question=question)

# 为了生成计划,我们构建一个简单的消息列表
messages = [{"role": "user", "content": prompt}]

print("--- 正在生成计划 ---")
# 使用流式输出来获取完整的计划
response_text = self.llm_client.think(messages=messages) or ""

print(f"✅ 计划已生成:\n{response_text}")

# 解析LLM输出的列表字符串
try:
# 找到```python和```之间的内容
plan_str = response_text.split("```python")[1].split("```")[0].strip()
# 使用ast.literal_eval来安全地执行字符串,将其转换为Python列表
plan = ast.literal_eval(plan_str)
return plan if isinstance(plan, list) else []
except (ValueError, SyntaxError, IndexError) as e:
print(f"❌ 解析计划时出错: {e}")
print(f"原始响应: {response_text}")
return []
except Exception as e:
print(f"❌ 解析计划时发生未知错误: {e}")
return []

if __name__ == '__main__':
# 1. 构建 planner
planner = Planner(HelloAgentLLM())

# 2. 提问
print("请输入问题: ")
planner.plan(input())

ExecutorPrompt.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决“当前步骤”,并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

# 历史步骤与结果:
{history}

# 当前步骤:
{current_step}

请仅输出针对“当前步骤”的回答:
"""

Executor.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from HelloAgentLLM import HelloAgentLLM
from ExecutorPrompt import EXECUTOR_PROMPT_TEMPLATE

class Executor:
def __init__(self, llm_client:HelloAgentLLM):
self.llm_client = llm_client

def execute(self, question: str, plan: list[str]) -> str:
"""
根据计划,逐步执行并解决问题。
"""
history = "" # 用于存储历史步骤和结果的字符串

print("\n--- 正在执行计划 ---")

for i, step in enumerate(plan):
print(f"\n-> 正在执行步骤 {i+1}/{len(plan)}: {step}")

prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question,
plan=plan,
history=history if history else "无", # 如果是第一步,则历史为空
current_step=step
)

messages = [{"role": "user", "content": prompt}]

response_text = self.llm_client.think(messages=messages) or ""

# 更新历史记录,为下一步做准备
history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n"

print(f"✅ 步骤 {i+1} 已完成,结果: {response_text}")

# 循环结束后,最后一步的响应就是最终答案
final_answer = response_text
return final_answer

PlanAndExecuteAgent.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from Planner import Planner
from Executor import Executor
from HelloAgentLLM import HelloAgentLLM

class PlanAndExecuteAgent:
def __init__(self, llm_client:HelloAgentLLM):
"""
初始化智能体,同时创建规划器和执行器实例。
"""
self.llm_client = llm_client
self.planner = Planner(self.llm_client)
self.executor = Executor(self.llm_client)

def run(self, question: str):
"""
运行智能体的完整流程:先规划,后执行。
"""
print(f"\n--- 开始处理问题 ---\n问题: {question}")

# 1. 调用规划器生成计划
plan = self.planner.plan(question)

# 检查计划是否成功生成
if not plan:
print("\n--- 任务终止 --- \n无法生成有效的行动计划。")
return

# 2. 调用执行器执行计划
final_answer = self.executor.execute(question, plan)

print(f"\n--- 任务完成 ---\n最终答案: {final_answer}")


if __name__ == '__main__':

# 1. 构造 agent
agent = PlanAndExecuteAgent(HelloAgentLLM())

# 2. 执行任务
print("请输入问题:")
agent.run(input())

Memory.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

ReflectionPrompt.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
INITIAL_PROMPT_TEMPLATE = """
你是一位资深的程序员。请根据以下要求,编写一个函数。
你的代码必须包含完整的函数签名、文档字符串。

要求: {task}

请直接输出代码,不要包含任何额外的解释。
"""



REFLECT_PROMPT_TEMPLATE = """
你是一位极其严格的代码评审专家和资深算法工程师,对代码的性能有极致的要求。
你的任务是审查以下代码,并专注于找出其在<strong>算法效率</strong>上的主要瓶颈。

# 原始任务:
{task}

# 待审查的代码:
``
{code}
``
请分析该代码的时间复杂度,并思考是否存在一种<strong>算法上更优</strong>的解决方案来显著提升性能。
如果存在,请清晰地指出当前算法的不足,并提出具体的、可行的改进算法建议(例如,使用筛法替代试除法)。
如果代码在算法层面已经达到最优,才能回答“无需改进”。

请直接输出你的反馈,不要包含任何额外的解释。
"""

REFINE_PROMPT_TEMPLATE = """
你是一位资深的程序员。你正在根据一位代码评审专家的反馈来优化你的代码。

# 原始任务:
{task}

# 你上一轮尝试的代码:
{last_code_attempt}
评审员的反馈:
{feedback}

请根据评审员的反馈,生成一个优化后的新版本代码。
你的代码必须包含完整的函数签名、文档字符串。
请直接输出优化后的代码,不要包含任何额外的解释。
"""

ReflectionAgent.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from Memory import Memory
from HelloAgentLLM import HelloAgentLLM
from ReflectionPrompt import INITIAL_PROMPT_TEMPLATE, REFLECT_PROMPT_TEMPLATE, REFINE_PROMPT_TEMPLATE

class ReflectionAgent:
def __init__(self, llm_client:HelloAgentLLM, 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)
print("初始执行效果:", initial_code)
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)
print("该轮反思反馈意见:", feedback)
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)
print("该轮优化代码:", refined_code)
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

if __name__ == '__main__':
# 1. 构建 agent
agent = ReflectionAgent(HelloAgentLLM())

# 2. 调用 agent
print("请输入问题: ")
agent.run(input())