hello-agents学习记录

智能体经典范式构建

现代的智能体,其核心能力 在于能将大语言模型的推理能力与外部世界联通。它能够自主地理解用户意图、拆解复杂任务,并通过调用代码解释器、搜索引擎、API等一系列"工具",来获取信息、执行操作,最终达成目标能力 )。 然而,智能体并非万能,它同样面临着来自大模型本身的"幻觉"问题、在复杂任务中可能陷入推理循环、以及对工具的错误使用等挑战,这些也构成了智能体的能力边界短板
为了更好地组织智能体的"思考"与"行动"过程,业界涌现出了多种经典的架构范式。在本章中,我们将聚焦于其中最具代表性的三种,并一步步从零实现它们:

  • ReAct (Reasoning and Acting): 一种将"思考"和"行动"紧密结合的范式,让智能体边想边做,动态调整
  • Plan-and-Solve: 一种"三思而后行"的范式,智能体首先生成一个完整的行动计划,然后严格执行
  • Reflection: 一种赋予智能体"反思"能力的范式,通过自我批判和修正来优化结果

ReAct

ReAct (Reason + Act)是最经典的一个智能体范式,由Shunyu Yao于2022年提出,其核心思想是模仿人类解决问题的方式,将推理 (Reasoning)行动 (Acting) 显式地结合起来,形成一个"思考-行动-观察"的循环

ReAct 的工作流程

在ReAct诞生之前,主流的方法可以分为两类(存在两大毛病):

  • 一类是"纯思考"型,如思维链 (Chain-of-Thought),它能引导模型进行复杂的逻辑推理,但无法与外部世界交互,容易产生事实幻觉(只会空想)

  • 另一类是"纯行动"型,模型直接输出要执行的动作,但缺乏规划和纠错能力(只会瞎干活)
    ReAct的巧妙之处在于,它认识到思考与行动是相辅相成的 。思考指导行动,而行动的结果又反过来修正思考。为此,ReAct范式通过一种特殊的提示工程来引导模型,使其每一步的输出都遵循一个固定的轨迹:

  • Thought (思考): 这是智能体的"内心独白"。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果

  • Action (行动) : 这是智能体决定采取的具体动作,通常是调用一个外部工具,例如 Search['华为最新款手机']

  • Observation (观察) : 这是执行Action后从外部工具返回的结果,例如搜索结果的摘要或API的返回值

智能体将不断重复这个 Thought -> Action -> Observation 的循环,将新的观察结果追加到历史记录中,形成一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。这个过程形成了一个强大的协同效应:推理使得行动更具目的性,而行动则为推理提供了事实依据
这种机制特别适用于以下场景:

  • 需要外部知识的任务:如查询实时信息(天气、新闻、股价)、搜索专业领域的知识等
  • 需要精确计算的任务:将数学问题交给计算器工具,避免LLM的计算错误
  • 需要与API交互的任务:如操作数据库、调用某个服务的API来完成特定功能

ReAct 的特点、局限性与调试技巧

ReAct 的主要特点

  • 高可解释性 :ReAct 最大的优点之一就是透明。通过 Thought 链,我们可以清晰地看到智能体每一步的"心路历程"------它为什么会选择这个工具,下一步又打算做什么。这对于理解、信任和调试智能体的行为至关重要(看得懂 AI 在干嘛)

  • 动态规划与纠错能力 :与一次性生成完整计划的范式不同,ReAct 是"走一步,看一步"。它根据每一步从外部世界获得的 Observation 来动态调整后续的 ThoughtAction。如果上一步的搜索结果不理想,它可以在下一步中修正搜索词,重新尝试(做一步、看结果、再决定下一步)

  • 工具协同能力:ReAct 范式天然地将大语言模型的推理能力与外部工具的执行能力结合起来。LLM 负责运筹帷幄(规划和推理),工具负责解决具体问题(搜索、计算),二者协同工作,突破了单一 LLM 在知识时效性、计算准确性等方面的固有局限
    ReAct 的固有局限性

  • 对LLM自身能力的强依赖 :ReAct 流程的成功与否,高度依赖于底层 LLM 的综合能力。如果 LLM 的逻辑推理能力、指令遵循能力或格式化输出能力不足,就很容易在 Thought 环节产生错误的规划,或者在 Action 环节生成不符合格式的指令,导致整个流程中断(模型越强,ReAct 越稳;弱模型很容易崩)

  • 执行效率问题:由于其循序渐进的特性,完成一个任务通常需要多次调用 LLM。每一次调用都伴随着网络延迟和计算成本。对于需要很多步骤的复杂任务,这种串行的"思考-行动"循环可能会导致较高的总耗时和费用(速度慢、消耗大)

  • 提示词的脆弱性:整个机制的稳定运行建立在一个精心设计的提示词模板之上。模板中的任何微小变动,甚至是用词的差异,都可能影响 LLM 的行为。此外,并非所有模型都能持续稳定地遵循预设的格式,这增加了在实际应用中的不确定性

  • 可能陷入局部最优 :步进式的决策模式意味着智能体缺乏一个全局的、长远的规划 。它可能会因为眼前的 Observation 而选择一个看似正确但长远来看并非最优的路径,甚至在某些情况下陷入"原地打转"的循环中
    调试技巧

当你构建的 ReAct 智能体行为不符合预期时,可以从以下几个方面入手进行调试:

  • 检查完整的提示词:在每次调用 LLM 之前,将最终格式化好的、包含所有历史记录的完整提示词打印出来。这是追溯 LLM 决策源头的最直接方式
  • 分析原始输出:当输出解析失败时(例如,正则表达式没有匹配到 Action),务必将 LLM 返回的原始、未经处理的文本打印出来。这能帮助你判断是 LLM 没有遵循格式,还是你的解析逻辑有误
  • 验证工具的输入与输出 :检查智能体生成的 tool_input 是否是工具函数所期望的格式,同时也要确保工具返回的 observation 格式是智能体可以理解和处理的
  • 调整提示词中的示例 (Few-shot Prompting):如果模型频繁出错,可以在提示词中加入一两个完整的"Thought-Action-Observation"成功案例,通过示例来引导模型更好地遵循你的指令
  • 尝试不同的模型或参数:更换一个能力更强的模型,或者调整 temperature 参数(通常设为0以保证输出的确定性),有时能直接解决问题
复制代码
工具 'Search' 已注册。

--- 第 1 步 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
Thought: 要回答这个问题,我需要查找华为最新发布的手机型号及其主要特点。这些信息可能在我的现有知识库之外,因此需要使用搜索引擎来获取最新数据。
Action: Search[华为最新手机型号及主要卖点]
🤔 思考: 要回答这个问题,我需要查找华为最新发布的手机型号及其主要特点。这些信息可能在我的现有知识库之外,因此需要使用搜索引擎来获取最新数据。
🎬 行动: Search[华为最新手机型号及主要卖点]
🔍 正在执行 [SerpApi] 网页搜索: 华为最新手机型号及主要卖点
👀 观察: [1] 华为手机- 华为官网
智能手机 ; Mate 系列. 非凡旗舰 · HUAWEI Mate XTs. 非凡大师 ; Pura 系列. 先锋影像 · HUAWEI Pura 80 Pro+ ; Pocket 系列. 美学新篇. HUAWEI Pocket 2 ; nova 系列. 专业人像.

[2] 2025年华为手机哪一款性价比高?华为手机推荐与市场分析 ...
现在华为手机最大的卖点只剩下鸿蒙HarmonyOS系统,以及饱受争议的品牌信仰。 这里推荐目前值得入手的几款华为系列手机,根据不同预算自行选择:. 华为目前最受欢迎,也是搭载 ...

[3] 2025年华为新款手机哪个性价比高?10款华为新款手机推荐
选华为主要还是要推荐高端手机,Mate 70和Pura 70系列是最新发布的旗舰机型。 HUAWEI Mate 70. 优点是,拍照配置依旧顶级,全焦段覆盖,适合专业摄影,做工出色,户外抗摔 ...

--- 第 2 步 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
Thought: 根据搜索结果,华为最新发布的旗舰机型包括Mate 70和Pura 80 Pro+。为了确定最新型号及其主要卖点,我将重点放在这些信息上。从提供的链接来看,Mate 70系列和Pura 80 Pro+都是近期发布的产品,但具体哪一个是"最新"还需要进一步确认。同时,我可以从这些信息中提取出它们的主要
卖点。
Action: Finish[根据最新信息,华为的最新手机可能是HUAWEI Pura 80 Pro+或HUAWEI Mate 70。其中,HUAWEI Mate 70的主要卖点包括顶级的拍照配置,全焦段覆盖,适合专业摄影,做工出色,并且具有良好的户外抗摔性能。而HUAWEI Pura 80 Pro+则强调了先锋影像技术。]
🤔 思考: 根据搜索结果,华为最新发布的旗舰机型包括Mate 70和Pura 80 Pro+。为了确定最新型号及其主要卖点,我将重点放在这些信息上。从提供的链接来看,Mate 70系列和Pura 80 Pro+都是近期发布的产品,但具体哪一个是"最新"还需要进一步确认。同时,我可以从这些信息中提取出它们的主要 
卖点。
🎉 最终答案: 根据最新信息,华为的最新手机可能是HUAWEI Pura 80 Pro+或HUAWEI Mate 70。其中,HUAWEI Mate 70的主要卖点包括顶级的拍照配置,全焦段覆盖,适合专业摄影,做工出色,并且具有良好的户外抗摔性能。而HUAWEI Pura 80 Pro+则强调了先锋影像技术。

Plan-and-Solve

Plan-and-Solve 这种范式将任务处理明确地分为两个阶段:先规划 (Plan),后执行 (Solve)

如果说 ReAct 像一个经验丰富的侦探,根据现场的蛛丝马迹(Observation)一步步推理,随时调整自己的调查方向;那么 Plan-and-Solve 则更像一位建筑师,在动工之前必须先绘制出完整的蓝图(Plan),然后严格按照蓝图来施工(Solve)。事实上我们现在用的很多大模型工具的Agent模式都融入了这种设计模式

Plan-and-Solve 的工作原理

Plan-and-Solve Prompting 由 Lei Wang 在2023年提出。其核心动机是为了解决思维链在处理多步骤、复杂问题时容易"偏离轨道"的问题
与 ReAct 将思考和行动融合在每一步不同,Plan-and-Solve 将整个流程解耦为两个核心阶段

  • 规划阶段 (Planning Phase): 首先,智能体会接收用户的完整问题。它的第一个任务不是直接去解决问题或调用工具,而是将问题分解,并制定出一个清晰、分步骤的行动计划。这个计划本身就是一次大语言模型的调用产物
  • 执行阶段 (Solving Phase) : 在获得完整的计划后,智能体进入执行阶段。它会严格按照计划中的步骤,逐一执行。每一步的执行都可能是一次独立的 LLM 调用,或者是对上一步结果的加工处理,直到计划中的所有步骤都完成,最终得出答案

这种"先谋后动"的策略,使得智能体在处理需要长远规划的复杂任务时,能够保持更高的目标一致性,避免在中间步骤中迷失方向

Plan-and-Solve 尤其适用于那些结构性强、可以被清晰分解的复杂任务,例如:

  • 多步数学应用题:需要先列出计算步骤,再逐一求解
  • 需要整合多个信息源的报告撰写:需要先规划好报告结构(引言、数据来源A、数据来源B、总结),再逐一填充内容
  • 代码生成任务:需要先构思好函数、类和模块的结构,再逐一实现

Plan-and-Solve 范式的工作流程

规划阶段 : 智能体首先调用 规划器 Planner,成功地将复杂的应用题分解成了一个包含四个逻辑步骤的 Python 列表。这个结构化的计划为后续的执行奠定了基础

执行阶段执行器 Executor 严格按照生成的计划,一步一步地向下执行。在每一步中,它都将历史结果作为上下文 ,确保了信息的正确传递(例如,步骤2正确地使用了步骤1的结果"15个",步骤3也正确使用了步骤2的结果"30个")

结果:整个过程逻辑清晰,步骤明确,最终智能体准确地得出了正确答案"70个"

python 复制代码
# 规划器提示词
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。

问题: {question}

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

# 执行器提示词
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

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

# 当前步骤:
{current_step}

