如何用 Python 从零手写一个简单的 Agent
从零到一:用 Python 构建你的第一个 AI Agent
副标题:深入理解 Agent 核心原理,手动实现一个具备推理、工具调用和记忆能力的智能体
摘要/引言
在当今 AI 技术飞速发展的时代,“Agent”(智能体)已成为最热门的概念之一。从 AutoGPT 到 LangChain,从企业级应用到个人助手,Agent 正在重塑我们与 AI 交互的方式。
然而,当我们使用这些现成框架时,往往会觉得 Agent 是一个"黑盒"——它如何理解任务?如何决定使用哪个工具?如何记住之前的对话?这些问题对于想要真正掌握 AI 技术的开发者来说至关重要。
本文的核心目标是带你从零开始,用 Python 手写一个简单但功能完整的 Agent。通过这个过程,你将:
- 深入理解 Agent 的核心工作原理
- 掌握 ReAct(推理-行动)模式的实现
- 学会如何让 AI 调用外部工具
- 实现简单的记忆机制
- 理解 Agent 设计中的关键权衡
本文不会使用 LangChain 等高级框架,而是专注于底层逻辑的实现。这样,当你未来使用这些框架时,你将能够真正理解它们在做什么,以及如何根据需求进行定制。
让我们开始这段激动人心的旅程吧!
目标读者与前置知识
目标读者:
- 有一定 Python 基础的开发者
- 对 AI 和大语言模型 (LLM) 有基本了解
- 想要深入理解 Agent 工作原理的技术爱好者
- 希望构建自定义 AI 应用的工程师
前置知识:
- Python 编程基础(熟悉类、函数、异步编程等概念)
- 对 HTTP 请求和 REST API 有基本了解
- 了解大语言模型的基本概念(如提示词工程、令牌限制等)
- (可选)对 OpenAI API 或其他 LLM API 有使用经验
文章目录
-
第一部分:引言与基础
- 什么是 Agent?
- 为什么要手写 Agent?
- Agent 的核心组件
- 本文的实现路线图
-
第二部分:核心概念与理论基础
- ReAct 模式:推理与行动的循环
- 工具使用的原理
- 记忆机制的类型与实现思路
- Agent 的决策流程
-
第三部分:环境准备
- 所需软件与库
- API 密钥配置
- 项目结构设计
-
第四部分:分步实现
- 第一步:构建 LLM 接口封装
- 第二步:实现工具系统
- 第三步:添加记忆模块
- 第四步:构建 Agent 核心逻辑
- 第五步:整合与测试
-
第五部分:关键代码解析与深度剖析
- 提示词工程的艺术
- 工具调用的解析与执行
- 记忆管理的策略
- 错误处理与重试机制
-
第六部分:结果展示与验证
- 示例任务运行演示
- 功能验证清单
- 性能基准测试
-
第七部分:性能优化与最佳实践
- 提示词优化技巧
- 工具调用效率提升
- 记忆管理策略
- 安全考虑
-
第八部分:常见问题与解决方案
- LLM 不遵循指令怎么办?
- 工具调用失败如何处理?
- 记忆溢出问题如何解决?
-
第九部分:未来展望与扩展方向
- 多 Agent 系统
- 自主学习能力
- 更复杂的工具生态系统
- 可视化与调试工具
-
第十部分:总结与附录
- 核心要点回顾
- 参考资料
- 完整代码示例
- 扩展阅读资源
第一部分:引言与基础
1.1 什么是 Agent?
在人工智能领域,“Agent”(智能体)是一个非常核心的概念。从广义上讲,Agent 是指能够感知环境、做出决策并执行行动的实体。这个概念可以追溯到人工智能的早期研究,但随着大语言模型 (LLM) 的出现,Agent 概念获得了新的内涵和实践价值。
在当今的 AI 语境下,我们可以将 Agent 定义为:
Agent 是一个由 LLM 驱动的系统,它能够理解用户意图,制定执行计划,调用外部工具,并且能够从经验中学习和调整策略。
让我们通过一个简单的例子来理解 Agent 能做什么:
用户请求: “帮我查一下明天北京的天气,如果温度超过 25 度,就推荐一个附近的室内活动,否则推荐户外活动。”
对于一个普通的 LLM 来说,这个请求可能无法完成,因为:
- 它不知道明天的天气(缺乏实时数据)
- 它不知道你在哪里(缺乏上下文信息)
- 它无法获取活动推荐(缺乏外部工具)
但对于一个 Agent 来说,它可以:
- 理解任务:解析用户的复杂请求
- 制定计划:先查天气,再根据结果决定下一步
- 调用工具:使用天气 API 获取数据
- 执行行动:根据天气情况调用活动推荐工具
- 返回结果:整理信息并以自然语言回复
这就是 Agent 的魔力所在——它不仅仅是一个"问答机器",而是一个能够主动解决问题的助手。
1.2 为什么要手写 Agent?
现在有很多优秀的框架(如 LangChain、AutoGPT、BabyAGI)可以帮助你快速构建 Agent,那为什么我们还要花时间手写一个呢?
1.2.1 深入理解底层原理
“好的开发者使用框架,伟大的开发者理解框架。” 通过手写 Agent,你将:
- 理解 Agent 决策的每一个步骤
- 掌握提示词工程的核心技巧
- 学习如何处理各种边界情况
- 明白框架背后的设计权衡
1.2.2 完全的控制权
现成框架虽然方便,但往往会限制你的灵活性。手写 Agent 让你能够:
- 完全自定义 Agent 的行为和思维过程
- 根据特定需求优化性能
- 添加独特的功能和工具
- 避免框架的"黑盒"问题
1.2.3 更好的调试和优化能力
当 Agent 表现不符合预期时,理解其内部工作原理将使你能够:
- 快速定位问题所在
- 有针对性地进行优化
- 实现自定义的日志和监控
- 理解 Agent 决策的"为什么"
1.2.4 学习成果的最大化
手写 Agent 的过程是一个深度学习的过程。你不仅会学会如何构建 Agent,还会:
- 提升你的 Python 编程技能
- 深入理解 LLM 的能力和局限性
- 学习系统设计和架构思维
- 培养解决复杂问题的能力
1.3 Agent 的核心组件
虽然不同的 Agent 可能有不同的实现方式,但大多数 Agent 都包含以下核心组件:
1.3.1 大语言模型 (LLM)
LLM 是 Agent 的"大脑",负责:
- 理解用户输入
- 生成推理过程
- 决定下一步行动
- 生成最终回复
在本文中,我们将使用 OpenAI 的 GPT 模型作为示例,但你可以轻松替换为其他模型(如 Claude、Llama 2 等)。
1.3.2 工具系统 (Tools)
工具是 Agent 与外部世界交互的接口。常见的工具包括:
- 搜索引擎(如 Google 搜索)
- 计算器
- 代码解释器
- 数据库查询
- 文件操作
- API 调用
工具系统使 Agent 能够获取实时信息、执行计算、操作数据等。
1.3.3 记忆模块 (Memory)
记忆使 Agent 能够"记住"之前的对话和行动。记忆可以分为:
- 短期记忆:当前对话的上下文
- 长期记忆:跨会话的信息和经验
- 工作记忆:当前任务的中间结果
没有记忆,Agent 将无法进行连贯的对话或完成复杂的多步骤任务。
1.3.4 规划与推理引擎 (Planning & Reasoning)
这个组件负责:
- 将复杂任务分解为子任务
- 制定执行计划
- 反思和调整策略
- 评估行动结果
规划能力使 Agent 能够处理需要多步骤才能完成的复杂任务。
1.3.5 用户界面 (UI)
UI 是用户与 Agent 交互的方式。可以是:
- 命令行界面
- Web 界面
- 移动应用
- API 接口
UI 的设计将直接影响用户体验。
1.4 本文的实现路线图
在本文中,我们将按照以下步骤构建我们的 Agent:
- 基础设置:配置开发环境,安装必要的库
- LLM 封装:创建一个简单的接口来与 LLM 交互
- 工具系统:实现几个基本工具(如搜索、计算)
- 记忆模块:添加短期记忆能力
- Agent 核心:实现 ReAct 模式的决策循环
- 整合测试:将所有组件整合并测试
- 优化扩展:讨论如何进一步改进
我们的目标是创建一个简单但功能完整的 Agent,能够:
- 理解复杂的用户请求
- 思考如何完成任务
- 决定使用什么工具
- 记住对话历史
- 提供有用的回复
让我们开始吧!
第二部分:核心概念与理论基础
在开始编码之前,让我们先深入理解 Agent 的核心概念和理论基础。这将为我们后续的实现奠定坚实的基础。
2.1 ReAct 模式:推理与行动的循环
ReAct(Reasoning + Acting)是一种用于构建 Agent 的强大范式,由 Google Research 在 2022 年的论文 “ReAct: Synergizing Reasoning and Acting in Language Models” 中提出。
2.1.1 ReAct 的核心思想
ReAct 的核心思想是将推理(思考做什么)和行动(实际执行)结合起来,形成一个循环:
思考 → 行动 → 观察 → 思考 → 行动 → ... → 答案
让我们用一个例子来说明这个过程:
用户问题: “2023 年诺贝尔物理学奖得主的年龄之和是多少?”
ReAct 过程:
- 思考:我需要先找到 2023 年诺贝尔物理学奖得主是谁,然后查找他们的年龄,最后相加。
- 行动:使用搜索工具查询"2023 年诺贝尔物理学奖得主"
- 观察:结果显示,2023 年诺贝尔物理学奖得主是 Anne L’Huillier、Ferenc Krausz 和 Pierre Agostini。
- 思考:现在我需要查找这三位得主的年龄。
- 行动:使用搜索工具查询"Anne L’Huillier 年龄"
- 观察:Anne L’Huillier 出生于 1958 年,2023 年时 65 岁。
- 思考:继续查找 Ferenc Krausz 的年龄。
- 行动:使用搜索工具查询"Ferenc Krausz 年龄"
- 观察:Ferenc Krausz 出生于 1962 年,2023 年时 61 岁。
- 思考:最后查找 Pierre Agostini 的年龄。
- 行动:使用搜索工具查询"Pierre Agostini 年龄"
- 观察:Pierre Agostini 出生于 1941 年,2023 年时 82 岁。
- 思考:现在我可以计算年龄之和:65 + 61 + 82 = 208。
- 答案:2023 年诺贝尔物理学奖得主的年龄之和是 208 岁。
这就是 ReAct 的完整流程。通过交替进行推理和行动,Agent 能够系统地解决复杂问题。
2.1.2 ReAct 的数学模型
我们可以用数学形式化地描述 ReAct 过程:
假设我们有:
- 一个问题 q q q
- 一系列思考 t 1 , t 2 , . . . , t n t_1, t_2, ..., t_n t1,t2,...,tn
- 一系列行动 a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1,a2,...,an
- 一系列观察 o 1 , o 2 , . . . , o n o_1, o_2, ..., o_n o1,o2,...,on
- 最终答案 a a a
ReAct 过程可以表示为:
( t 1 , a 1 ) ∼ p ( t , a ∣ q ) (t_1, a_1) \sim p(t, a | q) (t1,a1)∼p(t,a∣q)
o 1 ∼ Env ( a 1 ) o_1 \sim \text{Env}(a_1) o1∼Env(a1)
( t 2 , a 2 ) ∼ p ( t , a ∣ q , t 1 , a 1 , o 1 ) (t_2, a_2) \sim p(t, a | q, t_1, a_1, o_1) (t2,a2)∼p(t,a∣q,t1,a1,o1)
o 2 ∼ Env ( a 2 ) o_2 \sim \text{Env}(a_2) o2∼Env(a2)
. . . ... ...
a ∼ p ( a ∣ q , t 1 , a 1 , o 1 , . . . , t n , a n , o n ) a \sim p(a | q, t_1, a_1, o_1, ..., t_n, a_n, o_n) a∼p(a∣q,t1,a1,o1,...,tn,an,on)
其中:
- p ( t , a ∣ . . . ) p(t, a | ...) p(t,a∣...) 是 LLM 生成思考和行动的概率分布
- Env ( a ) \text{Env}(a) Env(a) 是执行行动 a a a 后从环境(工具)获得的观察
这个模型展示了 ReAct 如何通过迭代地生成思考、执行行动、获取观察,最终生成答案。
2.1.3 ReAct 提示词模板
为了让 LLM 按照 ReAct 模式工作,我们需要精心设计提示词。以下是一个基本的 ReAct 提示词模板:
你是一个有用的助手,可以使用工具来回答问题。
你可以使用以下工具:
{工具列表}
使用以下格式:
问题:输入的问题
思考:你应该总是思考下一步该做什么
行动:要执行的行动,应该是 [{工具名称}] 之一
行动输入:行动的输入
观察:行动的结果
...(重复思考/行动/行动输入/观察 多次)
思考:我现在知道最终答案了
答案:原始问题的最终答案
开始!
问题:{用户问题}
这个提示词模板告诉 LLM 如何思考、如何行动、如何观察,以及如何最终给出答案。在我们的实现中,我们将使用类似的提示词结构。
2.1.4 ReAct 流程图
让我们用一个 mermaid 流程图来可视化 ReAct 过程:
这个流程图清晰地展示了 ReAct 的决策循环:从接收问题开始,不断思考、行动、观察,直到有足够的信息来生成答案。
2.2 工具使用的原理
工具使用是 Agent 最重要的能力之一,它使 Agent 能够超越 LLM 的固有知识限制,与外部世界交互。
2.2.1 什么是工具?
在 Agent 的上下文中,工具可以定义为:
工具是一个封装好的功能,它接收特定格式的输入,执行某些操作,然后返回输出结果。
工具可以简单到一个计算器,也可以复杂到一个完整的 API 或数据库查询系统。
2.2.2 工具的核心组件
一个完整的工具定义应该包含以下组件:
- 名称 (Name):工具的唯一标识符
- 描述 (Description):工具的功能说明,帮助 LLM 理解何时使用该工具
- 输入模式 (Input Schema):定义工具需要什么参数
- 执行函数 (Execute Function):实际执行工具逻辑的代码
让我们用一个简单的计算器工具为例:
class CalculatorTool:
name = "calculator"
description = "执行数学计算,如加减乘除等。输入应该是一个数学表达式字符串。"
def execute(self, input_str: str) -> str:
try:
# 注意:在实际应用中,应该使用更安全的方式计算表达式
result = eval(input_str)
return f"计算结果:{result}"
except Exception as e:
return f"计算错误:{str(e)}"
这个工具包含了所有必要的组件:名称、描述和执行函数。
2.2.3 工具调用的过程
工具调用的完整过程如下:
- 工具选择:LLM 根据当前情况决定使用哪个工具
- 参数生成:LLM 生成工具所需的参数
- 参数验证:确保生成的参数符合工具的要求
- 工具执行:调用工具的执行函数
- 结果处理:将工具的返回结果转换为 LLM 可以理解的格式
让我们用 mermaid 序列图来可视化这个过程:
这个序列图展示了从工具决定到结果返回的完整流程。
2.3 记忆机制的类型与实现思路
记忆是 Agent 能够进行连贯对话和完成复杂任务的关键。没有记忆,Agent 将无法记住之前的对话内容,也无法从经验中学习。
2.3.1 记忆的类型
我们可以将 Agent 的记忆分为以下几种类型:
-
短期记忆 (Short-term Memory)
- 存储当前对话的上下文
- 通常包含最近的消息历史
- 容量有限(受 LLM 的上下文窗口限制)
-
长期记忆 (Long-term Memory)
- 存储跨会话的信息
- 可以包含用户偏好、历史交互等
- 通常需要外部存储(如数据库)
-
工作记忆 (Working Memory)
- 存储当前任务的中间结果
- 如正在处理的文档、计算结果等
- 通常在任务完成后清除
-
语义记忆 (Semantic Memory)
- 存储关于世界的一般知识
- 可以通过向量数据库实现
- 支持语义检索
让我们用一个表格来对比这些记忆类型:
| 记忆类型 | 存储内容 | 持续时间 | 实现方式 | 典型容量 |
|---|---|---|---|---|
| 短期记忆 | 当前对话上下文 | 会话期间 | 内存/上下文窗口 | 受 LLM 限制 |
| 长期记忆 | 用户偏好、历史交互 | 长期 | 数据库 | 大 |
| 工作记忆 | 任务中间结果 | 任务期间 | 内存 | 中 |
| 语义记忆 | 外部知识 | 长期 | 向量数据库 | 非常大 |
2.3.2 记忆的实现思路
不同类型的记忆有不同的实现方式:
-
短期记忆的实现
- 最简单的实现是使用一个列表存储消息历史
- 每次与 LLM 交互时,将历史消息作为上下文
- 需要管理消息数量,避免超过 LLM 的上下文窗口限制
-
长期记忆的实现
- 可以使用 SQL 数据库(如 SQLite、PostgreSQL)存储结构化数据
- 可以使用 NoSQL 数据库(如 MongoDB)存储非结构化数据
- 需要考虑数据的组织和检索方式
-
语义记忆的实现
- 使用嵌入模型将文本转换为向量
- 使用向量数据库(如 Pinecone、Chroma、Weaviate)存储和检索向量
- 支持语义相似性搜索,而不仅仅是关键词匹配
2.3.3 记忆的管理策略
随着对话的进行,记忆会不断增长,我们需要有效的管理策略:
-
截断 (Truncation)
- 只保留最近的 N 条消息
- 简单但可能丢失重要信息
-
总结 (Summarization)
- 定期将旧的对话内容总结为更简洁的形式
- 保留重要信息,减少令牌使用
-
重要性评分 (Importance Scoring)
- 为每条记忆分配重要性分数
- 优先保留重要的记忆
-
分层存储 (Hierarchical Storage)
- 将记忆分为不同层级
- 短期记忆:快速访问但容量小
- 长期记忆:容量大但访问较慢
在我们的实现中,我们将从最简单的短期记忆开始,使用一个列表来存储对话历史。
2.4 Agent 的决策流程
现在让我们将这些概念组合起来,看看 Agent 的完整决策流程是如何工作的。
2.4.1 Agent 的系统架构
首先,让我们看一下 Agent 的整体架构:
这个架构图展示了 Agent 的各个组件以及它们之间的关系。
2.4.2 Agent 的完整决策流程
现在让我们详细描述 Agent 的完整决策流程:
-
初始化
- 加载配置
- 初始化 LLM 连接
- 注册可用工具
- 初始化记忆系统
-
接收输入
- 从用户界面接收输入
- 将输入添加到短期记忆
-
理解任务
- 从记忆中检索相关上下文
- 使用 LLM 理解用户意图
- 确定任务的复杂性和范围
-
制定计划
- 如果任务复杂,将其分解为子任务
- 制定执行顺序
- 识别可能需要的工具
-
执行循环 (ReAct)
- 思考:分析当前状态,决定下一步
- 行动:选择并执行适当的工具
- 观察:获取工具执行结果
- 反思:评估结果,调整计划(如需要)
- 重复直到任务完成
-
生成回复
- 整理结果
- 以自然语言生成回复
- 确保回复清晰、有用
-
更新记忆
- 将交互添加到短期记忆
- 如需要,更新长期记忆
- 考虑是否需要总结或压缩记忆
-
返回结果
- 将回复发送给用户
- 等待下一个输入
让我们用一个更详细的 mermaid 流程图来可视化这个过程:
这个流程图详细展示了 Agent 从初始化到处理用户输入再到返回结果的完整过程。
第三部分:环境准备
现在我们已经理解了 Agent 的核心概念,让我们开始准备开发环境。在这一部分,我们将安装必要的库,配置 API 密钥,并设计项目结构。
3.1 所需软件与库
我们将使用以下软件和库:
3.1.1 基础软件
- Python 3.9+:我们将使用 Python 作为编程语言
- pip:Python 包管理器
- Git(可选):用于版本控制
3.1.2 Python 库
我们将使用以下 Python 库:
- openai:用于与 OpenAI API 交互
- python-dotenv:用于管理环境变量
- requests:用于发送 HTTP 请求(我们将用它实现简单的搜索功能)
- wikipedia:用于查询维基百科(作为一个示例工具)
- pydantic:用于数据验证(可选,但推荐)
让我们创建一个 requirements.txt 文件:
openai>=1.0.0
python-dotenv>=1.0.0
requests>=2.31.0
wikipedia>=1.4.0
pydantic>=2.0.0
3.2 安装步骤
让我们一步步设置我们的开发环境:
3.2.1 创建项目目录
首先,让我们创建一个项目目录:
mkdir simple-agent
cd simple-agent
3.2.2 创建虚拟环境(推荐)
使用虚拟环境可以避免依赖冲突:
# Windows
python -m venv venv
venv\Scripts\activate
# macOS/Linux
python3 -m venv venv
source venv/bin/activate
3.2.3 安装依赖
现在让我们安装所需的库:
pip install -r requirements.txt
3.3 API 密钥配置
我们将使用 OpenAI 的 API 来访问 LLM,所以你需要一个 OpenAI API 密钥。
3.3.1 获取 OpenAI API 密钥
3.3.2 配置环境变量
让我们创建一个 .env 文件来存储我们的 API 密钥:
touch .env
在 .env 文件中添加以下内容:
OPENAI_API_KEY=你的-openai-api-密钥
OPENAI_MODEL=gpt-3.5-turbo # 或你想使用的其他模型
重要提示:永远不要将 .env 文件提交到版本控制系统中,因为它包含敏感信息。让我们创建一个 .gitignore 文件:
echo ".env" >> .gitignore
echo "venv/" >> .gitignore
echo "__pycache__/" >> .gitignore
echo "*.pyc" >> .gitignore
3.4 项目结构设计
让我们设计一个清晰的项目结构:
simple-agent/
├── .env # 环境变量
├── .gitignore # Git 忽略文件
├── requirements.txt # 项目依赖
├── README.md # 项目说明
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── agent/ # Agent 相关代码
│ │ ├── __init__.py
│ │ ├── agent.py # Agent 核心实现
│ │ └── prompts.py # 提示词模板
│ ├── llm/ # LLM 相关代码
│ │ ├── __init__.py
│ │ └── llm_wrapper.py # LLM 封装
│ ├── tools/ # 工具相关代码
│ │ ├── __init__.py
│ │ ├── base.py # 工具基类
│ │ ├── calculator.py # 计算器工具
│ │ ├── search.py # 搜索工具
│ │ └── wikipedia.py # 维基百科工具
│ ├── memory/ # 记忆相关代码
│ │ ├── __init__.py
│ │ └── memory.py # 记忆实现
│ └── utils/ # 工具函数
│ ├── __init__.py
│ └── helpers.py # 辅助函数
└── examples/ # 示例代码
└── simple_usage.py # 简单使用示例
这个结构将帮助我们保持代码组织有序,易于理解和维护。
让我们创建这些目录和文件:
mkdir -p src/agent src/llm src/tools src/memory src/utils examples
touch src/__init__.py src/agent/__init__.py src/agent/agent.py src/agent/prompts.py
touch src/llm/__init__.py src/llm/llm_wrapper.py
touch src/tools/__init__.py src/tools/base.py src/tools/calculator.py src/tools/search.py src/tools/wikipedia.py
touch src/memory/__init__.py src/memory/memory.py
touch src/utils/__init__.py src/utils/helpers.py
touch examples/simple_usage.py README.md
现在我们已经准备好了开发环境和项目结构,让我们开始实现我们的 Agent!
第四部分:分步实现
现在我们将开始逐步实现我们的 Agent。我们将从最简单的组件开始,然后逐步添加更多功能。
4.1 第一步:构建 LLM 接口封装
首先,我们需要创建一个简单的接口来与 LLM 交互。这将使我们的代码更加模块化,也更容易在未来替换不同的 LLM。
让我们编辑 src/llm/llm_wrapper.py:
import os
from typing import List, Dict, Any, Optional
from dotenv import load_dotenv
from openai import OpenAI
# 加载环境变量
load_dotenv()
class LLMWrapper:
"""
大语言模型封装类,提供统一的接口来与不同的 LLM 交互
"""
def __init__(self, model: Optional[str] = None):
"""
初始化 LLM 封装
Args:
model: 要使用的模型名称,如果为 None,则使用环境变量中的模型
"""
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
self.model = model or os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
self.temperature = 0.7 # 控制输出的随机性
self.max_tokens = 2000 # 最大生成令牌数
def generate(
self,
messages: List[Dict[str, str]],
temperature: Optional[float] = None,
max_tokens: Optional[int] = None
) -> str:
"""
使用 LLM 生成回复
Args:
messages: 消息列表,格式为 [{"role": "user/system/assistant", "content": "..."}]
temperature: 控制输出的随机性,越高越随机
max_tokens: 最大生成令牌数
Returns:
LLM 生成的回复文本
"""
try:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature or self.temperature,
max_tokens=max_tokens or self.max_tokens
)
return response.choices[0].message.content.strip()
except Exception as e:
print(f"生成回复时出错: {e}")
return f"抱歉,生成回复时出错:{str(e)}"
def generate_with_system_prompt(
self,
user_prompt: str,
system_prompt: str = "你是一个有用的助手。",
**kwargs
) -> str:
"""
使用系统提示词生成回复
Args:
user_prompt: 用户提示词
system_prompt: 系统提示词
**kwargs: 其他参数,如 temperature, max_tokens 等
Returns:
LLM 生成的回复文本
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
return self.generate(messages, **kwargs)
这个封装类提供了一个简单的接口来与 OpenAI 的 API 交互。它包含两个主要方法:
generate:接受一个消息列表并生成回复generate_with_system_prompt:接受用户提示词和系统提示词,然后生成回复
让我们创建一个简单的测试来验证我们的 LLM 封装是否工作。编辑 examples/simple_usage.py:
import sys
import os
# 添加 src 目录到 Python 路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
from llm.llm_wrapper import LLMWrapper
def test_llm():
"""测试 LLM 封装"""
llm = LLMWrapper()
print("测试 LLM 封装...")
response = llm.generate_with_system_prompt(
user_prompt="请简单介绍一下你自己。",
system_prompt="你是一个友好的 AI 助手。"
)
print(f"LLM 回复: {response}")
if __name__ == "__main__":
test_llm()
现在让我们运行这个测试:
python examples/simple_usage.py
如果一切正常,你应该会看到 LLM 的回复。
4.2 第二步:实现工具系统
接下来,让我们实现工具系统。首先,我们将创建一个工具基类,然后实现几个具体的工具。
4.2.1 创建工具基类
编辑 src/tools/base.py:
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
class BaseTool(ABC):
"""
工具基类,所有工具都应该继承自这个类
"""
name: str = "" # 工具的唯一名称
description: str = "" # 工具的描述,帮助 LLM 理解何时使用该工具
@abstractmethod
def execute(self, input_str: str) -> str:
"""
执行工具
Args:
input_str: 工具的输入字符串
Returns:
工具执行后的结果字符串
"""
pass
def to_dict(self) -> Dict[str, str]:
"""
将工具转换为字典格式,方便 LLM 理解
Returns:
包含工具信息的字典
"""
return {
"name": self.name,
"description": self.description
}
这个基类定义了工具的基本接口。所有工具都必须实现 execute 方法,并提供 name 和 description 属性。
4.2.2 实现计算器工具
编辑 src/tools/calculator.py:
import ast
import operator
from typing import Any
from .base import BaseTool
# 定义允许的运算符
ALLOWED_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
}
def safe_eval(expr: str) -> Any:
"""
安全地评估数学表达式,避免使用 eval 的安全风险
Args:
expr: 数学表达式字符串
Returns:
表达式的计算结果
"""
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.BinOp):
op = type(node.op)
if op not in ALLOWED_OPERATORS:
raise ValueError(f"不允许的运算符: {op}")
return ALLOWED_OPERATORS[op](_eval(node.left), _eval(node.right))
elif isinstance(node, ast.UnaryOp):
op = type(node.op)
if op not in ALLOWED_OPERATORS:
raise ValueError(f"不允许的运算符: {op}")
return ALLOWED_OPERATORS[op](_eval(node.operand))
else:
raise ValueError(f"不允许的表达式节点: {type(node)}")
try:
# 解析表达式
tree = ast.parse(expr, mode='eval')
# 评估表达式
return _eval(tree.body)
except Exception as e:
raise ValueError(f"表达式计算错误: {str(e)}")
class CalculatorTool(BaseTool):
"""
计算器工具,用于执行数学计算
"""
name = "calculator"
description = "执行数学计算,如加减乘除、乘方等。输入应该是一个数学表达式字符串,例如 '2 + 3 * 4'。"
def execute(self, input_str: str) -> str:
"""
执行计算器工具
Args:
input_str: 数学表达式字符串
Returns:
计算结果字符串
"""
try:
result = safe_eval(input_str)
return f"计算结果:{input_str} = {result}"
except Exception as e:
return f"计算错误:{str(e)}"
这个计算器工具使用了一个安全的表达式评估方法,避免了直接使用 eval 带来的安全风险。
4.2.3 实现维基百科工具
编辑 src/tools/wikipedia.py:
import wikipedia
from .base import BaseTool
class WikipediaTool(BaseTool):
"""
维基百科工具,用于查询维基百科
"""
name = "wikipedia"
description = "查询维基百科获取信息。输入应该是一个搜索关键词,例如 'Python 编程语言'。"
def execute(self, input_str: str) -> str:
"""
执行维基百科工具
Args:
input_str: 搜索关键词
Returns:
维基百科摘要字符串
"""
try:
# 设置语言为中文(可选)
wikipedia.set_lang("zh")
# 搜索页面
search_results = wikipedia.search(input_str, results=3)
if not search_results:
return f"未找到与 '{input_str}' 相关的维基百科页面。"
# 获取第一个页面的摘要
try:
page = wikipedia.page(search_results[0])
summary = page.summary
# 限制摘要长度,避免太长
if len(summary) > 2000:
summary = summary[:2000] + "..."
return f"维基百科页面: {page.title}\nURL: {page.url}\n摘要: {summary}"
except wikipedia.DisambiguationError as e:
# 处理消歧义页面
return f"找到多个相关页面,请更具体地描述你的查询: {', '.join(e.options[:5])}"
except Exception as e:
return f"获取维基百科页面时出错: {str(e)}"
except Exception as e:
return f"查询维基百科时出错: {str(e)}"
这个工具使用 wikipedia 库来查询维基百科,获取相关信息。
4.2.4 实现搜索工具
编辑 src/tools/search.py:
import requests
from .base import BaseTool
class SearchTool(BaseTool):
"""
搜索工具,用于搜索网络(使用 DuckDuckGo API 作为示例)
"""
name = "search"
description = "搜索网络获取信息。输入应该是一个搜索查询,例如 '最新的 AI 新闻'。"
def execute(self, input_str: str) -> str:
"""
执行搜索工具
Args:
input_str: 搜索查询
Returns:
搜索结果字符串
"""
try:
# 使用 DuckDuckGo 的即时答案 API
url = "https://api.duckduckgo.com/"
params = {
"q": input_str,
"format": "json",
"no_html": 1,
"skip_disambig": 1
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# 提取摘要和相关主题
abstract = data.get("Abstract", "")
abstract_url = data.get("AbstractURL", "")
related_topics = data.get("RelatedTopics", [])
result_parts = []
if abstract:
result_parts.append(f"摘要: {abstract}")
if abstract_url:
result_parts.append(f"更多信息: {abstract_url}")
# 添加相关主题
if related_topics:
result_parts.append("\n相关主题:")
for topic in related_topics[:3]: # 只取前 3 个
if isinstance(topic, dict) and "Text" in topic:
result_parts.append(f"- {topic['Text']}")
if not result_parts:
return f"未找到与 '{input_str}' 相关的即时答案。你可以尝试使用更具体的搜索词。"
return "\n".join(result_parts)
except requests.exceptions.RequestException as e:
return f"搜索请求出错: {str(e)}"
except Exception as e:
return f"搜索时出错: {str(e)}"
这个工具使用 DuckDuckGo 的 API 来搜索网络,获取即时答案。
4.2.5 更新工具包的 __init__.py
让我们更新 src/tools/__init__.py,以便更容易导入我们的工具:
from .base import BaseTool
from .calculator import CalculatorTool
from .wikipedia import WikipediaTool
from .search import SearchTool
__all__ = [
"BaseTool",
"CalculatorTool",
"WikipediaTool",
"SearchTool"
]
4.3 第三步:添加记忆模块
接下来,让我们实现记忆模块。我们将从简单的短期
更多推荐



所有评论(0)