从零到一:用 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 有使用经验

文章目录

  1. 第一部分:引言与基础

    • 什么是 Agent?
    • 为什么要手写 Agent?
    • Agent 的核心组件
    • 本文的实现路线图
  2. 第二部分:核心概念与理论基础

    • ReAct 模式:推理与行动的循环
    • 工具使用的原理
    • 记忆机制的类型与实现思路
    • Agent 的决策流程
  3. 第三部分:环境准备

    • 所需软件与库
    • API 密钥配置
    • 项目结构设计
  4. 第四部分:分步实现

    • 第一步:构建 LLM 接口封装
    • 第二步:实现工具系统
    • 第三步:添加记忆模块
    • 第四步:构建 Agent 核心逻辑
    • 第五步:整合与测试
  5. 第五部分:关键代码解析与深度剖析

    • 提示词工程的艺术
    • 工具调用的解析与执行
    • 记忆管理的策略
    • 错误处理与重试机制
  6. 第六部分:结果展示与验证

    • 示例任务运行演示
    • 功能验证清单
    • 性能基准测试
  7. 第七部分:性能优化与最佳实践

    • 提示词优化技巧
    • 工具调用效率提升
    • 记忆管理策略
    • 安全考虑
  8. 第八部分:常见问题与解决方案

    • LLM 不遵循指令怎么办?
    • 工具调用失败如何处理?
    • 记忆溢出问题如何解决?
  9. 第九部分:未来展望与扩展方向

    • 多 Agent 系统
    • 自主学习能力
    • 更复杂的工具生态系统
    • 可视化与调试工具
  10. 第十部分:总结与附录

    • 核心要点回顾
    • 参考资料
    • 完整代码示例
    • 扩展阅读资源

第一部分:引言与基础

1.1 什么是 Agent?

在人工智能领域,“Agent”(智能体)是一个非常核心的概念。从广义上讲,Agent 是指能够感知环境做出决策执行行动的实体。这个概念可以追溯到人工智能的早期研究,但随着大语言模型 (LLM) 的出现,Agent 概念获得了新的内涵和实践价值。

在当今的 AI 语境下,我们可以将 Agent 定义为:

Agent 是一个由 LLM 驱动的系统,它能够理解用户意图,制定执行计划,调用外部工具,并且能够从经验中学习和调整策略。

让我们通过一个简单的例子来理解 Agent 能做什么:

用户请求: “帮我查一下明天北京的天气,如果温度超过 25 度,就推荐一个附近的室内活动,否则推荐户外活动。”

对于一个普通的 LLM 来说,这个请求可能无法完成,因为:

  1. 它不知道明天的天气(缺乏实时数据)
  2. 它不知道你在哪里(缺乏上下文信息)
  3. 它无法获取活动推荐(缺乏外部工具)

但对于一个 Agent 来说,它可以:

  1. 理解任务:解析用户的复杂请求
  2. 制定计划:先查天气,再根据结果决定下一步
  3. 调用工具:使用天气 API 获取数据
  4. 执行行动:根据天气情况调用活动推荐工具
  5. 返回结果:整理信息并以自然语言回复

这就是 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:

  1. 基础设置:配置开发环境,安装必要的库
  2. LLM 封装:创建一个简单的接口来与 LLM 交互
  3. 工具系统:实现几个基本工具(如搜索、计算)
  4. 记忆模块:添加短期记忆能力
  5. Agent 核心:实现 ReAct 模式的决策循环
  6. 整合测试:将所有组件整合并测试
  7. 优化扩展:讨论如何进一步改进