请仅输出针对"当前步骤"的回答:
"""
复制代码
--- 开始处理问题 ---
问题: 一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?
--- 正在生成计划 ---
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
 ```python
["计算周一卖出的苹果数量: 15个", "计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个", "计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个", "计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个"] ```
✅ 计划已生成:
 ```python
["计算周一卖出的苹果数量: 15个", "计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个", "计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个", "计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个"]```

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

-> 正在执行步骤 1/4: 计算周一卖出的苹果数量: 15个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
15
✅ 步骤 1 已完成,结果: 15

-> 正在执行步骤 2/4: 计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
30
✅ 步骤 2 已完成,结果: 30

-> 正在执行步骤 3/4: 计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
25
✅ 步骤 3 已完成,结果: 25

-> 正在执行步骤 4/4: 计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
70
✅ 步骤 4 已完成,结果: 70

--- 任务完成 ---
最终答案: 70

Reflection

在之前学习的 ReActPlan-and-Solve 范式中,智能体一旦完成了任务,其工作流程便告结束。然而,它们生成的初始答案,无论是行动轨迹还是最终结果,都可能存在谬误或有待改进之处。Reflection 机制的核心思想,正是为智能体引入一种事后(post-hoc)的自我校正循环,使其能够像人类一样,审视自己的工作,发现不足,并进行迭代优化

Reflection 机制的核心思想

Reflection 机制的灵感来源于人类的学习过程:我们完成初稿后会进行校对,解出数学题后会进行验算。这一思想在多个研究中得到了体现,例如 Shinn, Noah 在2023年提出的 Reflexion 框架。其核心工作流程可以概括为一个简洁的三步循环:执行 -> 反思 -> 优化
执行 (Execution) :首先,智能体使用我们熟悉的方法(如 ReActPlan-and-Solve)尝试完成任务,生成一个初步的解决方案或行动轨迹。这可以看作是"初稿"

反思 (Reflection) :接着,智能体进入反思阶段。它会调用一个独立的、或者带有特殊提示词的大语言模型实例,来扮演一个"评审员"的角色。这个"评审员"会审视第一步生成的"初稿",并从多个维度进行评估,例如:

  • 事实性错误:是否存在与常识或已知事实相悖的内容?
  • 逻辑漏洞:推理过程是否存在不连贯或矛盾之处?
  • 效率问题:是否有更直接、更简洁的路径来完成任务?
  • 遗漏信息 :是否忽略了问题的某些关键约束或方面? 根据评估,它会生成一段结构化的反馈 (Feedback),指出具体的问题所在和改进建议

优化 (Refinement):最后,智能体将"初稿"和"反馈"作为新的上下文,再次调用大语言模型,要求它根据反馈内容对初稿进行修正,生成一个更完善的"修订稿"

与前两种范式相比,Reflection 的价值在于:

它为智能体提供了一个内部纠错回路,使其不再完全依赖于外部工具的反馈(ReAct 的 Observation),从而能够修正更高层次的逻辑和策略错误

它将一次性的任务执行,转变为一个持续优化的过程,显著提升了复杂任务的最终成功率和答案质量

它为智能体构建了一个临时的"短期记忆"。整个"执行-反思-优化"的轨迹形成了一个宝贵的经验记录,智能体不仅知道最终答案,还记得自己是如何从有缺陷的初稿迭代到最终版本的。更进一步,这个记忆系统还可以是多模态的,允许智能体反思和修正文本以外的输出(如代码、图像等),为构建更强大的多模态智能体奠定了基础

Reflection 示例

复制代码
--- 开始处理任务 ---
任务: 编写一个Python函数,找出1到n之间所有的素数 (prime numbers)。

--- 正在进行初始尝试 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
```python
def find_primes(n):
    ...
    return primes```
📝 记忆已更新,新增一条 'execution' 记录。   

--- 第 1/2 轮迭代 ---

-> 正在进行反思...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
当前代码的时间复杂度为O(n * sqrt(n))。虽然对于较小的n值,这种实现是可以接受的,但当n非常大时,性能会显著下降。主要瓶颈在于每个数都需要进行试除法检查,这导致了较高的时间开销。

建议使用埃拉托斯特尼筛法(Sieve of Eratosthenes),该算法的时间复杂度为O(n log(log n)),能够显著提高查找素数的效率。

改进后的代码如下:
```python
def find_primes(n):
    ...
    return primes```
📝 记忆已更新,新增一条 'reflection' 记录。

-> 正在进行优化...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
```python
def find_primes(n):
    ...
    return primes```
📝 记忆已更新,新增一条 'execution' 记录。

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

-> 正在进行反思...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
当前代码使用了Eratosthenes筛法,时间复杂度为O(n log log n),空间复杂度为O(n)。此算法在寻找1到n之间的所有素数时已经非常高效,通常情况下无需进一步优化。但在某些特定场景下,可以考虑以下改进:

1. <strong>分段筛法(Segmented Sieve)</strong>:适用于n非常大但内存有限的情况。将区间分成多个小段,每段分别用筛法处理,减少内存使用。
2. <strong>奇数筛法(Odd Number Sieve)</strong>:除了2以外,所有素数都是奇数。可以在初始化`is_prime`数组时只标记奇数,这样可以将空间复杂度降低一半,同时减少一些不必要的计算。

然而,这些改进对于大多数应用场景来说并不是必需的,因为标准的Eratosthenes筛法已经足够高效。因此,在一般情况下,<strong>无需改进</strong>。
📝 记忆已更新,新增一条 'reflection' 记录。

✅ 反思认为代码已无需改进,任务完成。

--- 任务完成 ---
最终生成的代码:
```python
def find_primes(n):
    """
    Finds all prime numbers between 1 and n using the Sieve of Eratosthenes algorithm.

    :param n: The upper limit of the range to find prime numbers.
    :return: A list of all prime numbers between 1 and n.
    """
    if n < 2:
        return []

    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False

    p = 2
    while p * p <= n:
        if is_prime[p]:
            for i in range(p * p, n + 1, p):
                is_prime[i] = False
        p += 1

    primes = [num for num in range(2, n + 1) if is_prime[num]]
    return primes```

Reflection 机制的成本收益分析

尽管 Reflection 机制在提升任务解决质量上表现出色,但这种能力的获得并非没有代价。在实际应用中,我们需要权衡其带来的收益与相应的成本

(1)主要成本

  • 模型调用开销增加 : 这是最直接的成本。每进行一轮迭代,至少需要额外调用两次大语言模型(一次用于反思,一次用于优化)。如果迭代多轮,API 调用成本和计算资源消耗将成倍增加

  • 任务延迟显著提高 : Reflection 是一个串行过程,每一轮的优化都必须等待上一轮的反思完成。这使得任务的总耗时显著延长,不适合对实时性要求高的场景

  • 提示工程复杂度上升 : 如我们的案例所示,Reflection 的成功在很大程度上依赖于高质量、有针对性的提示词。为"执行"、"反思"、"优化"等不同阶段设计和调试有效的提示词,需要投入更多的开发精力
    (2)核心收益

  • 解决方案质量的跃迁 : 最大的收益在于,它能将一个"合格"的初始方案,迭代优化成一个"优秀"的最终方案。这种从功能正确到性能高效、从逻辑粗糙到逻辑严谨的提升,在很多关键任务中是至关重要的

  • 鲁棒性与可靠性增强: 通过内部的自我纠错循环,智能体能够发现并修复初始方案中可能存在的逻辑漏洞、事实性错误或边界情况处理不当等问题,从而大大提高了最终结果的可靠性
    综上所述,Reflection 机制是一种典型的"以成本换质量"的策略。它非常适合那些对最终结果的质量、准确性和可靠性有极高要求,且对任务完成的实时性要求相对宽松的场景。例如:

  • 生成关键的业务代码或技术报告

  • 在科学研究中进行复杂的逻辑推演

  • 需要深度分析和规划的决策支持系统

反之,如果应用场景需要快速响应,或者一个"大致正确"的答案就已经足够,那么使用更轻量的 ReActPlan-and-Solve 范式可能会是更具性价比的选择

小结

核心知识点回顾

  • ReAct : 我们构建了一个能与外部世界交互的 ReAct 智能体。通过"思考-行动-观察"的动态循环,它成功地利用搜索引擎回答了自身知识库无法覆盖的实时性问题。其核心优势在于环境适应性动态纠错能力,使其成为处理探索性、需要外部工具输入的任务的首选
  • Plan-and-Solve : 我们实现了一个先规划后执行的 Plan-and-Solve 智能体,并利用它解决了需要多步推理的数学应用题。它将复杂的任务分解为清晰的步骤,然后逐一执行。其核心优势在于结构性稳定性,特别适合处理逻辑路径确定、内部推理密集的任务
  • Reflection (自我反思与迭代) : 我们构建了一个具备自我优化能力的 Reflection 智能体。通过引入"执行-反思-优化"的迭代循环,它成功地将一个效率较低的初始代码方案,优化为了一个算法上更优的高性能版本。其核心价值在于能显著提升解决方案的质量,适用于对结果的准确性和可靠性有极高要求的场景

上下文工程

什么是上下文工程

在经历了数年**提示词工程(Prompt Engineering)**成为应用型AI的焦点之后,一个新的术语开始走到台前:上下文工程(Context Engineering) 。如今,用语言模型构建系统不再只是找对提示词里的句式和措辞,而是要回答一个更宏观的问题:什么样的上下文配置,最有可能让模型产出我们期望的行为?
所谓"上下文",是指在对大语言模型(LLM)进行采样时所包含的那组 tokens(系统提示词,当前问题,历史对话记录,工具返回的搜索结果,记忆、笔记、外部文件资料,上一轮的思考、行动、观察等,这些内容在底层都是一小段文字单元 tokens)。手头的工程问题,是在 LLM 的固有约束之下,优化这些 tokens 的效用,以便稳定地得到预期结果。想要有效驾驭 LLM,往往需要"在上下文中思考"------也就是说:在任何一次调用时,都要审视 LLM 可见的整体状态,并预判这种状态可能诱发的行为
上下文工程 vs. 提示工程

在现在前沿模型厂商的视角中,上下文工程是提示工程的自然演进 。提示工程关注如何编写与组织 LLM 的指令以获得更优结果(例如系统提示的写法与结构化策略);而上下文工程则是在推理阶段,如何策划与维护"最优的信息集合(tokens)",其中不仅包含提示本身,还包含其他会进入上下文窗口的一切信息

LLM 工程的早期阶段,提示往往是主要工作,因为大多数用例(除日常聊天外)都需要针对单轮分类文本生成做精调式的提示优化 。顾名思义,提示工程的核心是"如何写出有效提示",尤其是系统提示

然而,随着我们开始工程化地构建更强的智能体,它们在更长的时间范围内、跨多次推理轮次地工作,我们就需要能管理整个上下文状态的策略------其中包括系统指令、工具、MCP(Model Context Protocol)、外部数据、消息历史

一个循环运行的智能体,会不断产生下一轮推理可能相关的数据,这些信息必须被周期性地提炼。因此,上下文工程的"艺与术",在于从持续扩张的"候选信息宇宙"中,甄别哪些内容应当进入有限的上下文窗口(从一大堆杂乱信息里筛选、提炼、精简、结构化只把「当前这一步真正需要」的内容,放进有限的上下文窗口里)

为什么上下文工程重要

尽管模型的速度越来越快、可处理的数据规模越来越大,但我们观察到:LLM 和人类一样,在一定点上会"走神"或"混乱"。针堆找针(needle-in-a-haystack)类基准揭示了一个现象:上下文腐蚀(context rot) ------随着上下文窗口中的 tokens 增加,模型从上下文中准确回忆信息的能力反而下降(上下文内容越多,越难精准提取关键信息,容易混乱、走神,该问题普遍存在于所有大模型
不同模型的退化曲线或许更平滑,但这一特征几乎在所有模型上都会出现。因此,上下文必须被视作一种有限资源 ,且具有边际收益递减。就像人类有有限的工作记忆容量一样,LLM 也有一笔"注意力预算"。每新增一个 token,都会消耗这笔预算的一部分,因此我们更需要谨慎地筛选哪些 tokens 应该被提供给 LLM(额外信息会分散注意力,内容越多,模型效果边际变差
这种稀缺并非偶然,而是源自 LLM 的架构约束。Transformer 让每个 token 能够与上下文中的所有 token 建立关联,理论上形成 (n^2) 级别的两两注意力关系。随着上下文长度增长,模型对这些两两关系的建模能力会被"拉薄",从而自然地产生"上下文规模"与"注意力集中度"的张力。此外,模型的注意力模式来源于训练数据分布------短序列通常比长序列更常见,因此模型对"全上下文依赖"的经验更少、专门参数也更少( Transformer 架构限制(长文本会稀释注意力)与训练数据特性(更擅长短文本)
诸如位置编码插值(position encoding interpolation)等技术可以让模型在推理时"适配"比训练期更长的序列,但会牺牲部分对 token 位置的精确理解。总体上,这些因素共同形成的是一个性能梯度,而非"悬崖式"崩溃:模型在长上下文下依旧强大,但相较短上下文,在信息检索与长程推理上的精度会有所下降(长上下文不会让模型直接失效,但会削弱信息查找、长逻辑推理的能力
基于上述现实,有意识的上下文工程就成为构建强健智能体的必需品

有效上下文的"解剖学"

在"有限注意力预算"的约束下,优秀的上下文工程目标是:用尽可能少、但高信号密度的 tokens,最大化获得期望结果的概率 。落实到实践中,我们建议围绕以下组件开展工程化建设:系统提示工具示例
系统提示(System Prompt):语言清晰、直白,信息层级把握在"刚刚好"的高度。常见两极误区:

  • 过度硬编码:在提示中写入复杂、脆弱的 if-else 逻辑,长期维护成本高、很容易出错、一改动就崩
  • 过于空泛 :只给出宏观目标与泛化指引,缺少对期望输出的具体信号或假定了错误的"共享上下文"(只说大方向,没有具体要求)

✅ 正确写法:

  • 将提示分区组织(如:角色、规则、工具指引、输出描述等),用 XML/Markdown 分隔

  • 无论格式如何,追求的是能完整勾勒期望行为的"最小必要信息集"("最小"并不等于"最短")

  • 先用最好的模型在最小提示上试跑,再依据失败模式增补清晰的指令与示例
    工具(Tools):工具定义了智能体与信息/行动空间的契约,必须促进效率:既要返回token 友好的信息,又要鼓励高效的智能体行为。工具应当:

  • 职责单一、相互低重叠,接口语义清晰(一个工具只干一件事,功能不重复、不打架

  • 对错误鲁棒(能容错,不容易报错崩溃

  • 入参描述明确、无歧义 ,充分发挥模型擅长的表达与推理能力。 常见失败模式是"臃肿工具集":功能边界模糊,导致"选哪个工具"这一决策本身就含混不清。如果人类工程师都说不准用哪个工具,别指望智能体做得更好。精心甄别一个"最小可行工具集(MVTS)"往往能显著提升长期交互中的稳定性与可维护性
    示例(Few-shot)始终推荐提供示例,但不建议把"所有边界条件"的罗列一股脑塞进提示。请精挑细选一组多样且典型的示例,直接画像"期望行为"。对 LLM 而言,好的示例胜过千言万语

上下文检索与智能体式搜索

一个简洁的定义:智能体 = 在循环中自主调用工具的 LLM 。随着底层模型能力增强,智能体的自治水平便可提升:更能独立探索复杂问题空间,并从错误中恢复(模型越强,智能体越独立:能自己解决复杂问题、犯错后自己修正
工程实践正在从"推理前一次性检索(embedding 检索)"逐步过渡到"及时(Just-in-time, JIT)上下文 "

传统 RAG

  • 提前把所有相关文档、资料一次性检索出来,全部塞进上下文,固定流程,强行把检索到的内容全部塞进 Prompt,LLM被动接收
  • 缺点 :信息爆炸、冗余、浪费 token、模型容易混乱

及时上下文

  • 不再预先加载所有相关数据,而是维护轻量化引用 (文件路径、存储查询、URL 等),在运行时通过工具动态加载所需数据LLM决定何时去检索,检索什么
  • 分析大体量数据 :无需把整块数据一次性塞入上下文,只看头尾、关键片段
  • 认知模式更贴近人类 :我们不会死记硬背全部信息,而是用文件系统、收件箱、书签等外部索引按需提取
    除了存储效率,引用的元数据本身也能帮助精化行为(文件 / 路径本身就是信息,本身就能暗示内容用途、新旧、重要程度,AI 光看路径,就能辅助判断,不用读全文):目录层级、命名约定、时间戳等都在隐含地传达"目的与时效"。例如,tests/test_utils.py 与 src/core/test_utils.py 的语义暗示就不同
    允许智能体自主导航与检索还能实现渐进式披露(progressive disclosure):每一步交互都会产生新的上下文,反过来指导下一步决策------文件大小暗示复杂度、命名暗示用途、时间戳暗示相关性。智能体得以按层构建理解,只在工作记忆中保留"当前必要子集",并用"记笔记"的方式做补充持久化,从而维持聚焦而非"被大而全拖垮"
    需要权衡的是:按需实时读取、动态检索,速度更慢如果工具设计不合理,AI 容易乱调用、钻死胡同、浪费上下文

在不少场景中,混合策略 更有效:前置加载少量"高价值"上下文(System Prompt 中的「固定背景 / 规则资料部分」 )以保证速度,然后允许智能体按需继续自主探索(复杂细节、冷门文件,让 AI 自己按需检索、读取 )。边界的选择取决于任务动态性与时效要求。在工程上,可以预先放入类似"项目约定说明(如 README/指南)"的文件,同时提供 glob(按名字 / 类型查找文件)grep(在文件里关键词搜索内容) 等原语,让智能体即时检索具体文件,从而绕开过时索引与复杂语法树的沉没成本
总结

检索模式重大升级

  • 传统模式(老式 RAG)
    提前对文档切片、向量化、存入向量库;用户提问后代码强制批量检索,一次性塞入上下文,LLM 被动接收;文件更新后向量索引过期,维护成本高(重新切片 → 重新生成向量 → 重新入库)
  • 新式模式(JIT 即时上下文)
    不提前加载全部数据,只保留文件路径、索引等轻量引用;由 LLM 自主判断需求,运行时动态检索、按需加载内容,用完不常驻,从根源减少上下文腐蚀

核心架构:双层混合存储(关键核心)

  1. 静态固定内容 → 向量数据库 + RAG
  • 适用:长期不变的知识库、行业规范、产品手册、静态文档
  • 能力:支持语义检索,可匹配近义词、相关概念,适合知识类查询
  1. 动态频繁变更内容 → 本地原始文件(文件夹 / 服务器目录)
  • 适用:代码、README、项目规范、配置文件、迭代中的文档。
  • 能力 :通过 glob(查文件路径)grep(关键词精准匹配)实时读取,文件修改后即时生效,无索引过期问题,零维护成本
  1. 两者不可替代
  • 本地文件工具关键字精准匹配,无语义理解;
  • 向量 RAG :主打语义理解,弥补关键词检索的短板;
  • 工业方案双模式混合使用

智能体如何自主选择检索工具

  • 无需人工干预、无需手动标注文件功能:系统提示明确两种工具的使用场景;每个工具配备清晰功能描述;
  • LLM 依靠自身推理自主决策:
    (1)查知识、概念、模糊需求 → 调用 RAG 语义检索
    (2)查代码、配置、项目文件、精准关键词 → 调用 glob/grep 本地检索
    (3)本地文件理解:模型依靠项目目录结构、文件名、工程惯例自动识别文件作用,无需额外说明

上下文加载策略(平衡速度与灵活性)

  • 前置轻量加载:在系统提示中放入少量高价值固定信息(项目简介、核心规则),无需检索,保证响应速度
  • 运行时动态加载:核心资料不默认全量塞入,智能体根据任务进展,主动调用工具补充所需上下文
  • 渐进式披露 :智能体分层获取信息,只保留工作记忆中必要内容,搭配笔记工具做长期记忆,防止上下文膨胀

优势与取舍

  • 优势:解决索引过期问题、降低维护成本、抑制上下文腐蚀、提升长任务稳定性、让智能体具备自主探索能力
  • 取舍 :即时动态检索速度低于预检索,需要合理的工具规则约束,防止智能体滥用工具、无效探索

整体运行闭环

智能体(循环自主调用工具的 LLM)+ 轻量化常驻系统提示 + 向量 RAG 语义检索 + 本地文件精准检索 → 按需动态拼接每轮上下文 → 兼顾速度、准确度、灵活性,是现代高级智能体的标准架构

面向长时程任务的上下文工程

长时程任务 ,例如大型代码库迁移、跨数小时的系统性研究,行动步骤极多、交互轮次巨长,远远超出大模型上下文窗口上限 。指望无限增大上下文窗口并不能根治"上下文污染"与相关性退化的问题,即使窗口再大,依然会出现上下文污染、信息杂乱、关键信息被淹没、注意力稀释 ,因此需要直接面向这些约束的工程手段:压缩整合(Compaction)结构化笔记(Structured note-taking)子代理架构(Sub-agent architectures)

压缩整合(Compaction)------ 解决窗口过载问题

  • 定义 :当对话接近上下文上限时,对其进行高保真总结,并用该摘要重启一个新的上下文窗口,以维持长程连贯性
  • 实践 :让模型压缩并保留架构性决策、未解决缺陷、实现细节,丢弃重复的工具输出与噪声;新窗口携带压缩摘要 + 最近少量高相关工件(如"最近访问的若干文件")
  • 调参建议 :先优化召回(确保不遗漏关键信息),再优化精确度(剔除冗余内容);一种安全的"轻触式"压缩是对"深历史中的工具调用与结果"进行清理

结构化笔记(Structured note-taking)------ 解决遗忘问题

  • 定义 :也称"智能体记忆"。智能体以固定频率将关键信息写入上下文外的持久化存储 ,在后续阶段按需拉回(定期把关键信息写到外部文件 / 长效记忆中),只是在进行额外备份,当前上下文的对话、工具记录,完全保留、不会清空
  • 价值 :以极低的上下文开销维持持久状态与依赖关系。哪怕智能体中途调用了几十上百次工具 或者 因为上下文过载,多次压缩、多次重置、换了好几个新上下文窗口,只要读取结构化笔记(外部持久化记忆),就能立刻知道:之前做到哪一步、有哪些待办、哪里卡住、关键规则是什么,保持进度和一致性
  • 说明 :在非编码场景中同样有效(如长期策略性任务、游戏/仿真中的目标管理与统计计数)

子代理架构(Sub-agent architectures)

  • 思想 :由主代理 负责高层规划与综合,多个专长子代理 在"干净的上下文窗口"中各自深挖、调用工具并探索,最后仅回传凝练摘要(常见 1,000--2,000 tokens)
  • 好处:实现关注点分离。庞杂的搜索上下文留在子代理内部,主代理专注于整合与推理;适合需要并行探索的复杂研究/分析任务
  • 经验 :公开的多智能体研究系统显示,该模式在复杂研究任务上相较单代理基线具有显著优势

方法取舍可以遵循以下经验法则:

  • 压缩整合 :适合需要长对话连续性的任务,强调上下文的"接力"(任务从头到尾是一条直线,不能断、逻辑要连续。例如:连续几小时的长篇写作、持续对话、单线问题排查)
  • 结构化笔记 :适合有里程碑/阶段性成果的迭代式开发与研究(例如:代码项目开发、分阶段学术研究、需求迭代,AI 读取笔记就能接着上一阶段继续迭代,不会丢进度)
  • 子代理架构 :适合复杂研究与分析,能从并行探索中获益(拆分复杂、并行干活、降低主 Agent 压力)

GSSC 流水线详解(单轮上下文的加工流水线)

ContextBuilderHello-Agents(Datawhale 免费 AI 学习社区开发的「智能体入门必修课」配套代码框架) 中管理智能体上下文 的核心组件,通过 收集 → 筛选 → 结构化 → 压缩 四步固定流水线,解决上下文杂乱、token 超限、信息过载、上下文腐蚀等问题,让喂给大模型的信息精准、干净、有序、可控

  1. gather(收集)
  • 核心作用 :全量汇集所有可用信息,统一打包,把所有能用到的信息全部收集:系统指令长期记忆RAG 知识库最近若干条对话历史自定义信息
  • 所有信息统一封装为 ContextPacket 标准格式(包含内容、时间、token、相关性)
    系统指令标记为最高优先级,必须保留
  1. select(筛选)
  • 核心作用 :择优录取,严控 token 预算,系统指令全额保留,不参与筛选
  • 算分数:剩余信息按 70% 相关性 + 30% 新近性 计算综合得分
  • RAG 信息用专业向量分数评分相关性,其他信息用关键词兜底评分
  • 严筛选:按分数从高到低选择,token 预算花完立刻停止,过滤低分无用信息
    实现:只留高价值信息,拒绝无效内容挤占空间
  1. structure(结构化)
  • 核心作用 :标准化排版,固定模板格式,将筛选后的零散信息,自动分成5 大固定板块:

    Role \& Policies\] 角色与规则 \[Task\] 当前任务(用户问题) \[Evidence\] 证据资料(RAG 检索内容) \[Context\] 历史上下文(对话 / 记忆) \[Output\] 输出要求:格式固定清晰,让大模型更容易理解,方便调试和优化

  • 核心作用:超限瘦身,兜底防崩溃

  • 统计最终上下文的 token 数量,未超过预算直接返回,按标准化模板从前往后保留,保头保尾(角色 / 任务 / 输出),截断中间超长内容,保证上下文永远不超出模型 token 上限,避免报错、上下文污染

整体一句话闭环:gather 负责查询收集信息 (包括及时上下文) → select 负责筛选相关信息 → structure 负责标准化排版上下文信息 → compress 负责把超长信息压缩,四步联动,实现了上下文工程的全流程自动化,让智能体始终在干净、精简、高质量的上下文里工作,回答更准、长任务更稳

  • 简单任务:上下文工程 = GSSC 单次执行
  • 中等 Agent 任务(工具调用、多轮):上下文工程 = 循环迭代 GSSC
  • 超长时程任务(几小时、超大项目):上下文工程 = 以 GSSC 为底座 + 压缩整合 / 结构化笔记 / 子代理 做高阶兜底
复制代码
================================================================================
构建的上下文:
================================================================================
[Role & Policies]
你是一位资深的Python数据工程顾问。你的回答需要:1) 提供具体可行的建议 2) 解释技术原理 3) 给出代码示例

[Task]
如何优化Pandas的内存占用?

[Evidence]
Pandas内存优化的核心策略包括:
1. 使用合适的数据类型(如category代替object)
2. 分块读取大文件
3. 使用 chunksize 参数
---
数据类型优化可以显著减少内存占用。例如,将int64降级为int32可以节省50%的内存。

[Context]
user: 我正在开发一个数据分析工具
assistant: 很好!数据分析工具通常需要处理大量数据。您计划使用什么技术栈?
user: 我打算使用Python和Pandas,已经完成了CSV读取模块
assistant: 不错的选择!Pandas在数据处理方面非常强大。接下来您可能需要考虑数据清洗和转换。
记忆: 用户正在开发数据分析工具,使用Python和Pandas
记忆: 已完成CSV读取模块的开发

[Output]
请基于以上信息,提供准确、有据的回答。
================================================================================

在实际应用 ContextBuilder 时,以下几点最佳实践值得注意:

  • 动态调整 token 预算 :根据任务复杂度动态调整 max_tokens,简单任务使用较小预算,复杂任务增加预算
  • 相关性计算优化 :在生产环境中,将简单的关键词重叠替换为向量相似度计算,提升检索质量
  • 缓存机制 :对于不变的系统指令和知识库内容,可以实现缓存机制,避免重复计算
  • 监控与日志 :记录每次上下文构建的统计信息(选中信息数量、token 使用率等),便于后续优化
  • A/B 测试 :对于关键参数(如相关性权重新近性权重),通过 A/B 测试找到最优配置

NoteTool:结构化笔记

NoteTool 是为"长时程任务"提供的结构化外部记忆组件。它以 Markdown 文件作为载体,头部使用 YAML 前置元数据记录关键信息,正文用于记录状态、结论、阻塞与行动项等内容。这种设计结合了人类可读性、版本控制友好性(Git 保存记录)和易于回注上下文(需要的时候,可以轻松把笔记内容读进 AI 上下文里,辅助回答)的特性,是构建长时程智能体的重要工具

设计理念与应用场景

为什么需要 NoteTool?

对于需要长期追踪结构化管理的项目式任务,我们 需要使用 NoteTool

  • 结构化记录 :使用 Markdown + YAML 格式,既适合机器解析,也方便人类阅读和编辑
  • 版本友好 :纯文本格式,天然支持 Git 等版本控制系统
  • 低开销:无需复杂的数据库操作,适合轻量级的状态追踪
  • 灵活分类 :通过 typetags 灵活组织笔记,支持多维度检索(可以给笔记打标签、分类型)
    NoteTool 特别适合以下场景:

场景1:长期项目追踪

想象一个智能体正在协助完成一个大型代码库的重构任务,这可能需要几天甚至几周。NoteTool 可以记录:

  • task_state:当前阶段的任务状态和进度
  • conclusion:每个阶段结束后的关键结论
  • blocker:遇到的问题和阻塞点
  • action:下一步的行动计划

场景2:研究任务管理

一个智能研究助手在进行文献综述时,可以使用 NoteTool 记录:

  • 每篇论文的核心观点(conclusion)
  • 待深入调研的主题(action)
  • 重要的参考文献(reference)

场景3:与 ContextBuilder 配合

在每轮对话前,Agent 可以通过 searchlist 操作检索相关笔记,并将其注入到上下文

python 复制代码
# 在 Agent 的 run 方法中
def run(self, user_input: str) -> str:
    # 1. 检索相关笔记
    relevant_notes = self.note_tool.run({
        "action": "search",
        "query": user_input,
        "limit": 3
    })

    # 2. 将笔记内容转换为 ContextPacket
    note_packets = []
    for note in relevant_notes:
        note_packets.append(ContextPacket(
            content=note['content'],
            timestamp=note['updated_at'],
            token_count=self._count_tokens(note['content']),
            relevance_score=0.7,
            metadata={"type": "note", "note_type": note['type']}
        ))

    # 3. 构建上下文时传入笔记
    context = self.context_builder.build(
        user_query=user_input,
        custom_packets=note_packets,
        ...
    )

存储格式详解

NoteTool 采用了 Markdown + YAML 的混合格式,这种设计兼顾了结构化和可读性
(1)笔记文件格式:每个笔记都是一个独立的 .md 文件,这种格式的优势:

  • YAML 元数据:机器可解析,支持精确的字段提取和检索
  • Markdown 正文:人类可读,支持丰富的格式化(标题、列表、代码块等)
  • 文件名即 ID :简化管理,每个笔记的文件名就是其唯一标识
复制代码
---
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%

(2)索引文件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"
  }
}

最佳实践

在实际使用 NoteTool 时,以下最佳实践能帮助您构建更强大的长时程智能体

(1) 合理的笔记分类

  • task_state:记录阶段性进展和状态
  • conclusion:记录重要的结论和发现
  • blocker:记录阻塞问题,优先级最高
  • action:记录下一步行动计划
  • reference:记录重要的参考资料

(2) 定期清理和归档

  • 对于已解决的 blocker,更新为 conclusion
  • 对于过时的 action,及时删除或更新
  • 使用 tags 进行版本管理,如 ["v1.0", "completed"]

(3) 与 ContextBuilder 的配合

  • 在每轮对话前检索相关笔记
  • 根据笔记类型设置不同的相关性分数(blocker > action > conclusion)
  • 限制笔记数量,避免上下文过载

(4) 人机协作

  • 笔记是人类可读的 Markdown 格式,支持手动编辑
  • 使用 Git 进行版本控制,追踪笔记的演化
  • 在关键阶段,人工审核 Agent 生成的笔记

(5) 自动化工作流

  • 定期生成笔记摘要报告
  • 基于笔记自动生成项目进度文档
  • 将笔记内容同步到其他系统(如 Notion、Confluence)

TerminalTool:即时文件系统访问

在许多实际场景中,智能体需要即时访问和探索文件系统------查看日志文件、分析代码库结构、检索配置文件等(只操作本地文件,而不是像 RAGToolMemoryTool操作向量数据库中的文件)。这就是 TerminalTool 的用武之地
TerminalTool 为智能体提供了安全的命令行执行能力,支持常用的文件系统和文本处理命令,同时通过多层安全机制确保系统安全(不能乱删文件、不能乱改系统、防止搞破坏)。这种设计实现了"即时(Just-in-time, JIT)上下文"理念------智能体不需要预先加载所有文件,而是按需探索和检索

设计理念与安全机制

场景1:代码库探索

一个开发助手需要帮助用户理解一个大型代码库的结构:

复制代码
# 传统方式:预先索引所有文件(成本高、可能过时)
rag_tool.add_document("./project/**/*.py")  # 耗时、占用大量存储

# TerminalTool 方式:即时探索
terminal.run({"command": "find . -name '*.py' -type f"})  # 快速、实时
terminal.run({"command": "grep -r 'class UserService' ."})  # 精确定位
terminal.run({"command": "head -n 50 src/services/user.py"})  # 按需查看

场景2:日志文件分析

一个运维助手需要分析应用日志:

复制代码
# 检查日志文件大小
terminal.run({"command": "ls -lh /var/log/app.log"})

# 查看最新的错误日志
terminal.run({"command": "tail -n 100 /var/log/app.log | grep ERROR"})

# 统计错误类型分布
terminal.run({"command": "grep ERROR /var/log/app.log | cut -d':' -f3 | sort | uniq -c"})

场景3:数据文件预览

一个数据分析助手需要快速了解数据文件的结构:

复制代码
# 查看 CSV 文件的前几行
terminal.run({"command": "head -n 5 data/sales.csv"})

# 统计行数
terminal.run({"command": "wc -l data/*.csv"})

# 查看列名
terminal.run({"command": "head -n 1 data/sales.csv | tr ',' '\n'"})

上面这些场景的共同特点是:需要实时轻量级文件系统访问,而不是预先索引和向量化。TerminalTool 正是为这种"探索式"工作流设计的


安全机制详解

允许智能体执行命令是一个强大但危险的能力。TerminalTool 通过多层安全机制确保系统安全
第一层:命令白名单

只允许安全的只读命令,完全禁止任何可能修改系统的操作:

复制代码
ALLOWED_COMMANDS = {
    # 文件列表与信息
    'ls', 'dir', 'tree',
    # 文件内容查看
    'cat', 'head', 'tail', 'less', 'more',
    # 文件搜索
    'find', 'grep', 'egrep', 'fgrep',
    # 文本处理
    'wc', 'sort', 'uniq', 'cut', 'awk', 'sed',
    # 目录操作
    'pwd', 'cd',
    # 文件信息
    'file', 'stat', 'du', 'df',
    # 其他
    'echo', 'which', 'whereis',
}


# 如果智能体尝试执行白名单外的命令,会立即被拒绝:
terminal.run({"command": "rm -rf /"})
# ❌ 不允许的命令: rm
# 允许的命令: cat, cd, cut, dir, du, ...

第二层:工作目录限制(沙箱)

TerminalTool 只能访问指定的工作目录及其子目录,无法访问系统其他部分,这种沙箱机制确保了即使智能体的行为出现异常,也无法影响系统其他部分

复制代码
# 初始化时指定工作目录
terminal = TerminalTool(workspace="./project")

# 允许:访问工作目录内的文件
terminal.run({"command": "cat ./src/main.py"})  # ✅

# 禁止:访问工作目录外的文件
terminal.run({"command": "cat /etc/passwd"})  # ❌ 不允许访问工作目录外的路径

# 禁止:通过 .. 逃逸
terminal.run({"command": "cd ../../../etc"})  # ❌ 不允许访问工作目录外的路径

第三层:超时控制

每个命令都有执行时间限制,防止无限循环或资源耗尽:

复制代码
terminal = TerminalTool(
    workspace="./project",
    timeout=30  # 30秒超时
)

# 如果命令执行超过30秒
terminal.run({"command": "find / -name '*.log'"})
# ❌ 命令执行超时(超过 30 秒)

第四层:输出大小限制

限制命令输出的大小,防止内存溢出:

复制代码
terminal = TerminalTool(
    workspace="./project",
    max_output_size=10 * 1024 * 1024  # 10MB
)

# 如果输出超过10MB
terminal.run({"command": "cat huge_file.log"})
# ... (前10MB的内容) ...
# ⚠️ 输出被截断(超过 10485760 字节)

与其他工具的协同

(1)与 MemoryTool 协同

TerminalTool 发现的信息可以存储到记忆系统中:

复制代码
# 使用 TerminalTool 发现项目结构
structure = terminal.run({"command": "tree -L 2 src"})

# 存储到语义记忆
memory_tool.run({
    "action": "add",
    "content": f"项目结构:\n{structure}",
    "memory_type": "semantic",
    "importance": 0.8,
    "metadata": {"type": "project_structure"}
})

(2)与 NoteTool 协同

重要的发现可以记录为结构化笔记

复制代码
# 发现一个性能瓶颈
log_analysis = terminal.run({"command": "grep 'slow query' app.log | tail -n 10"})

# 记录为 blocker 笔记
note_tool.run({
    "action": "create",
    "title": "数据库慢查询问题",
    "content": f"## 问题描述\n发现多个慢查询,影响系统性能\n\n## 日志分析\n```\n{log_analysis}\n```\n\n## 下一步\n1. 分析慢查询SQL\n2. 添加索引\n3. 优化查询逻辑",
    "note_type": "blocker",
    "tags": ["performance", "database"]
})

(3)与 ContextBuilder 协同

TerminalTool 的输出可以作为上下文的一部分

复制代码
# 探索代码库
code_structure = terminal.run({"command": "ls -R src"})
recent_changes = terminal.run({"command": "git log --oneline -10"})

# 转换为 ContextPacket
from hello_agents.context import ContextPacket
from datetime import datetime

packets = [
    ContextPacket(
        content=f"代码库结构:\n{code_structure}",
        timestamp=datetime.now(),
        token_count=len(code_structure) // 4,
        relevance_score=0.7,
        metadata={"type": "code_structure", "source": "terminal"}
    ),
    ContextPacket(
        content=f"最近提交:\n{recent_changes}",
        timestamp=datetime.now(),
        token_count=len(recent_changes) // 4,
        relevance_score=0.8,
        metadata={"type": "git_history", "source": "terminal"}
    )
]

# 在构建上下文时包含这些信息
context = context_builder.build(
    user_query="如何重构用户服务模块?",
    custom_packets=packets
)

长程智能体:代码库维护助手

1.使用 即时文件系统访问 TerminalTool 直接读本地代码文件,不用提前存数据库,现场查代码、查 bug、看结构,收集信息

  1. 结构化笔记(NoteTool)将项目笔记存储在本地 MD 文件,关软件、隔天再开,历史记录永不丢,AI 会自动翻旧笔记, 保持跨会话连贯性

  2. 使用上下文工程 ContextBuilder(GSSC) 收集有用的信息(笔记 + 代码 + 记忆),筛选结构化压缩,只将高质量内容放入prompt中智能上下文管理

  3. LLM输出结果后,触发后处理操作,分析回答,自动记录重要信息:如果发现问题,自动创建 blocker 笔记;如果是任务规划,自动创建 action 笔记,自动化知识管理

  4. 支持人机协作:AI 自动分析,开发者可以手动改笔记、加计划,开发者和 AI 一起管理项目

python 复制代码
def run(self, user_input: str, mode: str = "auto") -> str:
        """运行助手

        Args:
            user_input: 用户输入
            mode: 运行模式
                - "auto": 自动决策是否使用工具
                - "explore": 侧重代码探索
                - "analyze": 侧重问题分析
                - "plan": 侧重任务规划

        Returns:
            str: 助手的回答
        """
        print(f"\n{'='*80}")
        print(f"👤 用户: {user_input}")
        print(f"{'='*80}\n")

        # 第一步:根据模式执行预处理(自动查代码/分析/规划)
        pre_context = self._preprocess_by_mode(user_input, mode)

        # 第二步:检索相关笔记
        relevant_notes = self._retrieve_relevant_notes(user_input)
        note_packets = self._notes_to_packets(relevant_notes)

        # 第三步:构建优化的上下文
        context = self.context_builder.build(
            user_query=user_input,
            conversation_history=self.conversation_history,
            system_instructions=self._build_system_instructions(mode),
            custom_packets=note_packets + pre_context
        )

        # 第四步:调用 LLM
        print("🤖 正在思考...")
        response = self.llm.invoke(context)

        # 第五步:后处理
        self._postprocess_response(user_input, response)

        # 第六步:更新对话历史
        self._update_history(user_input, response)

        print(f"\n🤖 助手: {response}\n")
        print(f"{'='*80}\n")

        return response

智能体通信协议

智能体通信协议基础

为何需要通信协议

之前智能体面临的限制

  • 首先是工具集成的困境:每当需要访问新的外部服务(如 GitHub API、数据库、文件系统),我们都必须编写专门的 Tool 类。这不仅工作量大,而且不同开发者编写的工具无法互相兼容
  • 其次是能力扩展的瓶颈:智能体的能力被限制在预先定义的工具集内,无法动态发现和使用新的服务
  • 最后是协作的缺失:当任务复杂到需要多个专业智能体协作时(如研究员+撰写员+编辑),我们只能通过手动编排来协调它们的工作
    通信协议 提供了一套标准化的接口规范 ,让智能体能够以统一的方式访问各种外部服务,而无需为每个服务编写专门的适配器。这就像互联网的 TCP/IP 协议,它让不同的设备能够相互通信,而不需要为每种设备编写专门的通信代码

通信协议带来的改变是根本性的:标准化接口 让不同服务提供统一的访问方式,互操作性 使得不同开发者的工具可以无缝集成,动态发现 允许智能体在运行时发现新的服务和能力,可扩展性让系统能够轻松添加新的功能模块
传统模式(手写 Tool) :新增外部 API → 写新 Tool 类 → 加到 Agent → 重启生效 → 仅限当前 Agent 使用

MCP 标准化模式:第三方服务 → 包装为 MCP 服务 → 部署运行 → 所有 Agent 连接即用 → 动态扩展、多人共享

三种协议设计理念比较

智能体通信协议 并非单一的解决方案,而是针对不同通信场景设计的一系列标准
MCP:智能体与工具的桥梁

MCP(Model Context Protocol)由 Anthropic 团队提出,其核心设计理念是标准化智能体与外部工具/资源的通信方式 。想象一下,你的智能体需要访问文件系统、数据库、GitHub、Slack 等各种服务。传统做法是为每个服务编写专门的适配器,这不仅工作量大,而且难以维护。MCP 通过定义统一的协议规范,让所有服务都能以相同的方式被访问

MCP 的设计哲学是"上下文共享"。它不仅仅是一个 RPC(远程过程调用)协议 (RPC协议:「调用接口 → 远端执行 → 返回结果」只返回调用结果,不附带上下文信息),更重要的是它允许智能体和工具之间共享丰富的上下文信息 。当智能体访问一个代码仓库时,MCP 服务器不仅能提供文件内容,还能提供代码结构、依赖关系、提交历史等上下文信息,让智能体能够做出更智能的决策

A2A:智能体间的对话

A2A(Agent-to-Agent Protocol)协议由 Google 团队提出,其核心设计理念是实现智能体之间的点对点通信 。与 MCP 关注智能体与工具的通信不同,A2A 关注的是智能体之间如何相互协作。这种设计让智能体能够像人类团队一样进行对话、协商和协作

A2A 的设计哲学是"对等通信"。在 A2A 网络中,每个智能体既是服务提供者,也是服务消费者。智能体可以主动发起请求,也可以响应其他智能体的请求。这种对等的设计避免了中心化协调器的瓶颈,让智能体网络更加灵活和可扩展

ANP:智能体网络的基础设施

ANP(Agent Network Protocol)是一个概念性的协议框架,目前由开源社区维护,还没有成熟的生态,其核心设计理念是构建大规模智能体网络的基础设施 。如果说 MCP 解决的是"如何访问工具"A2A 解决的是"如何与其他智能体对话",那么 ANP 解决的是"如何在大规模网络中发现和连接智能体"

ANP 的设计哲学是"去中心化服务发现"。在一个包含成百上千个智能体的网络中,如何让智能体能够找到它需要的服务?ANP 提供了服务注册、发现和路由机制,让智能体能够动态地发现网络中的其他服务,而不需要预先配置所有的连接关系

HelloAgents 通信协议架构设计

HelloAgents 通信架构 = 封装统一工具入口 + 内部自动协议分发 ,开发者只需要调用框架提供的工具(提供一致的run()方法),工具内部会自动用 MCP / A2A / ANP 完成真正的操作

python 复制代码
from hello_agents.tools import MCPTool, A2ATool, ANPTool

# 1. MCP:访问工具
mcp_tool = MCPTool()
result = mcp_tool.run({
    "action": "call_tool",
    "tool_name": "add",
    "arguments": {"a": 10, "b": 20}
})
print(f"MCP计算结果: {result}")  # 输出: 30.0

# 2. ANP:服务发现
anp_tool = ANPTool()
anp_tool.run({
    "action": "register_service",
    "service_id": "calculator",
    "service_type": "math",
    "endpoint": "http://localhost:8080"
})
services = anp_tool.run({"action": "discover_services"})
print(f"发现的服务: {services}")

# 3. A2A:智能体通信
a2a_tool = A2ATool("http://localhost:5000")
print("A2A工具创建成功")

MCP 协议实战

MCP 协议概念介绍

MCP:智能体的"USB-C"

想象一下,你的智能体可能需要同时做很多事情,例如:

  • 读取本地文件系统的文档
  • 查询 PostgreSQL 数据库
  • 搜索 GitHub 上的代码
  • 发送 Slack 消息
  • 访问 Google Drive

传统方式下,你需要为每个服务编写适配器代码,处理不同的 API、认证方式、错误处理等。这不仅工作量大,而且难以维护。更重要的是,不同 LLM 平台的 function call 实现差异巨大,切换模型时需要重写大量代码

MCP 的出现改变了这一切。它就像 USB-C 统一了各种设备的连接方式一样,MCP 统一了智能体与工具的交互方式。无论你使用 Claude、GPT 还是其他模型,只要它们支持 MCP 协议,就能无缝访问相同的工具和资源

python 复制代码
# ====== 1. 模拟不同LLM的 Function Call 输出(格式完全不同) ===========
# GPT 的工具调用格式(私有格式A)
gpt_tool_response = {
    "function_call": {
        "name": "get_weather",
        "parameters": {"city": "北京"}
    }
}

# Claude 的工具调用格式(私有格式B)
claude_tool_response = {
    "tool_uses": [
        {"name": "get_weather", "input": {"city": "北京"}}
    ]
}

# ====== 2. 你必须写两套适配代码(痛点!) ======================
# 解析 GPT 的工具调用
def parse_gpt_tool(response):
    tool_name = response["function_call"]["name"]
    params = response["function_call"]["parameters"]
    return tool_name, params

# 解析 Claude 的工具调用
def parse_claude_tool(response):
    tool_name = response["tool_uses"][0]["name"]
    params = response["tool_uses"][0]["input"]
    return tool_name, params

# ================ 3. 切换模型时,必须切换解析函数 ==================
# 用GPT
tool, args = parse_gpt_tool(gpt_tool_response)
# 用Claude,必须换函数
tool, args = parse_claude_tool(claude_tool_response)

print(f"调用工具:{tool},参数:{args}")
python 复制代码
# ========= 1. MCP 标准:所有模型输出统一格式(全世界通用) ==============
# GPT 输出 MCP 标准格式
gpt_mcp_response = {
    "mcp_protocol": "v1",
    "tool_call": {
        "name": "get_weather",
        "params": {"city": "北京"}
    }
}

# Claude 输出 MCP 标准格式(和GPT完全一样!)
claude_mcp_response = {
    "mcp_protocol": "v1",
    "tool_call": {
        "name": "get_weather",
        "params": {"city": "北京"}
    }
}

# ================ 2. 只写一套解析代码(永久通用!) ==================
def parse_mcp_tool(response):
    # 一套代码解析所有模型
    tool_name = response["tool_call"]["name"]
    params = response["tool_call"]["params"]
    return tool_name, params

# ==================== 3. 切换模型,代码一行不改! ====================
# 用GPT
tool, args = parse_mcp_tool(gpt_mcp_response)
# 用Claude,代码完全不变
tool, args = parse_mcp_tool(claude_mcp_response)

print(f"调用工具:{tool},参数:{args}")
MCP 架构

假设你正在使用 Claude Desktop 询问:"我桌面上有哪些文档?"

完整的交互流程:用户问题 → Claude Desktop(Host) → Claude 模型分析 → 需要文件信息 → MCP Client 连接 → MCP Client 把 LLM 的意图转成 MCP 标准协议格式 → 文件系统 MCP Server → 执行操作 → 返回结果 → Claude 生成回答 → 显示在 Claude Desktop 上

Host 层:负责 人机交互,管理整个对话流程
MCP Client 层:负责 协议转发、权限、请求格式化
MCP Server 层:负责 复杂资源读取、上下文组装、实际操作

MCP能力

MCP 协议提供了三大核心能力,这三种能力的区别在于:Tools 是主动的 (执行操作),Resources 是被动的 (提供数据),Prompts 是指导性的(提供模板)

MCP工作流程

1.工具发现阶段 :MCP Client 连接到 Server 后,首先调用list_tools()获取所有可用工具的描述信息(包括工具名称、功能说明、参数定义)

2.上下文构建:Client 将工具列表转换为 LLM 能理解的格式,添加到系统提示词中。例如:

复制代码
你可以使用以下工具:
- read_file(path: str): 读取指定路径的文件内容
- search_code(query: str, language: str): 在代码库中搜索

3.模型推理 :LLM 分析用户问题和可用工具,决定是否需要调用工具以及调用哪个工具。这个决策基于工具的描述和当前对话上下文

4.工具执行 :如果 LLM 决定使用工具,Client 通过 MCP Server 执行所选工具,获取结果

5.结果整合:工具执行结果被送回给 LLM,LLM 结合结果生成最终回答

MCP 与 Function Calling 的差异

方式 1:使用 Function Calling

python 复制代码
# 步骤1:为每个LLM提供商定义函数
# OpenAI格式
openai_tools = [
    {
        "type": "function",
        "function": {
            "name": "search_github",
            "description": "搜索GitHub仓库",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"}
                },
                "required": ["query"]
            }
        }
    }
]

# Claude格式
claude_tools = [
    {
        "name": "search_github",
        "description": "搜索GitHub仓库",
        "input_schema": {  # 注意:不是parameters
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "搜索关键词"}
            },
            "required": ["query"]
        }
    }
]

# 步骤2:自己实现工具函数
def search_github(query):
    import requests
    response = requests.get(
        "https://api.github.com/search/repositories",
        params={"q": query}
    )
    return response.json()

# 步骤3:处理不同模型的响应格式
# OpenAI的响应
if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    result = search_github(**json.loads(tool_call.function.arguments))

# Claude的响应
if response.content[0].type == "tool_use":
    tool_use = response.content[0]
    result = search_github(**tool_use.input)

方式 2:使用 MCP

python 复制代码
from hello_agents.protocols import MCPClient

# 步骤1:连接到社区提供的MCP服务器(无需自己实现)
github_client = MCPClient([
    "npx", "-y", "@modelcontextprotocol/server-github"
])

fs_client = MCPClient([
    "npx", "-y", "@modelcontextprotocol/server-filesystem", "."
])

# 步骤2:统一的调用方式(与模型无关)
async with github_client:
    # 自动发现工具
    tools = await github_client.list_tools()

    # 调用工具(标准化接口)
    result = await github_client.call_tool(
        "search_repositories",
        {"query": "AI agents"}
    )

# 步骤3:任何支持MCP的模型都能使用
# OpenAI、Claude、Llama等都使用相同的MCP客户端
复制代码
# MCP 完整实例

第一步:给 LLM 固定「MCP 规则提示词」
发给 Claude 的系统提示:当你需要读取文件、操作本地资源时,不要使用原生工具格式,请固定用下面格式描述你的操作意图:
【MCP_ACTION】
服务:文件服务
操作:读取文件
参数:path = xxx

第二步:用户提问
查看 readme.md

第三步:LLM 输出(纯文本、通用格式,无厂商绑定)
我需要读取项目中的 readme.md 文件
【MCP_ACTION】
服务:文件服务
操作:读取文件
参数:path = readme.md

第四步:MCP Client 解析
MCP Client 内置一段简单解析逻辑:
识别到文本里有 【MCP_ACTION】
提取:服务名、操作名、参数
自动翻译成 标准 MCP 协议请求报文

第五步:MCP Server 执行
文件服务 Server:读取 readme.md
附加项目上下文(文件目录、编码、关联文件等)
按 MCP 标准格式返回结果

第六步:结果回传给 LLM 整理回答

Function CallingMCP 并非竞争关系,而是相辅相成的。Function Calling大语言模型的一项核心能力,它体现了模型内在的智能,使模型能够理解何时需要调用函数,并精准生成相应的调用参数。相对地,MCP 则扮演着基础设施协议的角色,它在工程层面解决了工具与模型如何连接的问题,通过标准化的方式来描述和调用工具

MCP 传输方式详解

MCP 协议的一个重要特性是传输层无关性(Transport Agnostic)。这意味着 MCP 协议本身不依赖于特定的传输方式,可以在不同的通信通道上运行。HelloAgents 基于 FastMCP 2.0,提供了完整的传输方式支持,让你可以根据实际场景选择最合适的传输模式

传输方式概览

HelloAgents 的 MCPClient 支持五种传输方式,每种都有不同的使用场景

Memory :同进程 → 共享内存直接传,无网络、无命令行
Stdio :父子进程 → 命令行管道收发消息(Claude Code、Cursor 本地标配)
HTTP :普通网络 → 一问一答,短请求,远程服务通用
SSE :HTTP 单向长连接 → 服务端主动推送消息
Streamable HTTP :双向流式 HTTP → 长连接、实时双向传输

本地桌面 Agent(Claude/Cursor):优先用 Stdio

云端 / 远程服务:优先用 HTTP / StreamableHTTP

代码开发测试:优先用 Memory

python 复制代码
from hello_agents.tools import MCPTool
from hello_agents.protocols import MCPClient

# 1. Memory Transport - 内存传输(用于测试)
# 不指定任何参数,使用内置演示服务器
mcp_tool = MCPTool()

# 2. Stdio Transport - 标准输入输出传输(本地开发)
# 使用命令列表启动本地服务器
mcp_tool = MCPTool(server_command=["python", "examples/mcp_example_server.py"])

# 3. Stdio Transport with Args - 带参数的命令传输
# 可以传递额外参数
mcp_tool = MCPTool(server_command=["python", "examples/mcp_example_server.py", "--debug"])

# 4. Stdio Transport - 社区服务器(npx方式)
# 使用npx启动社区MCP服务器
mcp_tool = MCPTool(server_command=["npx", "-y", "@modelcontextprotocol/server-filesystem", "."])

# 5. HTTP/SSE/StreamableHTTP Transport
# 注意:MCPTool主要用于Stdio和Memory传输
# 对于HTTP/SSE等远程传输,建议直接使用MCPClient
# 连接到远程 HTTP MCP 服务器
client = MCPClient("http://api.example.com/mcp")

# 连接到 SSE MCP 服务器
client = MCPClient(
   "http://localhost:8080/sse",
    transport_type="sse"
)

# 连接到 StreamableHTTP MCP 服务器
client = MCPClient(
    "http://localhost:8080/mcp",
    transport_type="streamable_http"
)

MCP 服务器

复制代码
# 1.自动化网页测试(Playwright)

# Agent可以自动:
# - 打开浏览器访问网站
# - 填写表单并提交
# - 截图验证结果
# - 生成测试报告
playwright_tool = MCPTool(
    name="playwright",
    server_command=["npx", "-y", "@playwright/mcp"]
)


2.智能笔记助手(Obsidian + Perplexity)

# Agent可以:
# - 搜索最新技术资讯(Perplexity)
# - 整理成结构化笔记
# - 保存到Obsidian知识库
# - 自动建立笔记间的链接


3.项目管理自动化(Jira + GitHub)

# Agent可以:
# - 从GitHub Issue创建Jira任务
# - 同步代码提交到Jira
# - 自动更新Sprint进度
# - 生成项目报告


4.内容创作工作流(YouTube + Notion + Spotify)

# Agent可以:
# - 获取YouTube视频字幕
# - 生成内容摘要
# - 保存到Notion数据库
# - 播放背景音乐(Spotify)

A2A 协议实战

协议设计动机

MCP 协议解决了智能体与工具的交互,而 A2A 协议则解决智能体之间的协作问题。在一个需要多智能体(如研究员、撰写员、编辑)协作的任务中,它们需要通信委托任务协商能力同步状态

传统的中央协调器(星型拓扑)方案存在三个主要问题:

  • 单点故障:协调器失效导致系统整体瘫痪
  • 性能瓶颈:所有通信都经过中心节点,限制了并发
  • 扩展困难 :增加或修改智能体需要改动中心逻辑

A2A 协议采用点对点(P2P)架构(网状拓扑),允许智能体直接通信,从根本上解决了上述问题。它的核心是任务(Task)和工件(Artifact)这两个抽象概念,这是它与 MCP 最大的区别

比如你有一个主 Agent,要做一个 "AI 主题项目",但它自己不会写文案,就用 A2A 和 "文案 Agent" 协作:

1.主 Agent 先看文案 Agent 的「Agent Card」 :知道它能写 AI 文章,擅长 1000 字左右的内容

2.主 Agent 给文案 Agent 发一个Task :"撰写一篇关于 AI 的文章,1000 字,风格通俗易懂"

3.文案 Agent 中途给主 Agent 发Message :"任务已完成 50%,正在润色结尾部分"

4.文案 Agent 写完后,发一条 Message ,里面的 Part 包含:文章文本(文字)+ 配图(图片)+ 参考资料(文件),这个完整的文章就是Artifact

5.主 Agent 拿到 Artifact,再交给别的 Agent 做排版 / 发布
为实现对协作过程的管理,A2A 为任务定义了标准化的生命周期 ,包括创建协商代理执行中完成失败 等状态

该机制使智能体可以进行任务协商进度跟踪异常处理

A2A 请求生命周期是一个序列,详细说明了请求遵循的四个主要步骤:代理发现身份验证发送消息 API发送消息流 API。下图借鉴了官网的流程图,用来展示了操作流程,说明了客户端、A2A 服务器和身份验证服务器之间的交互

使用A2A协议

python 复制代码
from hello_agents.protocols import A2AServer, A2AClient
import threading
import time

# 1. 创建多个Agent服务
researcher = A2AServer(
    name="researcher",
    description="研究员"
)

@researcher.skill("research")
def do_research(text: str) -> str:
    import re
    match = re.search(r'research\s+(.+)', text, re.IGNORECASE)
    topic = match.group(1).strip() if match else text
    return str({"topic": topic, "findings": f"{topic}的研究结果"})

writer = A2AServer(
    name="writer",
    description="撰写员"
)

@writer.skill("write")
def write_article(text: str) -> str:
    import re
    match = re.search(r'write\s+(.+)', text, re.IGNORECASE)
    content = match.group(1).strip() if match else text
    
    # 尝试解析研究数据
    try:
        data = eval(content)
        topic = data.get("topic", "未知主题")
        findings = data.get("findings", "无研究结果")
    except:
        topic = "未知主题"
        findings = content
    
    return f"# {topic}\n\n基于研究:{findings}\n\n文章内容..."

editor = A2AServer(
    name="editor",
    description="编辑"
)

@editor.skill("edit")
def edit_article(text: str) -> str:
    import re
    match = re.search(r'edit\s+(.+)', text, re.IGNORECASE)
    article = match.group(1).strip() if match else text
    
    result = {
        "article": article + "\n\n[已编辑优化]",
        "feedback": "文章质量良好",
        "approved": True
    }
    return str(result)

# 2. 启动所有服务
threading.Thread(target=lambda: researcher.run(port=5000), daemon=True).start()
threading.Thread(target=lambda: writer.run(port=5001), daemon=True).start()
threading.Thread(target=lambda: editor.run(port=5002), daemon=True).start()
time.sleep(2)  # 等待服务启动

# 3. 创建客户端连接到各个Agent
researcher_client = A2AClient("http://localhost:5000")
writer_client = A2AClient("http://localhost:5001")
editor_client = A2AClient("http://localhost:5002")

# 4. 协作流程
def create_content(topic):
    # 步骤1:研究
    research = researcher_client.execute_skill("research", f"research {topic}")
    research_data = research.get('result', '')
    
    # 步骤2:撰写
    article = writer_client.execute_skill("write", f"write {research_data}")
    article_content = article.get('result', '')
    
    # 步骤3:编辑
    final = editor_client.execute_skill("edit", f"edit {article_content}")
    return final.get('result', '')

# 使用
result = create_content("AI在医疗领域的应用")
print(f"\n最终结果:\n{result}")

在智能体中使用 A2A 工具

提前造好两个专业子 Agent(技术、销售),并启动成独立服务 -> 主 Agent(接待员)把这两个子 Agent当做工具注册 -> 用户提问题 → 主 Agent 的 LLM 分析问题类型 -> LLM 自动选择对应的子 Agent 工具调用 -> 子 Agent 处理,返回结果 → 主 Agent 整理后回答用户

python 复制代码
from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.tools import A2ATool
from hello_agents.protocols import A2AServer
import threading
import time
from dotenv import load_dotenv

load_dotenv()
llm = HelloAgentsLLM()

# 1. 创建技术专家Agent服务
tech_expert = A2AServer(
    name="tech_expert",
    description="技术专家,回答技术问题"
)

@tech_expert.skill("answer")
def answer_tech_question(text: str) -> str:
    import re
    match = re.search(r'answer\s+(.+)', text, re.IGNORECASE)
    question = match.group(1).strip() if match else text
    # 实际应用中,这里会调用LLM或知识库
    return f"技术回答:关于'{question}',我建议您查看我们的技术文档..."

# 2. 创建销售顾问Agent服务
sales_advisor = A2AServer(
    name="sales_advisor",
    description="销售顾问,回答销售问题"
)

@sales_advisor.skill("answer")
def answer_sales_question(text: str) -> str:
    import re
    match = re.search(r'answer\s+(.+)', text, re.IGNORECASE)
    question = match.group(1).strip() if match else text
    return f"销售回答:关于'{question}',我们有特别优惠..."

# 3. 启动服务
threading.Thread(target=lambda: tech_expert.run(port=6000), daemon=True).start()
threading.Thread(target=lambda: sales_advisor.run(port=6001), daemon=True).start()
time.sleep(2)

# 4. 创建接待员Agent(使用HelloAgents的SimpleAgent)
receptionist = SimpleAgent(
    name="接待员",
    llm=llm,
    system_prompt="""你是客服接待员,负责:
1. 分析客户问题类型(技术问题 or 销售问题)
2. 将问题转发给相应的专家
3. 整理专家的回答并返回给客户

请保持礼貌和专业。"""
)

# 添加技术专家工具
tech_tool = A2ATool(
    agent_url="http://localhost:6000",
    name="tech_expert",
    description="技术专家,回答技术相关问题"
)
receptionist.add_tool(tech_tool)

# 添加销售顾问工具
sales_tool = A2ATool(
    agent_url="http://localhost:6001",
    name="sales_advisor",
    description="销售顾问,回答价格、购买相关问题"
)
receptionist.add_tool(sales_tool)

# 5. 处理客户咨询
def handle_customer_query(query):
    print(f"\n客户咨询:{query}")
    print("=" * 50)
    response = receptionist.run(query)
    print(f"\n客服回复:{response}")
    print("=" * 50)

# 测试不同类型的问题
if __name__ == "__main__":
    handle_customer_query("你们的API如何调用?")
    handle_customer_query("企业版的价格是多少?")
    handle_customer_query("如何集成到我的Python项目中?")

A2A 协议还支持 Agent 间的协商机制

python 复制代码
from hello_agents.protocols import A2AServer, A2AClient
import threading
import time

# 创建两个需要协商的Agent
agent1 = A2AServer(
    name="agent1",
    description="Agent 1"
)

@agent1.skill("propose")
def handle_proposal(text: str) -> str:
    """处理协商提案"""
    import re
    
    # 解析提案
    match = re.search(r'propose\s+(.+)', text, re.IGNORECASE)
    proposal_str = match.group(1).strip() if match else text
    
    try:
        proposal = eval(proposal_str)
        task = proposal.get("task")
        deadline = proposal.get("deadline")
        
        # 评估提案
        if deadline >= 7:  # 至少需要7天
            result = {"accepted": True, "message": "接受提案"}
        else:
            result = {
                "accepted": False,
                "message": "时间太紧",
                "counter_proposal": {"deadline": 7}
            }
        return str(result)
    except:
        return str({"accepted": False, "message": "无效的提案格式"})

agent2 = A2AServer(
    name="agent2",
    description="Agent 2"
)

@agent2.skill("negotiate")
def negotiate_task(text: str) -> str:
    """发起协商"""
    import re
    
    # 解析任务和截止日期
    match = re.search(r'negotiate\s+task:(.+?)\s+deadline:(\d+)', text, re.IGNORECASE)
    if match:
        task = match.group(1).strip()
        deadline = int(match.group(2))
        
        # 向agent1发送提案
        proposal = {"task": task, "deadline": deadline}
        return str({"status": "negotiating", "proposal": proposal})
    else:
        return str({"status": "error", "message": "无效的协商请求"})

# 启动服务
threading.Thread(target=lambda: agent1.run(port=7000), daemon=True).start()
threading.Thread(target=lambda: agent2.run(port=7001), daemon=True).start()

基于 A2A 协议实现的「销售 Agent 与供应链 Agent,围绕定制礼品订单的多轮平等协商」

一、Agent角色

销售 Agent(发起方):对接客户需求,发起协商,绑定客户预算、工期的不可突破底线

供应链 Agent(响应方):评估订单可行性,绑定成本、产能的不可突破底线

两者通过 A2A 协议完成标准化通信,无需额外适配接口

二、协商全流程

  • 需求拆解:销售 Agent 拿到客户需求,按规则生成初始订单提案
  • 提案发送:通过 A2A 协议,把提案发给供应链 Agent 做评估
    多轮协商:供应链按规则返回「接受 / 拒绝 / 反提案」;销售再按规则判断是接受、继> - 续谈、还是终止
  • 结果闭环:要么达成一致输出最终订单,要么触发终止条件(超协商轮次 / 突破底线)结束协商

三、协商的核心实现

真正的协商主体是「预设规则 + LLM + A2A 协议 + 流程管控」组成的 Agent 决策系统

  • 预设规则:定死协商红线(成本、预算、工期底线),绝对不能突破
  • LLM:仅在规则内,负责解析需求、生成提案、评估反提案,做弹性调整
  • A2A 协议:仅负责两个 Agent 之间的标准化消息传递,相当于 "专用电话线"
  • 流程管控 :限制协商轮次,避免无限循环,明确终止条件

四、核心本质

和之前「主 Agent 直接给子 Agent 分配任务」的模式完全不同:这是平等的双向协商,不是上下级命令,双方都有决策权,都可以接受、拒绝、提出反要求,直到达成共识

python 复制代码
from hello_agents.protocols import A2AClient

# 销售Agent:协商的发起方
sales_agent = A2AServer(
    name="sales_agent",
    description="销售Agent,对接客户定制需求,和供应链协商订单,给客户最终答复"
)

# 销售硬规则(不可突破)
SALES_RULES = """
1. 客户预算上限120元/个,超过120元直接终止协商
2. 客户最晚交货期7天,超过7天直接终止协商
3. 最多和供应链协商5轮,超过5轮未达成一致,终止协商
4. 优先满足客户的交货期要求,再谈价格
"""

# 初始化A2AClient,连接供应链Agent
supply_chain_client = A2AClient(agent_url="http://localhost:8001")

@sales_agent.skill("init_negotiation")
def start_negotiation(customer_demand: str) -> str:
    """对接客户需求,发起和供应链的协商,返回最终结果"""
    # 1. 解析客户需求,生成初始提案
    parse_prompt = f"""
    你是销售专员,根据客户需求,生成给供应链的订单提案,严格遵守规则:
    {SALES_RULES}
    客户需求:{customer_demand}
    输出JSON格式的提案,包含:quantity(订单量)、target_price(目标单价)、target_deadline(目标交货期/天)、custom_requirement(定制要求)
    """
    initial_proposal = llm.call(system_prompt=parse_prompt, user_input=customer_demand)
    
    # 2. 初始化协商状态
    max_rounds = 5
    current_round = 1
    negotiation_result = None
    current_proposal = initial_proposal

    # 3. 多轮协商循环(A2A协议负责通信)
    while current_round <= max_rounds:
        print(f"===== 第{current_round}轮协商 =====")
        print(f"当前提案:{current_proposal}")

        # 核心:通过A2AClient,给供应链Agent发提案(A2A协议负责消息传递)
        supply_response = supply_chain_client.call_skill(
            skill_name="evaluate_proposal",
            params={"proposal_text": current_proposal}
        )
        print(f"供应链响应:{supply_response}")

        # 4. 解析供应链的响应,做决策
        response_data = eval(supply_response)
        if response_data.get("accepted"):
            # 协商成功
            negotiation_result = f"协商成功!最终订单:{current_proposal}"
            break
        elif "counter_proposal" in response_data:
            # 收到反提案,评估是否符合销售规则
            counter_proposal = response_data["counter_proposal"]
            evaluate_prompt = f"""
            你是销售专员,评估供应链的反提案是否符合客户规则:
            {SALES_RULES}
            反提案:{counter_proposal}
            客户需求:{customer_demand}
            输出结果:
            - accept:符合规则,接受反提案
            - continue:不符合,生成新的提案继续协商
            - terminate:突破底线,终止协商
            同时输出你的理由
            """
            evaluate_result = llm.call(system_prompt=evaluate_prompt, user_input=counter_proposal)
            
            if "accept" in evaluate_result:
                negotiation_result = f"协商成功!最终订单:{counter_proposal}"
                break
            elif "terminate" in evaluate_result:
                negotiation_result = f"协商终止:{evaluate_result}"
                break
            else:
                # 生成新的提案,继续下一轮
                current_proposal = llm.call(
                    system_prompt=f"根据供应链反提案,生成新的协商提案,遵守规则:{SALES_RULES}",
                    user_input=f"客户需求:{customer_demand},供应链反提案:{counter_proposal}"
                )
                current_round += 1
        else:
            # 被直接拒绝,终止协商
            negotiation_result = f"协商失败:{response_data.get('message')}"
            break

    # 5. 协商超时,终止
    if not negotiation_result:
        negotiation_result = "协商终止:超过最大协商轮次,未达成一致"
    
    return negotiation_result

# 启动销售Agent服务,端口8000
threading.Thread(target=lambda: sales_agent.run(port=8000), daemon=True).start()
time.sleep(1)

ANP 协议实战

MCP 协议解决了工具调用A2A 协议解决点对点智能体协作之后,ANP 协议则专注于解决大规模、开放网络环境下的智能体管理问题

协议目标

当一个网络中存在大量功能各异的智能体(例如,自然语言处理、图像识别、数据分析等)时,系统会面临一系列挑战:

  • 服务发现:当新任务到达时,如何快速找到能够处理该任务的智能体?
  • 智能路由:如果多个智能体都能处理同一任务,如何选择最合适的一个(如根据负载、成本等)并向其分派任务?
  • 动态扩展 :如何让新加入网络的智能体被其他成员发现和调用?

ANP 的设计目标就是提供一套标准化的机制,来解决上述的服务发现路由选择网络扩展性问题


在这个流程图里,主要包括以下几个步骤:

  1. 服务的发现与匹配 :首先,智能体 A 通过一个公开的发现服务(DNS),基于语义或功能描述进行查询,以定位到符合其任务需求的智能体 B。该发现服务通过预先爬取各智能体对外暴露的标准端点(.well-known/agent-descriptions)来建立索引,从而实现服务需求方与提供方的动态匹配
  2. 基于 DID 的身份验证 :在交互开始时,智能体 A 使用其私钥对包含自身 DID 的请求进行签名(DID:Decentralized Identifier,去中心化标识符,就是给每个AI Agent发的唯一标识)。智能体 B 收到后,通过解析该 DID 获取对应的公钥(在DID服务器中,通过智能体A 的DID,获取其公钥),并以此验证签名的真实性与请求的完整性,从而建立起双方的可信通信
  3. 标准化的服务执行 :身份验证通过后,智能体 B 响应请求,双方依据预定义的标准接口和数据格式进行数据交换或服务调用(如预订、查询等)。标准化的交互流程是实现跨平台、跨系统互操作性的基础

总而言之,该机制的核心是利用 DID 构建了一个去中心化的信任根基,并借助标准化的描述协议实现了服务的动态发现。这套方法使得智能体能够在无需中央协调的前提下,安全、高效地在互联网上形成协作网络

python 复制代码
# 1.创建服务发现中心(查询、注册agent服务)
from hello_agents.protocols import ANPDiscovery, register_service

# 创建服务发现中心
discovery = ANPDiscovery()

# 注册Agent服务
register_service(
    discovery=discovery,
    service_id="nlp_agent_1",
    service_name="NLP处理专家A",
    service_type="nlp",
    capabilities=["text_analysis", "sentiment_analysis", "ner"],
    endpoint="http://localhost:8001",
    metadata={"load": 0.3, "price": 0.01, "version": "1.0.0"}
)

register_service(
    discovery=discovery,
    service_id="nlp_agent_2",
    service_name="NLP处理专家B",
    service_type="nlp",
    capabilities=["text_analysis", "translation"],
    endpoint="http://localhost:8002",
    metadata={"load": 0.7, "price": 0.02, "version": "1.1.0"}
)

print("✅ 服务注册完成")


# 2.发现服务
from hello_agents.protocols import discover_service

# 按类型查找
nlp_services = discover_service(discovery, service_type="nlp")
print(f"找到 {len(nlp_services)} 个NLP服务")

# 选择负载最低的服务
best_service = min(nlp_services, key=lambda s: s.metadata.get("load", 1.0))
print(f"最佳服务:{best_service.service_name} (负载: {best_service.metadata['load']})")


# 3.构建 Agent 网络(管理节点间的连接和通信)
from hello_agents.protocols import ANPNetwork

# 创建网络
network = ANPNetwork(network_id="ai_cluster")

# 添加节点
for service in discovery.list_all_services():
    network.add_node(service.service_id, service.endpoint)

# 建立连接(根据能力匹配)
network.connect_nodes("nlp_agent_1", "nlp_agent_2")

stats = network.get_network_stats()
print(f"✅ 网络构建完成,共 {stats['total_nodes']} 个节点")
python 复制代码
# 完整的分布式任务调度系统

from hello_agents.protocols import ANPDiscovery, register_service
from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.tools.builtin import ANPTool
import random
from dotenv import load_dotenv

load_dotenv()
llm = HelloAgentsLLM()

# 1. 创建服务发现中心
discovery = ANPDiscovery()

# 2. 注册多个计算节点
for i in range(10):
    register_service(
        discovery=discovery,
        service_id=f"compute_node_{i}",
        service_name=f"计算节点{i}",
        service_type="compute",
        capabilities=["data_processing", "ml_training"],
        endpoint=f"http://node{i}:8000",
        metadata={
            "load": random.uniform(0.1, 0.9),
            "cpu_cores": random.choice([4, 8, 16]),
            "memory_gb": random.choice([16, 32, 64]),
            "gpu": random.choice([True, False])
        }
    )

print(f"✅ 注册了 {len(discovery.list_all_services())} 个计算节点")

# 3. 创建任务调度Agent
scheduler = SimpleAgent(
    name="任务调度器",
    llm=llm,
    system_prompt="""你是一个智能任务调度器,负责:
1. 分析任务需求
2. 选择最合适的计算节点
3. 分配任务

选择节点时考虑:负载、CPU核心数、内存、GPU等因素。"""
)

# 添加ANP工具
anp_tool = ANPTool(
    name="service_discovery",
    description="服务发现工具,可以查找和选择计算节点",
    discovery=discovery
)
scheduler.add_tool(anp_tool)

# 4. 智能任务分配
def assign_task(task_description):
    print(f"\n任务:{task_description}")
    print("=" * 50)

    # 让Agent智能选择节点
    response = scheduler.run(f"""
    请为以下任务选择最合适的计算节点:
    {task_description}

    要求:
    1. 列出所有可用节点
    2. 分析每个节点的特点
    3. 选择最合适的节点
    4. 说明选择理由
    """)

    print(response)
    print("=" * 50)

# 测试不同类型的任务
assign_task("训练一个大型深度学习模型,需要GPU支持")
assign_task("处理大量文本数据,需要高内存")
assign_task("运行轻量级数据分析任务")

创建你的第一个 MCP 服务器

为什么要构建自定义 MCP 服务器

虽然可以直接使用公开的 MCP 服务,但在许多实际应用场景中,需要构建自定义的 MCP 服务器以满足特定需求

主要动机包括以下几点:

  • 封装业务逻辑:将企业内部特有的业务流程或复杂操作封装为标准化的 MCP 工具,供智能体统一调用
  • 访问私有数据:创建一个安全可控的接口或代理,用于访问内部数据库、API 或其他无法对公网暴露的私有数据源
  • 性能专项优化:针对高频调用或对响应延迟有严苛要求的应用场景,进行深度优化
  • 功能定制扩展:实现标准 MCP 服务未提供的特定功能,例如集成专有算法模型或连接特定的硬件设备

MCP 示例

python 复制代码
#!/usr/bin/env python3
"""天气查询 MCP 服务器"""

import json
import requests
import os
from datetime import datetime
from typing import Dict, Any
from hello_agents.protocols import MCPServer

# 创建 MCP 服务器
weather_server = MCPServer(name="weather-server", description="真实天气查询服务")

CITY_MAP = {
    "北京": "Beijing", "上海": "Shanghai", "广州": "Guangzhou",
    "深圳": "Shenzhen", "杭州": "Hangzhou", "成都": "Chengdu",
    "重庆": "Chongqing", "武汉": "Wuhan", "西安": "Xi'an",
    "南京": "Nanjing", "天津": "Tianjin", "苏州": "Suzhou"
}


def get_weather_data(city: str) -> Dict[str, Any]:
    """从 wttr.in 获取天气数据"""
    city_en = CITY_MAP.get(city, city)
    url = f"https://wttr.in/{city_en}?format=j1"
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    data = response.json()
    current = data["current_condition"][0]

    return {
        "city": city,
        "temperature": float(current["temp_C"]),
        "feels_like": float(current["FeelsLikeC"]),
        "humidity": int(current["humidity"]),
        "condition": current["weatherDesc"][0]["value"],
        "wind_speed": round(float(current["windspeedKmph"]) / 3.6, 1),
        "visibility": float(current["visibility"]),
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    }


# 定义工具函数
def get_weather(city: str) -> str:
    """获取指定城市的当前天气"""
    try:
        weather_data = get_weather_data(city)
        return json.dumps(weather_data, ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({"error": str(e), "city": city}, ensure_ascii=False)


def list_supported_cities() -> str:
    """列出所有支持的中文城市"""
    result = {"cities": list(CITY_MAP.keys()), "count": len(CITY_MAP)}
    return json.dumps(result, ensure_ascii=False, indent=2)


def get_server_info() -> str:
    """获取服务器信息"""
    info = {
        "name": "Weather MCP Server",
        "version": "1.0.0",
        "tools": ["get_weather", "list_supported_cities", "get_server_info"]
    }
    return json.dumps(info, ensure_ascii=False, indent=2)


# 注册工具到服务器
weather_server.add_tool(get_weather)
weather_server.add_tool(list_supported_cities)
weather_server.add_tool(get_server_info)


if __name__ == "__main__":
    weather_server.run()
python 复制代码
# 测试自定义MCP服务器

#!/usr/bin/env python3
"""测试天气查询 MCP 服务器"""

import asyncio
import json
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'HelloAgents'))
# MCP Client 帮你自动启动了本地的MCP Server,再建立连接调用方法
from hello_agents.protocols.mcp.client import MCPClient


async def test_weather_server():
    server_script = os.path.join(os.path.dirname(__file__), "14_weather_mcp_server.py")
    client = MCPClient(["python", server_script])

    try:
        async with client:
            # 测试1: 获取服务器信息
            info = json.loads(await client.call_tool("get_server_info", {}))
            print(f"服务器: {info['name']} v{info['version']}")

            # 测试2: 列出支持的城市
            cities = json.loads(await client.call_tool("list_supported_cities", {}))
            print(f"支持城市: {cities['count']} 个")

            # 测试3: 查询北京天气
            weather = json.loads(await client.call_tool("get_weather", {"city": "北京"}))
            if "error" not in weather:
                print(f"\n北京天气: {weather['temperature']}°C, {weather['condition']}")

            # 测试4: 查询深圳天气
            weather = json.loads(await client.call_tool("get_weather", {"city": "深圳"}))
            if "error" not in weather:
                print(f"深圳天气: {weather['temperature']}°C, {weather['condition']}")

            print("\n✅ 所有测试完成!")

    except Exception as e:
        print(f"❌ 测试失败: {e}")


if __name__ == "__main__":
    asyncio.run(test_weather_server())
python 复制代码
"""在 Agent 中使用天气 MCP 服务器"""

import os
from dotenv import load_dotenv
from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.tools import MCPTool

load_dotenv()


def create_weather_assistant():
    """创建天气助手"""
    llm = HelloAgentsLLM()

    assistant = SimpleAgent(
        name="天气助手",
        llm=llm,
        system_prompt="""你是天气助手,可以查询城市天气。