我们的目标是创建一个简单但功能完整的 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 过程:

  1. 思考:我需要先找到 2023 年诺贝尔物理学奖得主是谁,然后查找他们的年龄,最后相加。
  2. 行动:使用搜索工具查询"2023 年诺贝尔物理学奖得主"
  3. 观察:结果显示,2023 年诺贝尔物理学奖得主是 Anne L’Huillier、Ferenc Krausz 和 Pierre Agostini。
  4. 思考:现在我需要查找这三位得主的年龄。
  5. 行动:使用搜索工具查询"Anne L’Huillier 年龄"
  6. 观察:Anne L’Huillier 出生于 1958 年,2023 年时 65 岁。
  7. 思考:继续查找 Ferenc Krausz 的年龄。
  8. 行动:使用搜索工具查询"Ferenc Krausz 年龄"
  9. 观察:Ferenc Krausz 出生于 1962 年,2023 年时 61 岁。
  10. 思考:最后查找 Pierre Agostini 的年龄。
  11. 行动:使用搜索工具查询"Pierre Agostini 年龄"
  12. 观察:Pierre Agostini 出生于 1941 年,2023 年时 82 岁。
  13. 思考:现在我可以计算年龄之和:65 + 61 + 82 = 208。
  14. 答案: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,aq)