使用 get_weather 工具查询天气,支持中文城市名。
"""
    )

    # 添加天气 MCP 工具
    server_script = os.path.join(os.path.dirname(__file__), "14_weather_mcp_server.py")
    weather_tool = MCPTool(server_command=["python", server_script])
    assistant.add_tool(weather_tool)

    return assistant


def demo():
    """演示"""
    assistant = create_weather_assistant()

    print("\n查询北京天气:")
    response = assistant.run("北京今天天气怎么样?")
    print(f"回答: {response}\n")


def interactive():
    """交互模式"""
    assistant = create_weather_assistant()

    while True:
        user_input = input("\n你: ").strip()
        if user_input.lower() in ['quit', 'exit']:
            break
        response = assistant.run(user_input)
        print(f"助手: {response}")


if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == "demo":
        demo()
    else:
        interactive()

自动化深度研究智能体

项目概述与架构设计

为什么需要深度研究助手

在信息爆炸的时代,我们每天都需要快速了解新的技术、概念或事件。传统的研究方式有几个痛点。首先是信息过载 。搜索引擎返回成千上万的结果,你需要逐个点开链接,阅读大量内容,才能找到有用的信息。其次是缺少结构 。即使找到了相关信息,这些信息往往是碎片化的,缺少系统性的组织。最后是重复劳动。每次研究新主题时,都需要重复"搜索→阅读→总结→整理"的过程
这就是深度研究助手需要解决的问题。它不仅仅是一个搜索工具,而是一个能够自主规划、执行和总结的研究助手

深度研究助手的核心价值:

  • 节省时间:将 1-2 小时的研究工作压缩到 5-10 分钟
  • 提高质量:系统化的研究流程,避免遗漏重要信息
  • 可追溯:记录所有搜索结果和来源,方便验证和引用
  • 可扩展:可以轻松添加新的搜索引擎、数据源和分析工具

技术架构概览

系统分为四层架构设计:

前端层 (Vue3+TypeScript) :全屏模态对话框 UI、Markdown 结果可视化

后端层 (FastAPI) :API 路由(/research/stream)

智能体层 (HelloAgents) :三个专门 Agent(TODO Planner、Task Summarizer、Report Writer)+ 两个核心工具(SearchTool、NoteTool)

外部服务层:搜索引擎+ LLM 提供商

用户输入 :用户在前端输入研究主题
前端发送 :前端通过 SSE 连接到/research/stream
后端接收 :FastAPI 接收请求,创建研究状态
规划阶段 :调用研究规划 Agent,分解为 3 个子任务
执行阶段:逐个执行每个子任务

  • 使用 SearchTool 搜索
  • 调用任务总结 Agent 总结
  • 使用 NoteTool 记录结果

报告阶段 :调用报告生成 Agent,整合所有总结
流式返回 :通过 SSE 推送进度和结果到前端
前端展示:前端实时更新任务状态、进度条、日志、报告

复制代码
# 项目目录结构

helloagents-deepresearch/
├── backend/                    # 后端代码
│   ├── src/
│   │   ├── agent.py           # 核心协调器
│   │   ├── main.py            # FastAPI入口
│   │   ├── models.py          # 数据模型
│   │   ├── prompts.py         # Prompt模板
│   │   ├── config.py          # 配置管理
│   │   └── services/          # 服务层
│   │       ├── planner.py     # 规划服务
│   │       ├── summarizer.py  # 总结服务
│   │       ├── reporter.py    # 报告服务
│   │       └── search.py      # 搜索服务
│   ├── .env                   # 环境变量
│   ├── pyproject.toml         # 依赖管理
│   └── workspace/             # 研究笔记
│
└── frontend/                   # 前端代码
    ├── src/
    │   ├── App.vue            # 主组件
    │   ├── components/        # UI组件
    │   │   └── ResearchModal.vue
    │   └── composables/       # 组合式函数
    │       └── useResearch.ts
    ├── package.json           # npm依赖
    └── vite.config.ts         # 构建配置

TODO 驱动的研究范式

什么是 TODO 驱动的研究

传统的搜索引擎只能回答单个问题,而深度研究需要回答一系列相关的问题。TODO 驱动的研究范式将复杂的研究主题分解为多个子任务(TODO),逐个执行并整合结果

这种范式的核心思想是:将"研究"这个复杂任务转化为"规划→执行→整合"的流程
让我们通过一个例子来理解这个转变。假设你想研究"Datawhale 是一个什么样的组织?",传统的搜索方式如下,这种方式的问题在于每个链接只涵盖主题的一个方面、缺少系统性结构,需要手动整理和总结

复制代码
用户输入:Datawhale是一个什么样的组织?
搜索引擎:返回10-20个链接
用户:逐个点开链接,阅读内容,记录笔记
结果:碎片化的信息,缺少系统性

TODO 驱动方式:系统化研究

复制代码
用户输入:Datawhale是一个什么样的组织?

系统规划:
  ├─ TODO 1:Datawhale的基本信息(组织定位)
  ├─ TODO 2:Datawhale的主要项目(核心内容)
  ├─ TODO 3:Datawhale的社区文化(价值观)
  └─ TODO 4:Datawhale的影响力(社会贡献)

系统执行:
  对每个TODO:
    1. 搜索相关资料
    2. 总结关键信息
    3. 记录来源引用

系统整合:
  生成结构化报告:
    ├─ 第一部分:组织定位(来自TODO 1)
    ├─ 第二部分:核心内容(来自TODO 2)
    ├─ 第三部分:价值观(来自TODO 3)
    ├─ 第四部分:社会贡献(来自TODO 4)
    └─ 参考文献:所有来源引用

这种方式的优势在于将复杂主题分解为清晰的子问题,每个子任务的搜索结果和总结都被记录下来,方便追溯。同时,系统化的研究流程避免了遗漏重要信息,可以轻松添加新的子任务或调整执行顺序

一个完整的 TODO 驱动研究系统包含三个核心要素:

(1)智能规划器(TODO Planner) :负责将研究主题分解为子任务。一个好的规划器需要理解主题的关键方面和研究目标,将主题分解为 3-5 个子任务(太少覆盖不全,太多会冗余),并为每个子任务设计合适的搜索查询

(2)任务执行器(Task Executor) :负责执行每个子任务。执行器需要使用搜索引擎获取相关资料,提取关键信息并去除冗余内容,同时保存所有来源引用以方便验证

(3)报告生成器(Report Writer):负责整合所有子任务的结果。生成器需要按照逻辑顺序组织内容,合并重复的信息,并为每个观点添加来源引用

三阶段研究流程

三阶段研究流程

TODO 驱动的研究流程分为三个阶段:规划(Planning)、执行(Execution)、报告(Reporting)。每个阶段都有专门的 Agent 负责
(1)阶段 1:规划

规划阶段的目标是将研究主题分解为 3-5 个子任务。系统接收研究主题和当前日期作为输入,输出 JSON 格式的子任务列表。每个子任务包含三个字段:title(任务标题)、intent(研究意图)和 query(搜索查询)

研究规划 Agent 会根据主题特点采用不同的分解策略,通常从基础概念入手,然后了解技术现状、实际应用和发展趋势,必要时还会进行对比分析。例如,对于"Datawhale 是一个什么样的组织?",规划 Agent 可能生成以下子任务:

复制代码
[
  {
    "title": "Datawhale的基本信息",
    "intent": "了解Datawhale的组织定位、成立时间、发展历程",
    "query": "Datawhale organization introduction history 2024"
  },
  {
    "title": "Datawhale的主要项目",
    "intent": "了解Datawhale的核心开源项目和教程",
    "query": "Datawhale projects tutorials open source 2024"
  },
......
]

(2)阶段 2:执行

执行阶段逐个执行每个子任务,搜索并总结相关资料。系统接收子任务列表和搜索引擎配置作为输入,输出每个子任务的总结(Markdown 格式)和来源引用列表。执行流程如下:

对于每个子任务,执行器会:
1.搜索资料:使用配置的搜索引擎执行搜索

复制代码
search_results = search_tool.run({
    "input": task.query,
    "backend": "tavily",
    "mode": "structured",
    "max_results": 5
})

2.获取搜索结果:提取标题、URL、摘要

复制代码
{
  "results": [
    {
      "title": "What is a Multimodal Model?",
      "url": "https://example.com/multimodal-model",
      "snippet": "A multimodal model is an AI model that can process multiple types of data..."
    },
    ...
  ]
}

3.调用总结 Agent:总结搜索结果

复制代码
summary = summarizer_agent.run(
    task=task,
    search_results=search_results
)

4.记录总结和来源:保存到 NoteTool

复制代码
note_tool.run({
    "action": "create",
    "title": task.title,
    "content": f"## {task.title}\n\n{summary}\n\n## 来源\n{sources}",
    "tags": ["research", "summary"]
})

任务总结 Agent 会从每个搜索结果中提取核心观点,合并相似信息,保留重要的数字、日期、名称等关键数据,并为每个观点添加来源引用。例如,对于"Datawhale 的基本信息"的搜索结果,总结 Agent 可能生成:

复制代码
## Datawhale的基本信息

Datawhale是一个专注于数据科学与AI领域的开源组织,成立于2018年[1]。组织的核心使命是"for the learner,和学习者一起成长",致力于构建一个纯粹的学习社区[2]。

**核心定位:**

1. **开源教育平台**:提供高质量的AI和数据科学学习资源[1]
2. **学习者社区**:汇聚了数万名AI学习者和实践者[3]
3. **知识共享**:倡导开源精神,所有内容完全免费开放[2]

**发展历程:**

- **2018年**:Datawhale成立,发布首个开源教程[1]
- **2020年**:成为国内领先的AI学习社区之一[3]
- **2024年**:累计发布50+开源项目,影响10万+学习者[4]

## 来源

[1] https://github.com/datawhalechina
[2] https://datawhale.club/about
[3] https://www.zhihu.com/org/datawhale
[4] https://datawhale.cn

# 在执行过程中,系统会实时推送进度信息到前端:

{
  "type": "status",
  "message": "正在搜索:Datawhale的基本信息"
}

{
  "type": "status",
  "message": "正在总结搜索结果..."
}

{
  "type": "task",
  "task": {
    "id": 1,
    "title": "Datawhale的基本信息",
    "status": "completed"
  }
}

(3)阶段 3:报告

报告阶段的目标是整合所有子任务的总结,生成最终报告。系统接收所有子任务的总结和研究主题作为输入,输出 Markdown 格式的最终报告。报告包含标题、概述、各个子任务的详细分析、总结和参考文献五个部分。例如,对于"Datawhale 是一个什么样的组织?",最终报告可能是:

复制代码
# Datawhale是一个什么样的组织?

## 概述

本报告系统地研究了Datawhale这个开源组织,涵盖基本信息、主要项目、社区文化和影响力四个方面。

## 1. Datawhale的基本信息

Datawhale是一个专注于数据科学与AI领域的开源组织,成立于2018年...

(此处插入子任务1的总结)

## 2. Datawhale的主要项目

Datawhale发布了多个高质量的开源教程,包括Hello-Agents、Joyful-Pandas等...

(此处插入子任务2的总结)
......
## 总结

通过本次研究,我们了解了Datawhale的组织定位、核心项目、社区文化和社会贡献。Datawhale是一个纯粹的学习社区,为AI教育做出了重要贡献。

## 参考文献

[1] https://github.com/datawhalechina
[2] https://datawhale.club/about
...

报告生成 Agent 会按照子任务的逻辑顺序组织内容,在开头添加简要概述,合并重复的信息,统一 Markdown 格式,并将所有来源引用整理到参考文献部分

智能体系统设计

在深度研究助手中,我们设计了三个专门的 Agent,每个 Agent 负责一个特定的任务。这使得每个 Agent 都很简单,易于理解和维护

Agent 1:研究规划专家(TODO Planner)

职责:将研究主题分解为 3-5 个子任务

设计理念:研究规划专家的核心任务是理解用户的研究主题,分析主题的关键方面,然后生成一系列子任务。这个过程类似于人类研究者在开始研究前的"头脑风暴"阶段

复制代码
# Prompt 设计(关键设计点:提示词包含当前日期以获取最新信息,明确要求 JSON 格式输出便于解析,通过示例帮助 Agent 理解期望输出,并强调子任务数量、逻辑关系等约束)

todo_planner_instructions = """
你是一个研究规划专家。你的任务是将用户的研究主题分解为3-5个子任务。

当前日期:{current_date}

研究主题:{research_topic}

请分析这个研究主题,将其分解为3-5个子任务。每个子任务应该:
1. 涵盖主题的一个重要方面
2. 有明确的研究目标
3. 可以通过搜索引擎找到相关资料

请以JSON格式返回子任务列表,每个子任务包含:
- title:任务标题(简洁明了)
- intent:任务意图(为什么要研究这个)
- query:搜索查询(用于搜索引擎的查询字符串,可以使用英文以获得更好的搜索结果)

示例输出:
[
  {{
    "title": "什么是多模态模型",
    "intent": "了解多模态模型的基础概念,为后续研究打下基础",
    "query": "multimodal model definition concept 2024"
  }},
  ...
]

请确保:
1. 子任务数量在3-5个之间
2. 子任务之间有逻辑关系(如从基础到应用,从现状到趋势)
3. 搜索查询能够准确找到相关资料
4. 只返回JSON,不要包含其他文本
"""
python 复制代码
# 代码实现
class PlanningService:
    def __init__(self, llm: HelloAgentsLLM):
        self._agent = ToolAwareSimpleAgent(
            name="TODO Planner",
            system_prompt="你是一个研究规划专家",
            llm=llm,
            tool_call_listener=self._on_tool_call
        )
    
    def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
        prompt = todo_planner_instructions.format(
            current_date=get_current_date(),
            research_topic=state.research_topic,
        )
        
        response = self._agent.run(prompt)
        tasks_payload = self._extract_tasks(response)
        
        todo_items = []
        for idx, item in enumerate(tasks_payload, start=1):
            task = TodoItem(
                id=idx,
                title=item["title"],
                intent=item["intent"],
                query=item["query"],
            )
            todo_items.append(task)
        
        return todo_items
    
    def _extract_tasks(self, response: str) -> List[dict]:
        """从Agent响应中提取JSON"""
        # 使用正则表达式提取JSON部分
        json_match = re.search(r'\[.*\]', response, re.DOTALL)
        if json_match:
            json_str = json_match.group(0)
            return json.loads(json_str)
        else:
            raise ValueError("无法从响应中提取JSON")

Agent 2:任务总结专家(Task Summarizer)

职责:总结搜索结果,提取关键信息

设计理念:任务总结专家的核心任务是阅读搜索结果,提取关键信息,并以结构化的方式呈现。这个过程类似于人类研究者在阅读文献后做笔记的过程

复制代码
# Prompt 设计(提示词包含任务标题、意图、查询等上下文帮助 Agent 理解任务,明确要求输出包含核心观点、关键数据、来源引用,强调为每个观点添加来源引用,并通过示例帮助 Agent 理解期望的输出格式)

task_summarizer_instructions = """
你是一个任务总结专家。你的任务是总结搜索结果,提取关键信息。