o 1 ∼ Env ( a 1 ) o_1 \sim \text{Env}(a_1) o1Env(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,aq,t1,a1,o1)
o 2 ∼ Env ( a 2 ) o_2 \sim \text{Env}(a_2) o2Env(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) ap(aq,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 工具的核心组件

一个完整的工具定义应该包含以下组件:

  1. 名称 (Name):工具的唯一标识符
  2. 描述 (Description):工具的功能说明,帮助 LLM 理解何时使用该工具
  3. 输入模式 (Input Schema):定义工具需要什么参数
  4. 执行函数 (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 工具调用的过程

工具调用的完整过程如下:

  1. 工具选择:LLM 根据当前情况决定使用哪个工具
  2. 参数生成:LLM 生成工具所需的参数
  3. 参数验证:确保生成的参数符合工具的要求
  4. 工具执行:调用工具的执行函数
  5. 结果处理:将工具的返回结果转换为 LLM 可以理解的格式

让我们用 mermaid 序列图来可视化这个过程:

外部环境/API 工具系统 LLM Agent 外部环境/API 工具系统 LLM Agent alt [参数有效] [参数无效] 思考,决定使用工具X 返回工具名称和参数 验证参数 执行工具调用 返回结果 返回处理后的结果 返回错误信息 将结果作为观察

这个序列图展示了从工具决定到结果返回的完整流程。

2.3 记忆机制的类型与实现思路

记忆是 Agent 能够进行连贯对话和完成复杂任务的关键。没有记忆,Agent 将无法记住之前的对话内容,也无法从经验中学习。

2.3.1 记忆的类型

我们可以将 Agent 的记忆分为以下几种类型:

  1. 短期记忆 (Short-term Memory)

    • 存储当前对话的上下文
    • 通常包含最近的消息历史
    • 容量有限(受 LLM 的上下文窗口限制)
  2. 长期记忆 (Long-term Memory)

    • 存储跨会话的信息
    • 可以包含用户偏好、历史交互等
    • 通常需要外部存储(如数据库)
  3. 工作记忆 (Working Memory)

    • 存储当前任务的中间结果
    • 如正在处理的文档、计算结果等
    • 通常在任务完成后清除
  4. 语义记忆 (Semantic Memory)

    • 存储关于世界的一般知识
    • 可以通过向量数据库实现
    • 支持语义检索

让我们用一个表格来对比这些记忆类型:

记忆类型 存储内容 持续时间 实现方式 典型容量
短期记忆 当前对话上下文 会话期间 内存/上下文窗口 受 LLM 限制
长期记忆 用户偏好、历史交互 长期 数据库
工作记忆 任务中间结果 任务期间 内存
语义记忆 外部知识 长期 向量数据库 非常大
2.3.2 记忆的实现思路

不同类型的记忆有不同的实现方式:

  1. 短期记忆的实现

    • 最简单的实现是使用一个列表存储消息历史
    • 每次与 LLM 交互时,将历史消息作为上下文
    • 需要管理消息数量,避免超过 LLM 的上下文窗口限制
  2. 长期记忆的实现

    • 可以使用 SQL 数据库(如 SQLite、PostgreSQL)存储结构化数据
    • 可以使用 NoSQL 数据库(如 MongoDB)存储非结构化数据
    • 需要考虑数据的组织和检索方式
  3. 语义记忆的实现

    • 使用嵌入模型将文本转换为向量
    • 使用向量数据库(如 Pinecone、Chroma、Weaviate)存储和检索向量
    • 支持语义相似性搜索,而不仅仅是关键词匹配
2.3.3 记忆的管理策略

随着对话的进行,记忆会不断增长,我们需要有效的管理策略:

  1. 截断 (Truncation)

    • 只保留最近的 N 条消息
    • 简单但可能丢失重要信息
  2. 总结 (Summarization)

    • 定期将旧的对话内容总结为更简洁的形式
    • 保留重要信息,减少令牌使用
  3. 重要性评分 (Importance Scoring)

    • 为每条记忆分配重要性分数
    • 优先保留重要的记忆
  4. 分层存储 (Hierarchical Storage)

    • 将记忆分为不同层级
    • 短期记忆:快速访问但容量小
    • 长期记忆:容量大但访问较慢

在我们的实现中,我们将从最简单的短期记忆开始,使用一个列表来存储对话历史。

2.4 Agent 的决策流程

现在让我们将这些概念组合起来,看看 Agent 的完整决策流程是如何工作的。

2.4.1 Agent 的系统架构

首先,让我们看一下 Agent 的整体架构:

存储层

工具层

核心层

协调层

用户界面层

用户界面

Agent 协调器

大语言模型

规划器

推理器

工具注册表

工具集合

短期记忆

长期记忆

向量存储

这个架构图展示了 Agent 的各个组件以及它们之间的关系。

2.4.2 Agent 的完整决策流程

现在让我们详细描述 Agent 的完整决策流程:

  1. 初始化

    • 加载配置
    • 初始化 LLM 连接
    • 注册可用工具
    • 初始化记忆系统
  2. 接收输入

    • 从用户界面接收输入
    • 将输入添加到短期记忆
  3. 理解任务

    • 从记忆中检索相关上下文
    • 使用 LLM 理解用户意图
    • 确定任务的复杂性和范围
  4. 制定计划

    • 如果任务复杂,将其分解为子任务
    • 制定执行顺序
    • 识别可能需要的工具
  5. 执行循环 (ReAct)

    • 思考:分析当前状态,决定下一步
    • 行动:选择并执行适当的工具
    • 观察:获取工具执行结果
    • 反思:评估结果,调整计划(如需要)
    • 重复直到任务完成
  6. 生成回复

    • 整理结果
    • 以自然语言生成回复
    • 确保回复清晰、有用
  7. 更新记忆

    • 将交互添加到短期记忆
    • 如需要,更新长期记忆
    • 考虑是否需要总结或压缩记忆
  8. 返回结果

    • 将回复发送给用户
    • 等待下一个输入

让我们用一个更详细的 mermaid 流程图来可视化这个过程:

ReAct 循环

开始

初始化 Agent

接收用户输入

添加到短期记忆

检索相关上下文

理解用户意图

任务是否复杂?

分解任务为子任务

进入 ReAct 循环

创建执行计划

思考下一步

是否有足够信息?

选择工具

生成工具参数

参数是否有效?

执行工具

获取观察结果

添加观察到记忆

生成最终回复

更新记忆

返回结果给用户

等待下一个输入

这个流程图详细展示了 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 密钥
  1. 访问 OpenAI 官网
  2. 注册或登录账户
  3. 进入 API 密钥页面
  4. 点击 “Create new secret key” 创建一个新的 API 密钥
  5. 复制生成的密钥(注意:你只会看到一次)
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 交互。它包含两个主要方法:

  1. generate:接受一个消息列表并生成回复
  2. 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 方法,并提供 namedescription 属性。

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 第三步:添加记忆模块

接下来,让我们实现记忆模块。我们将从简单的短期

更多推荐