任务标题:{task_title}
任务意图:{task_intent}
搜索查询:{task_query}

搜索结果:
{search_results}

请仔细阅读以上搜索结果,提取关键信息,并以Markdown格式返回总结。

总结应该包含:
1. **核心观点**:搜索结果中的核心观点和结论
2. **关键数据**:重要的数字、日期、名称等
3. **来源引用**:为每个观点添加来源引用(使用[1]、[2]等标记)

请确保:
1. 总结简洁明了,避免冗余
2. 保留重要的细节和数据
3. 为每个观点添加来源引用
4. 使用Markdown格式(标题、列表、加粗等)

示例输出:
## 核心观点

多模态模型是一种能够处理多种类型数据的AI模型[1]。与传统的单模态模型不同,多模态模型可以同时理解文本、图像、音频等[2]。

**关键特点:**
- 跨模态理解[1]
- 统一表示[3]
- 端到端训练[2]

## 来源

[1] https://example.com/source1
[2] https://example.com/source2
[3] https://example.com/source3
"""
python 复制代码
# 代码实现
class SummarizationService:
    def __init__(self, llm: HelloAgentsLLM):
        self._agent = ToolAwareSimpleAgent(
            name="Task Summarizer",
            system_prompt="你是一个任务总结专家",
            llm=llm,
            tool_call_listener=self._on_tool_call
        )
    
    def summarize_task(
        self,
        task: TodoItem,
        search_results: List[dict]
    ) -> str:
        # 格式化搜索结果
        formatted_sources = self._format_sources(search_results)
        
        prompt = task_summarizer_instructions.format(
            task_title=task.title,
            task_intent=task.intent,
            task_query=task.query,
            search_results=formatted_sources,
        )
        
        summary = self._agent.run(prompt)
        return summary
    
    def _format_sources(self, search_results: List[dict]) -> str:
        """格式化搜索结果"""
        formatted = []
        for idx, result in enumerate(search_results, start=1):
            formatted.append(
                f"[{idx}] {result['title']}\n"
                f"URL: {result['url']}\n"
                f"摘要: {result['snippet']}\n"
            )
        return "\n".join(formatted)

Agent 3:报告撰写专家(Report Writer)

职责:整合所有子任务的总结,生成最终报告

设计理念:报告撰写专家的核心任务是将所有子任务的总结整合成一份结构化的报告。这个过程类似于人类研究者在完成所有调研后撰写研究报告的过程

python 复制代码
# Prompt 设计(提示词明确要求报告包含标题、概述、详细分析、总结、参考文献等结构,强调按逻辑顺序组织内容,要求合并重复信息消除冗余,并保留所有来源引用)

report_writer_instructions = """
你是一个报告撰写专家。你的任务是整合所有子任务的总结,生成一份结构化的研究报告。

研究主题:{research_topic}

子任务总结:
{task_summaries}

请整合以上所有子任务的总结,生成一份结构化的研究报告。

报告应该包含:
1. **标题**:研究主题
2. **概述**:简要介绍研究主题和报告结构(2-3段)
3. **各个子任务的详细分析**:按照逻辑顺序组织(使用二级标题)
4. **总结**:总结研究的主要发现(1-2段)
5. **参考文献**:所有来源引用(按照子任务分组)

请确保:
1. 报告结构清晰,逻辑连贯
2. 消除重复的信息
3. 保留所有来源引用
4. 使用Markdown格式

示例输出:
# 多模态大模型的最新进展

## 概述

本报告系统地研究了多模态大模型的最新进展...

## 1. 什么是多模态模型

(此处插入子任务1的总结)

## 2. 最新的多模态模型有哪些

(此处插入子任务2的总结)

...

## 总结

通过本次研究,我们了解了...

## 参考文献

### 任务1:什么是多模态模型
[1] https://example.com/source1
...
"""
python 复制代码
# 代码实现
class ReportingService:
    def __init__(self, llm: HelloAgentsLLM):
        self._agent = ToolAwareSimpleAgent(
            name="Report Writer",
            system_prompt="你是一个报告撰写专家",
            llm=llm,
            tool_call_listener=self._on_tool_call
        )
    
    def generate_report(
        self,
        research_topic: str,
        task_summaries: List[Tuple[TodoItem, str]]
    ) -> str:
        # 格式化子任务总结
        formatted_summaries = self._format_summaries(task_summaries)
        
        prompt = report_writer_instructions.format(
            research_topic=research_topic,
            task_summaries=formatted_summaries,
        )
        
        report = self._agent.run(prompt)
        return report
    
    def _format_summaries(
        self,
        task_summaries: List[Tuple[TodoItem, str]]
    ) -> str:
        """格式化子任务总结"""
        formatted = []
        for idx, (task, summary) in enumerate(task_summaries, start=1):
            formatted.append(
                f"## 任务{idx}:{task.title}\n"
                f"意图:{task.intent}\n\n"
                f"{summary}\n"
            )
        return "\n".join(formatted)

ToolAwareSimpleAgent 的设计

在深度研究助手中,我们需要记录每个 Agent 的工具调用情况,用于:

  • 调试:查看 Agent 调用了哪些工具,传入了什么参数
  • 日志:记录研究过程中的所有操作
  • 分析:分析 Agent 的行为模式
  • 进度展示 :实时显示 Agent 正在做什么

SimpleAgent本身不支持工具调用监听,因此我们需要扩展它

ToolAwareSimpleAgent 在 SimpleAgent 的基础上增加了一个 tool_call_listener 参数,这是一个回调函数,每次工具调用时都会被调用

python 复制代码
# 使用示例
from hello_agents import ToolAwareSimpleAgent

def tool_listener(call_info):
    print(f"Agent: {call_info['agent_name']}")
    print(f"工具: {call_info['tool_name']}")
    print(f"参数: {call_info['parsed_parameters']}")
    print(f"结果: {call_info['result']}")

agent = ToolAwareSimpleAgent(
    name="研究助手",
    system_prompt="你是一个研究助手",
    llm=llm,
    tool_call_listener=tool_listener
)

# ToolAwareSimpleAgent继承自SimpleAgent,重写了_execute_tool_call方法:
class ToolAwareSimpleAgent(SimpleAgent):
    def __init__(
        self,
        name: str,
        system_prompt: str,
        llm: HelloAgentsLLM,
        tool_registry: Optional[ToolRegistry] = None,
        tool_call_listener: Optional[Callable] = None,
    ):
        super().__init__(
            name=name,
            system_prompt=system_prompt,
            llm=llm,
            tool_registry=tool_registry,
        )
        self._tool_call_listener = tool_call_listener
    
    def _execute_tool_call(self, tool_name: str, parameters: str) -> str:
        """执行工具调用,并通知监听器"""
        # 解析参数
        parsed_parameters = self._parse_parameters(parameters)
        
        # 调用工具
        result = super()._execute_tool_call(tool_name, parameters)
        
        # 通知监听器
        if self._tool_call_listener:
            self._tool_call_listener({
                "agent_name": self.name,
                "tool_name": tool_name,
                "parsed_parameters": parsed_parameters,
                "result": result,
            })
        
        return result

在深度研究助手中,我们使用 ToolAwareSimpleAgent 来记录所有 Agent 的工具调用:

python 复制代码
class DeepResearchAgent:
    def __init__(self, config: Configuration):
        self.config = config
        self.llm = HelloAgentsLLM(...)
        
        # 创建工具调用监听器
        def tool_listener(call_info):
            self._emit_event({
                "type": "tool_call",
                "agent": call_info["agent_name"],
                "tool": call_info["tool_name"],
                "parameters": call_info["parsed_parameters"],
            })
        
        # 创建三个Agent,都使用相同的监听器
        self.planner = PlanningService(self.llm, tool_listener)
        self.summarizer = SummarizationService(self.llm, tool_listener)
        self.reporter = ReportingService(self.llm, tool_listener)

Agent 协作模式

三个 Agent 之间是顺序协作 的关系

顺序协作模式的特点是:

  • 线性流程:Agent 按照固定的顺序执行
  • 明确的输入输出:每个 Agent 的输入来自上一个 Agent 的输出
  • 无并发:同一时间只有一个 Agent 在工作

DeepResearchAgent 是整个系统的核心协调器,负责调度三个 Agent

python 复制代码
class DeepResearchAgent:
    def run(self, research_topic: str) -> str:
        # 1. 规划阶段
        self._emit_event({"type": "status", "message": "正在规划研究任务..."})
        todo_list = self.planner.plan_todo_list(research_topic)
        self._emit_event({"type": "tasks", "tasks": todo_list})
        
        # 2. 执行阶段
        task_summaries = []
        for task in todo_list:
            self._emit_event({
                "type": "status",
                "message": f"正在研究:{task.title}"
            })
            
            # 搜索
            search_results = self.search_service.search(task.query)
            
            # 总结
            summary = self.summarizer.summarize_task(task, search_results)
            task_summaries.append((task, summary))
            
            self._emit_event({
                "type": "task_completed",
                "task_id": task.id
            })
        
        # 3. 报告阶段
        self._emit_event({"type": "status", "message": "正在生成报告..."})
        report = self.reporter.generate_report(research_topic, task_summaries)
        self._emit_event({"type": "report", "content": report})
        
        return report

工具系统集成

SearchTool 扩展

搜索是深度研究助手最核心的功能,多搜索引擎使得系统能够适应不同的使用场景和需求

在深度研究助手中,我们通过配置文件选择搜索引擎:

复制代码
# config.py
class SearchAPI(str, Enum):
    TAVILY = "tavily"
    DUCKDUCKGO = "duckduckgo"
    PERPLEXITY = "perplexity"
    SEARXNG = "searxng"
    ADVANCED = "advanced"

class Configuration(BaseModel):
    search_api: SearchAPI = SearchAPI.DUCKDUCKGO
    # ...


# .env
SEARCH_API=tavily
python 复制代码
# 搜索结果可能包含重复的 URL,我们需要去重:
def deduplicate_sources(sources: List[dict]) -> List[dict]:
    """去除重复的URL"""
    seen_urls = set()
    unique_sources = []
    
    for source in sources:
        if source["url"] not in seen_urls:
            seen_urls.add(source["url"])
            unique_sources.append(source)
    
    return unique_sources

# 搜索结果可能包含大量文本,我们需要限制每个来源的 Token 数量:
def limit_source_tokens(source: dict, max_tokens: int = 2000) -> dict:
    """限制来源的Token数量"""
    snippet = source["snippet"]
    
    # 简单的Token估算:1个Token约等于4个字符
    max_chars = max_tokens * 4
    
    if len(snippet) > max_chars:
        snippet = snippet[:max_chars] + "..."
    
    return {
        **source,
        "snippet": snippet
    }

NoteTool 使用

在研究过程中,我们需要记录每个子任务的搜索结果、总结以及最终的研究报告。这些信息需要持久化到磁盘,以便在研究过程中断时能够从上次的进度继续,同时也方便查看研究过程中的所有操作,分析研究的质量和效率

NoteTool 将笔记存储在指定的工作空间目录中,每个笔记是一个 Markdown 文件。笔记的文件名是任务 ID,内容包含任务标题、任务意图、搜索查询、搜索结果和总结

最后生成的文件风格会是下面的树状图风格:

复制代码
workspace/
├── notes/
│   ├── 1.md  # 任务1的笔记
│   ├── 2.md  # 任务2的笔记
│   ├── 3.md  # 任务3的笔记
│   └── ...
└── reports/
    └── final_report.md  # 最终报告

在深度研究助手中,我们使用 NoteTool 来记录每个子任务的研究进度:

python 复制代码
class NotesService:
    def __init__(self, workspace: str):
        self.note_tool = NoteTool(workspace=workspace)
    
    def save_task_summary(
        self,
        task: TodoItem,
        search_results: List[dict],
        summary: str
    ):
        """保存任务总结"""
        # 格式化笔记内容
        content = self._format_note_content(
            task=task,
            search_results=search_results,
            summary=summary
        )
        
        # 创建笔记
        self.note_tool.run({
            "action": "create",
            "title": f"任务{task.id}:{task.title}",
            "content": content,
            "tags": ["research", "summary"]
        })
    
    def _format_note_content(
        self,
        task: TodoItem,
        search_results: List[dict],
        summary: str
    ) -> str:
        """格式化笔记内容"""
        content = f"# 任务{task.id}:{task.title}\n\n"
        content += f"## 任务信息\n\n"
        content += f"- **意图**:{task.intent}\n"
        content += f"- **查询**:{task.query}\n\n"
        
        content += f"## 搜索结果\n\n"
        for idx, result in enumerate(search_results, start=1):
            content += f"[{idx}] {result['title']}\n"
            content += f"URL: {result['url']}\n"
            content += f"摘要: {result['snippet']}\n\n"
        
        content += f"## 总结\n\n{summary}\n"
        
        return content

ToolRegistry 工具管理

ToolRegistry 是 HelloAgents 框架的工具注册表,用于管理所有工具的注册和调用。在深度研究助手中,我们使用ToolRegistry 来管理 SearchToolNoteTool

在创建 Agent 之前,我们需要先注册工具:

python 复制代码
from hello_agents import ToolAwareSimpleAgent
from hello_agents.tools import ToolRegistry
from hello_agents.tools import SearchTool
from hello_agents.tools import NoteTool

# 创建工具
search_tool = SearchTool(backend="hybrid")
note_tool = NoteTool(workspace="./workspace/notes")

# 创建注册表
registry = ToolRegistry()

# 注册工具
registry.register_tool(search_tool)
registry.register_tool(note_tool)

# 创建Agent
agent = ToolAwareSimpleAgent(
    name="研究助手",
    system_prompt="你是一个研究助手",
    llm=llm,
    tool_registry=registry
)

服务层实现

本节将详细介绍核心服务的实现,包括 PlanningServiceSummarizationServiceReportingServiceSearchService。这些服务是连接 Agent 和工具的桥梁,负责具体的业务逻辑

任务规划服务

PlanningService 负责调用研究规划 Agent,将研究主题分解为子任务。这是整个研究流程的第一步,也是最关键的一步
(1)方案实现

它的核心职责是:

  • 构建规划 Prompt:根据研究主题和当前日期构建 Prompt
  • 调用规划 Agent :调用 TODO Planner Agent 生成子任务列表
  • 解析 JSON 响应:从 Agent 的响应中提取 JSON 格式的子任务列表
  • 验证子任务格式:确保每个子任务包含必需的字段(title、intent、query)
python 复制代码
import re
import json
from typing import List, Callable, Optional
from datetime import datetime

from hello_agents import HelloAgentsLLM
from hello_agents import ToolAwareSimpleAgent
from models import TodoItem, SummaryState
from prompts import todo_planner_instructions

class PlanningService:
    """任务规划服务"""

    def __init__(
        self,
        llm: HelloAgentsLLM,
        tool_call_listener: Optional[Callable] = None
    ):
        self._llm = llm
        self._tool_call_listener = tool_call_listener

        # 创建规划Agent
        self._agent = ToolAwareSimpleAgent(
            name="TODO Planner",
            system_prompt="你是一个研究规划专家,擅长将复杂的研究主题分解为清晰的子任务。",
            llm=llm,
            tool_call_listener=tool_call_listener
        )

    def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
        """规划TODO列表

        Args:
            state: 研究状态,包含研究主题

        Returns:
            子任务列表
        """
        # 构建Prompt
        prompt = todo_planner_instructions.format(
            current_date=self._get_current_date(),
            research_topic=state.research_topic,
        )

        # 调用Agent
        response = self._agent.run(prompt)

        # 解析JSON
        tasks_payload = self._extract_tasks(response)

        # 验证并创建TodoItem
        todo_items = []
        for idx, item in enumerate(tasks_payload, start=1):
            # 验证必需字段
            if not all(key in item for key in ["title", "intent", "query"]):
                raise ValueError(f"任务{idx}缺少必需字段")

            task = TodoItem(
                id=idx,
                title=item["title"],
                intent=item["intent"],
                query=item["query"],
            )
            todo_items.append(task)

        return todo_items

    def _get_current_date(self) -> str:
        """获取当前日期"""
        return datetime.now().strftime("%Y年%m月%d日")

    def _extract_tasks(self, response: str) -> List[dict]:
        """从Agent响应中提取JSON

        Agent的响应可能包含额外的文本,如:
        "好的,我将为您规划以下任务:\n[{...}, {...}]\n这些任务涵盖了..."

        我们需要提取其中的JSON部分。
        """
        # 方法1:使用正则表达式提取JSON数组
        json_match = re.search(r'\[.*\]', response, re.DOTALL)
        if json_match:
            json_str = json_match.group(0)
            try:
                return json.loads(json_str)
            except json.JSONDecodeError as e:
                raise ValueError(f"JSON解析失败:{e}")

        # 方法2:如果没有找到JSON数组,尝试直接解析整个响应
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            raise ValueError("无法从响应中提取JSON")

(2)JSON 解析与验证

Agent 返回的 JSON 可能包含额外的文本或格式错误,我们需要 robust 的解析逻辑:
常见问题

包含额外文本 :Agent 可能在 JSON 前后添加说明文字
格式错误 :JSON 可能缺少引号、逗号等
字段缺失:某些子任务可能缺少必需字段
解决方案

使用正则表达式 :提取 JSON 部分
多种解析策略 :先尝试提取 JSON 数组,再尝试直接解析
字段验证:确保每个子任务包含必需字段

python 复制代码
# 示例
# Agent响应示例1:包含额外文本
response1 = """
好的,我将为您规划以下任务:

[
  {
    "title": "什么是多模态模型",
    "intent": "了解基础概念",
    "query": "multimodal model definition"
  },
  {
    "title": "最新的多模态模型",
    "intent": "了解技术现状",
    "query": "latest multimodal models 2024"
  }
]

这些任务涵盖了Datawhale组织的基本信息和核心项目。
"""

# 提取JSON
tasks1 = service._extract_tasks(response1)
# 结果:[{"title": "Datawhale的基本信息", ...}, ...]

# Agent响应示例2:纯JSON
response2 = """
[
  {"title": "Datawhale的基本信息", "intent": "了解组织定位", "query": "Datawhale organization introduction"},
  {"title": "Datawhale的主要项目", "intent": "了解核心内容", "query": "Datawhale projects tutorials 2024"}
]
"""

# 提取JSON
tasks2 = service._extract_tasks(response2)
# 结果:[{"title": "什么是多模态模型", ...}, ...]

(3)规划质量评估
一个好的规划应该满足以下标准:

覆盖全面:涵盖主题的所有重要方面

逻辑清晰:子任务之间有明确的逻辑关系

查询精准:搜索查询能够准确找到相关资料

数量适中:3-5 个子任务

python 复制代码
# 质量评估方法
def evaluate_plan(self, todo_items: List[TodoItem]) -> dict:
    """评估规划质量

    Returns:
        评估结果,包含分数和建议
    """
    score = 100
    suggestions = []

    # 检查数量
    if len(todo_items) < 3:
        score -= 20
        suggestions.append("子任务数量过少,可能遗漏重要信息")
    elif len(todo_items) > 5:
        score -= 10
        suggestions.append("子任务数量过多,可能存在冗余")

    # 检查查询质量
    for task in todo_items:
        if len(task.query.split()) < 2:
            score -= 10
            suggestions.append(f"任务「{task.title}」的查询过于简单")

    # 检查逻辑关系
    # (这里可以添加更复杂的逻辑检查)

    return {
        "score": score,
        "suggestions": suggestions
    }

总结服务

SummarizationService 负责调用任务总结 Agent,总结搜索结果。这是研究流程的核心环节,决定了研究的质量

它的职责是:

  • 格式化搜索结果:将搜索结果格式化为易读的文本
  • 构建总结 Prompt:根据任务信息和搜索结果构建 Prompt
  • 调用总结 Agent:调用 Task Summarizer Agent 生成总结
  • 提取来源引用:从总结中提取来源引用
python 复制代码
from typing import List, Callable, Optional, Tuple

from hello_agents import HelloAgentsLLM
from hello_agents import ToolAwareSimpleAgent
from models import TodoItem
from prompts import task_summarizer_instructions

class SummarizationService:
    """总结服务"""

    def __init__(
        self,
        llm: HelloAgentsLLM,
        tool_call_listener: Optional[Callable] = None
    ):
        self._llm = llm
        self._tool_call_listener = tool_call_listener

        # 创建总结Agent
        self._agent = ToolAwareSimpleAgent(
            name="Task Summarizer",
            system_prompt="你是一个任务总结专家,擅长从搜索结果中提取关键信息。",
            llm=llm,
            tool_call_listener=tool_call_listener
        )

    def summarize_task(
        self,
        task: TodoItem,
        search_results: List[dict]
    ) -> Tuple[str, List[str]]:
        """总结任务

        Args:
            task: 任务信息
            search_results: 搜索结果列表

        Returns:
            (总结文本, 来源URL列表)
        """
        # 格式化搜索结果
        formatted_sources = self._format_sources(search_results)

        # 构建Prompt
        prompt = task_summarizer_instructions.format(
            task_title=task.title,
            task_intent=task.intent,
            task_query=task.query,
            search_results=formatted_sources,
        )

        # 调用Agent
        summary = self._agent.run(prompt)

        # 提取来源URL
        source_urls = [result["url"] for result in search_results]

        return summary, source_urls

    def _format_sources(self, search_results: List[dict]) -> str:
        """格式化搜索结果

        将搜索结果格式化为易读的文本,包含:
        - 序号
        - 标题

### 报告结构设计

最终报告应该包含以下部分,.......

## 参考文献

### 任务1:什么是多模态模型
- https://example.com/multimodal-model-definition
....

### 任务2:最新的多模态模型有哪些
- https://example.com/gpt4v
....
...

报告生成服务

ReportingService 负责调用报告生成 Agent,整合所有子任务的总结。这是研究流程的最后一步,生成最终的研究报告

它的职责是:

  • 格式化子任务总结:将所有子任务的总结格式化为统一的格式
  • 构建报告 Prompt:根据研究主题和子任务总结构建 Prompt
  • 调用报告 Agent:调用 Report Writer Agent 生成最终报告
  • 整理引用:将所有来源引用整理到参考文献部分
python 复制代码
from typing import List, Callable, Optional, Tuple

from hello_agents import HelloAgentsLLM
from hello_agents import ToolAwareSimpleAgent
from models import TodoItem
from prompts import report_writer_instructions

class ReportingService:
    """报告生成服务"""

    def __init__(
        self,
        llm: HelloAgentsLLM,
        tool_call_listener: Optional[Callable] = None
    ):
        self._llm = llm
        self._tool_call_listener = tool_call_listener

        # 创建报告Agent
        self._agent = ToolAwareSimpleAgent(
            name="Report Writer",
            system_prompt="你是一个报告撰写专家,擅长整合信息并生成结构化的报告。",
            llm=llm,
            tool_call_listener=tool_call_listener
        )

    def generate_report(
        self,
        research_topic: str,
        task_summaries: List[Tuple[TodoItem, str, List[str]]]
    ) -> str:
        """生成最终报告

        Args:
            research_topic: 研究主题
            task_summaries: 子任务总结列表,每个元素是(任务, 总结, 来源URL列表)

        Returns:
            最终报告(Markdown格式)
        """
        # 格式化子任务总结
        formatted_summaries = self._format_summaries(task_summaries)

        # 构建Prompt
        prompt = report_writer_instructions.format(
            research_topic=research_topic,
            task_summaries=formatted_summaries,
        )

        # 调用Agent
        report = self._agent.run(prompt)

        return report

    def _format_summaries(
        self,
        task_summaries: List[Tuple[TodoItem, str, List[str]]]
    ) -> str:
        """格式化子任务总结

        将所有子任务的总结格式化为统一的格式,包含:
        - 任务序号
        - 任务标题
        - 任务意图
        - 总结内容
        - 来源URL
        """
        formatted = []
        for idx, (task, summary, source_urls) in enumerate(task_summaries, start=1):
            formatted.append(
                f"## 任务{idx}:{task.title}\n\n"
                f"**意图**:{task.intent}\n\n"
                f"{summary}\n\n"
                f"**来源**:\n"
            )
            for url in source_urls:
                formatted.append(f"- {url}\n")
            formatted.append("\n")

        return "".join(formatted)

搜索调度服务

SearchService 负责调度搜索引擎,执行搜索并返回结果。这是连接 Agent 和 SearchTool 的桥梁。在这里我们没有采用往常一样的使得 simpleAgent 直接调用工具的形式,而是将 SearchTool 的执行结果通过中间层来返回给 Agent,这样会使得 Agent 更加专注处理得到的信息

它的职责是:

  • 调度搜索引擎:根据配置选择搜索引擎
  • 执行搜索:调用 SearchTool 执行搜索
  • 处理结果:去重、限制 Token、格式化
  • 错误处理:处理搜索失败的情况
python 复制代码
from typing import List, Optional
import logging

from hello_agents.tools import SearchTool
from config import Configuration

logger = logging.getLogger(__name__)

class SearchService:
    """搜索调度服务"""

    def __init__(self, config: Configuration):
        self.config = config

        # 创建SearchTool
        self.search_tool = SearchTool(backend="hybrid")

    def search(
        self,
        query: str,
        max_results: int = 5
    ) -> List[dict]:
        """执行搜索

        Args:
            query: 搜索查询
            max_results: 最大结果数量

        Returns:
            搜索结果列表
        """
        try:
            # 调用SearchTool
            raw_response = self.search_tool.run({
                "input": query,
                "backend": self.config.search_api.value,
                "mode": "structured",
                "max_results": max_results
            })

            # 提取结果
            results = raw_response.get("results", [])

            # 处理结果
            results = self._deduplicate_sources(results)
            results = self._limit_source_tokens(results)

            logger.info(f"搜索成功:{query},返回{len(results)}个结果")

            return results

        except Exception as e:
            logger.error(f"搜索失败:{query},错误:{e}")
            return []

    def _deduplicate_sources(self, sources: List[dict]) -> List[dict]:
        """去除重复的URL"""
        seen_urls = set()
        unique_sources = []

        for source in sources:
            url = source.get("url", "")
            if url and url not in seen_urls:
                seen_urls.add(url)
                unique_sources.append(source)

        return unique_sources

    def _limit_source_tokens(
        self,
        sources: List[dict],
        max_tokens_per_source: int = 2000
    ) -> List[dict]:
        """限制每个来源的Token数量"""
        limited_sources = []

        for source in sources:
            snippet = source.get("snippet", "")

            # 简单的Token估算:1个Token约等于4个字符
            max_chars = max_tokens_per_source * 4

            if len(snippet) > max_chars:
                snippet = snippet[:max_chars] + "..."

            limited_sources.append({
                **source,
                "snippet": snippet
            })

        return limited_sources

总结

(1)TODO 驱动的研究范式

我们提出了一种新的研究范式------TODO 驱动的研究。这种范式将复杂的研究主题分解为可执行的子任务,通过三个阶段完成研究:

  • 规划阶段 :将研究主题分解为 3-5 个子任务,每个子任务包含标题意图搜索查询

  • 执行阶段:对每个子任务执行搜索和总结,生成结构化的知识

  • 报告阶段:整合所有子任务的总结,生成最终的研究报告
    这种范式的优势在于:

  • 可控性强:每个子任务都有明确的目标和范围

  • 质量可靠:通过专门的 Agent 保证每个环节的质量

  • 易于调试:可以单独调试每个子任务

  • 可扩展性好:可以轻松添加新的子任务或修改现有子任务
    (2)三 Agent 协作系统

我们设计了三个专门的 Agent,各司其职:

  • TODO Planner(研究规划专家):负责将研究主题分解为子任务

  • Task Summarizer(任务总结专家):负责总结每个子任务的搜索结果

  • Report Writer(报告撰写专家):负责整合所有子任务的总结,生成最终报告
    这种设计的优势在于:

  • 职责清晰:每个 Agent 专注于一个特定的任务

  • Prompt 优化:可以为每个 Agent 定制专门的 Prompt

  • 易于维护:修改一个 Agent 不会影响其他 Agent

  • 质量保证:每个 Agent 都是该领域的"专家"
    (3)ToolAwareSimpleAgent 的设计

我们扩展了 HelloAgents 框架的SimpleAgent,实现了ToolAwareSimpleAgent。这个 Agent 具有工具调用监听能力,可以:

  • 监听工具调用:通过回调函数监听每次工具调用
  • 实时反馈:将工具调用信息实时推送给前端
  • 调试支持:记录所有工具调用,便于调试
    (4)工具系统集成

我们充分利用了 HelloAgents 框架的工具系统:

  • SearchTool:扩展支持更多种搜索引擎(Tavily、DuckDuckGo、Perplexity 等)
  • NoteTool:持久化研究进度,支持恢复和审计
  • ToolRegistry :统一管理所有工具,支持自定义扩展

通过配置化的设计,用户可以轻松切换搜索引擎,无需修改代码
(5)核心服务实现

我们实现了四个核心服务,连接 Agent 和工具:

  • PlanningService:调用规划 Agent,解析 JSON,验证格式
  • SummarizationService:调用总结 Agent,处理搜索结果,提取来源
  • ReportingService:调用报告 Agent,整合总结,生成报告
  • SearchService :调度搜索引擎,处理结果,错误降级,结果缓存

这些服务各司其职,通过清晰的接口协作,实现了从研究主题到最终报告的自动化流程

参考

hello-agents

相关推荐
老码观察1 小时前
数环通LinkBot:当AI智能体遇上企业集成
人工智能
天云数据1 小时前
战略契合,落地先行:天云数据AI+能源双向赋能的实战范本
人工智能·能源
用户3457703593571 小时前
【简单上手】服务器上部署兼容 OpenAI API 的 LLM 的 vLLM 方案
人工智能
青岛前景互联信息技术有限公司1 小时前
以一体化管控新思路,构建园区全域全维度安全管理体系
大数据·人工智能·物联网
加勒比海带661 小时前
目标检测算法——农林行业数据集汇总附下载链接【Plant】
大数据·图像处理·人工智能·算法·目标检测
工业机器人销售服务1 小时前
法奥协作机器人:智能避障,安全协作
人工智能·机器人
Quz1 小时前
在 Claude Code中配置DeepSeek:从报错到成功调用【支持DeepSeekV4】
人工智能
搭贝1 小时前
建筑多分支企业数字化实战:凯驿景澄建设项目管理系统落地案例
大数据·人工智能·低代码·数字化·工程项目技术方案
MediaTea1 小时前
人工智能通识课:机器学习之监督学习
人工智能·学习·机器学习