跳转至

Agent 概念

🧠 什么是 LangChain Agent?它和 Chain 最本质的区别是什么?

LangChain Agent 不是一条固定的流水线,而是一个能根据任务动态选择行动的智能体。你可以把它想象成一个项目经理:你告诉它最终目标(用户的问题),它自己决定需要什么工具、以什么顺序调用工具、如何分析结果,最终完成任务。

Chain(链)则是事先编排好的固定操作序列。比如一个 RetrievalQA 链,它的逻辑是写死的:收到问题 -> 向量检索 -> 拼接到 Prompt -> 调用 LLM -> 返回答案。链不具备自主决策能力。

最本质的区别:

  • 决策权:Agent 自主决定下一步做什么;Chain 硬编码了执行流程。

  • 灵活性:Agent 能处理未知的、多步推理的问题,可以根据中间结果调整策略;Chain 只能处理预设流程内的问题。

  • 循环执行:Agent 内建了“思考-行动-观察”的循环,直到达成目标;Chain 调用一次就结束。

举个例子:用户问“明天北京的天气怎么样?如果下雨,给我推荐室内活动”。

  • 如果用 Chain,你必须事先写死:先调用天气 API,再根据返回值判断,再调用活动推荐 API。任何一个环节变化都需要修改链。

  • 如果用 Agent,你只需告诉它有两个工具:WeatherToolActivityTool。Agent 会自己推理:“我需要先查天气 -> 调用天气工具 -> 发现下雨 -> 再调用活动工具搜索室内活动 -> 整理回答”。所有决策都由 LLM 做出。

LangChain 中的实现:Agent 通常与 AgentExecutor 配合使用。AgentExecutor 负责运行 Agent 的循环、管理工具调用、处理错误,是 Agent 的“执行引擎”。

✅ 经验之谈:我刚用 LangChain 时,总想把所有流程都写成 Chain,结果越写越复杂。后来发现,一旦业务逻辑稍微复杂一点,用 Agent 会省很多事,因为它把“怎么完成”的决策交给了 LLM,你只需要专心把工具做好。但 Agent 也带来了不确定性,需要更多调试。


🔄 画出 Agent 的执行循环:思考 → 行动 → 观察 → 思考……并解释每一步。

Agent 的执行循环就是著名的 ReAct 模式(Reasoning + Acting),其流程如下:

image.png

每一步的含义:

  • 思考 (Thought):Agent 分析当前状态和已有信息,决定下一步应该做什么。它用自然语言表达推理过程,比如“我需要先获取天气数据,所以应该使用 Weather 工具”。

  • 行动 (Action):Agent 输出一个具体的工具调用指令,包括工具名称和输入参数。例如 Action: Weather, Action Input: "北京"

  • 观察 (Observation):工具执行后返回的结果。Agent 会把这个结果作为新的信息,反馈到下一轮思考中。例如 Observation: 北京明天晴,25度

  • 循环:Agent 根据观察再次思考,判断是否还需要进一步行动,或者已经可以给出最终答案。如果需要更多信息,就继续循环;否则输出最终答案。

AgentExecutor 的内部循环:

  1. 将用户输入、可用工具列表、历史交互(scratchpad)组装成 Prompt,发给 LLM。

  2. LLM 返回 Thought/Action/Action Input。

  3. 解析输出,如果包含 Action,则调用相应工具。

  4. 将工具输出作为 Observation 追加到 scratchpad。

  5. 重复步骤 1,直到 LLM 输出 Final Answer 或达到最大步数。

✅ 经验之谈:这个循环让 Agent 有了“试错”能力。但也要警惕,如果工具描述不清晰,Agent 可能会在循环里打转,反复调用同一个工具。这时需要设置合理的 max_iterations,并且让工具返回足够明确的信息。


📚 Agent 有哪几种主要类型?(ReAct, OpenAI Functions, StructuredChat 等)各自的特点。

LangChain 支持多种 Agent 类型,它们的核心区别在于如何构造 Prompt 和如何解析 LLM 的输出。

  1. ReAct Agent

  2. 特点:经典模式,强调“Thought, Action, Observation”。Prompt 通常包含详细的示例(Few-shot),指导 LLM 按固定格式输出。

  3. 适用场景:开源模型,或者对格式控制要求高的场景。

  4. 优势:逻辑清晰,可解释性强,不依赖模型特定的 API。

  5. 局限:输出格式有时不稳定,需要较长的 Prompt,会消耗更多 token。

  6. OpenAI Functions Agent

  7. 特点:专门为 OpenAI 的 Function Calling 能力设计。LLM 不再输出文本格式的 Action,而是直接生成一个函数调用 JSON。

  8. 适用场景:使用 gpt-3.5-turbo-0613gpt-4-0613 及以上版本。

  9. 优势:稳定性极高,支持复杂参数结构,能一次调用多个函数。

  10. 局限:强依赖 OpenAI API,其他模型不支持。

  11. StructuredChat Agent

  12. 特点:支持输出结构化的聊天消息,能够处理多参数、嵌套参数的工具。它使用 FunctionMessage 等消息类型。

  13. 适用场景:需要复杂工具调用,但不想绑定 OpenAI 时(例如使用 Anthropic 模型)。

  14. 优势:灵活,可以适配多种 LLM。

  15. 局限:配置较复杂,Prompt 工程要求高。

  16. SelfAskWithSearch Agent

  17. 特点:专门为需要多步搜索的场景设计。它会先提出一个中间问题,然后搜索,再根据搜索结果继续提问。

  18. 适用场景:需要多跳推理的事实性问题,例如“《哈利波特》中哈利的教父的堂姐的女儿是谁?”

  19. 优势:非常适合逐步挖掘信息。

  20. 局限:只适用于搜索类任务,通用性差。

  21. Conversational Agent

  22. 特点:为多轮对话优化,使用 ConversationalChatAgent,它能自动管理对话历史。

  23. 适用场景:聊天机器人。

  24. 优势:能结合上下文理解后续问题。

  25. 局限:推理能力不如 ReAct 或 Functions Agent。

  26. XML Agent

  27. 特点:用 XML 标签解析 Agent 的输出,适合需要复杂嵌套结构的场景。

✅ 我的选择经验:如果使用 OpenAI 的模型,无脑上 OpenAI Functions Agent,它真的太稳了。如果模型不支持 Function Calling,或者需要极致的可解释性,就选 ReAct。StructuredChat 我通常只在有特殊输出结构需求时才用。


📝 ReAct Agent 的 Prompt 结构是怎样的?为什么它强调“Thought, Action, Observation”?

ReAct Agent 的 Prompt 是整个 Agent 行为的核心。它通常包含以下部分:

  1. 系统指令/角色描述

告诉 LLM 你是一个助手,可以使用工具,并说明如何输出。

  1. 可用工具列表

每个工具的名字、描述和参数。例如:

You have access to the following tools:
> Weather: 查询指定城市的天气,输入城市名称
> Calculator: 计算数学表达式,输入表达式
  1. 输出格式要求

明确要求 LLM 按特定格式输出,通常是:

Thought: [你的思考过程]
Action: [工具名称]
Action Input: [工具输入]
Observation: [工具返回的结果]
... (重复)
Final Answer: [最终答案]
  1. 示例(Few-shot)

这是关键部分。LangChain 内置了一些示例,展示如何一步步推理和调用工具。这些示例教会 LLM 如何拆解问题、何时应该使用工具。

  1. 用户的问题

为什么强调“Thought, Action, Observation”?

这个结构源自认知科学中的“思考-行动-观察”循环。对于 LLM 来说,它有几个重要作用:

  • 分解复杂问题:强制 LLM 把推理过程写出来(Thought),这样它自己也能更清晰地规划下一步。

  • 结构化解耦:把“思考”(内部)和“行动”(外部)分开,让 LLM 知道何时该想、何时该做。

  • 可解释性:开发者可以阅读 Thought,理解 Agent 的决策逻辑,方便调试。

  • 错误恢复:如果 Observation 显示错误,Agent 可以根据 Thought 调整策略,而不是盲目重试。

✅ 实际感受:ReAct Prompt 就像教一个实习生如何工作。你不仅告诉他有什么工具,还给了他几个完整的例子,让他照着做。但这个 Prompt 非常长(轻松上千 token),成本不低。而且偶尔会出现格式错误,比如忘记写“Action:”,LangChain 的解析器就会报错。所以后来我基本都转向 Function Calling 了。


⚡ OpenAI Functions Agent 与 ReAct Agent 相比,本质区别在哪里?它如何利用 Function Calling?

本质区别:ReAct Agent 依赖 LLM 生成自由格式的文本,然后由 LangChain 用正则解析出工具名和参数。而 OpenAI Functions Agent 则利用了 OpenAI 模型的 Function Calling 原生能力,LLM 直接输出结构化的 JSON 函数调用,不再需要文本解析。

Function Calling 的工作原理:

  1. 在调用 OpenAI API 时,我们可以定义一组 functions 参数,描述可用的函数及其参数 schema(JSON Schema)。

  2. 当 LLM 认为需要调用某个函数时,它不会生成文本,而是返回一个 function_call 对象,里面包含函数名和参数(JSON 格式)。

  3. LangChain 的 AgentExecutor 收到这个对象后,直接调用对应的工具,并将结果作为 FunctionMessage 再次发送给 LLM。

  4. LLM 基于结果继续推理,或者最终生成自然语言回复。

对比:

查看内嵌表格

✅ 体验:Function Calling 让工具调用从“可能出错”变成了“基本不出错”。因为 LLM 不再需要去“猜”如何格式化输出,它天然就知道该如何构造 JSON。而且它支持复杂的参数类型(如数组、对象),这在 ReAct 里几乎不可能。不过它绑定了 OpenAI,对于需要多模型兼容的场景,可能要用别的方案(如 Anthropic 的 Tool Use)。


🤔 在什么情况下你会选 OpenAI Functions Agent,而不是 ReAct Agent?

选择取决于模型、任务复杂度、稳定性要求和可移植性需求。

我会选 OpenAI Functions Agent 的情况:

  • 使用 OpenAI 的模型:这是首要条件。

  • 工具参数复杂:例如参数是嵌套对象、数组,或者需要可选参数。Function Calling 的 JSON Schema 可以完美定义这些,而 ReAct 的文本解析会非常痛苦。

  • 需要高稳定性的生产环境:Function Calling 的格式错误率极低,省去很多调试时间。

  • 需要并行调用多个函数:OpenAI 支持在单次响应中返回多个 tool_calls,Agent 可以同时执行多个独立操作,提高效率。ReAct 只能串行。

  • 希望 Prompt 更短:Functions Agent 的 Prompt 简洁,节省 token 成本。

我会选 ReAct Agent 的情况:

  • 使用非 OpenAI 模型,如 Llama、Claude 等(虽然 Claude 也有 Tool Use,但 LangChain 的 ReAct 更通用)。

  • 需要极致的可解释性:ReAct 的 Thought 是显式的自然语言,可以完整看到模型的“心路历程”。这在安全审计或教育场景很有价值。

  • 对可移植性要求高:如果未来可能切换模型提供商,ReAct 更通用。

  • 简单的工具调用:如果只是简单的字符串输入输出,ReAct 也足够。

✅ 个人做法:我现在绝大多数项目都用 Functions Agent,因为稳定性带来的收益远超其他。只有在需要支持开源模型的时候,才切换到 ReAct。而且对于 ReAct,我也会尽量减少复杂参数,尽量让工具只接收一个字符串。


🔧 StructuredChat Agent 可以输出更复杂的结构吗?举例说明它如何处理多参数工具。

StructuredChat Agent 是 LangChain 中一种更灵活的 Agent,它不像 ReAct 那样完全依赖文本解析,而是使用了聊天消息格式(如 FunctionMessageToolMessage)来传递工具调用。它可以处理更复杂的输出结构,尤其是在需要多参数、嵌套参数,或者希望输出更复杂的结构化数据时。

处理多参数工具的能力:

StructuredChat Agent 的 Prompt 中会明确要求 LLM 用特定的格式输出。例如,对于多参数工具,它可以要求 LLM 输出一个 JSON 对象,而不是简单的字符串。然后 LangChain 会解析这个 JSON,并作为参数传递给工具。

示例:假设你有一个 send_email 工具,需要收件人、主题、正文、附件(数组)等多个参数。

  • 在 ReAct 中,你可能会将输入压缩成一个字符串,然后让工具自行解析,这容易出错。

  • 在 StructuredChat Agent 中,你可以这样定义工具:

from langchain.tools import StructuredTool
def send_email(to: str, subject: str, body: str, attachments: list[str] = None):
    # 实际发送逻辑
    pass

tool = StructuredTool.from_function(
    func=send_email,
    name="send_email",
    description="发送邮件"
)

然后 LLM 会生成如下结构:

Action:
{
  "action": "send_email",
  "action_input": {
    "to": "abc@example.com",
    "subject": "会议通知",
    "body": "明天下午3点开会",
    "attachments": ["agenda.pdf"]
  }
}
  • LangChain 的 StructuredChatAgent 会将这个 JSON 解析成字典,并用 **kwargs 的形式传给 send_email 函数。这完全绕开了文本解析的不确定性。

优势:它能够输出任意的 JSON 结构,因此可以处理复杂参数。它还支持嵌套对象,只要工具的参数 schema 定义得当。

局限:StructuredChatAgent 的配置比 Functions Agent 复杂一些,对 LLM 的指令遵循能力要求较高。如果 LLM 没有输出合法的 JSON,会解析失败。

✅ 使用场景:当你的工具需要多个参数,但又不能使用 OpenAI Functions Agent(比如你用的是 Llama 或其他模型)时,StructuredChat Agent 是一个不错的选择。但请注意,它依然依赖 LLM 生成符合格式的文本,稳定性不如原生的 Function Calling。


🔍 SelfAskWithSearch Agent 的工作流程是怎样的?它什么时候会问一个中间问题?

SelfAskWithSearch Agent 是 LangChain 中专门为多步搜索设计的 Agent。它模拟了人类在查找复杂信息时的行为:当一个问题不能直接搜索到答案时,会先把它拆解成一个更简单的、可搜索的子问题,然后再根据结果逐步逼近最终答案。

工作流程:

  1. 用户提出一个需要多跳推理的问题,例如:“谁是《哈利波特》中哈利的教父的堂姐的女儿?”

  2. Agent 首先判断无法直接搜索这个问题,于是生成一个中间问题:“哈利波特的教父是谁?”

  3. 调用搜索工具,得到结果:“小天狼星布莱克”。

  4. Agent 接着问:“小天狼星布莱克的堂姐是谁?”

  5. 搜索得到:“贝拉特里克斯·莱斯特兰奇”。

  6. Agent 再问:“贝拉特里克斯·莱斯特兰奇的女儿是谁?”

  7. 搜索得到:“戴尔菲”。

  8. Agent 整理信息,输出最终答案:“戴尔菲”。

它什么时候会问一个中间问题? 当 LLM 分析用户问题后,认为无法通过单次搜索获得答案,或者问题中包含了未知实体时,它会自动生成一个中间问题。这个决策是由 LLM 根据 Prompt 中的示例(Few-shot)做出的。LangChain 的 SelfAskWithSearchAgent 内置了处理这种多跳推理的 Prompt。

特点:

  • 仅适用于搜索类任务,因为它依赖一个搜索工具(如 Google 搜索、向量库检索)。

  • 执行过程中会多次调用搜索工具,每次搜索一个简单问题。

  • Prompt 中包含了清晰的示例,让 LLM 学会何时分解问题。

限制:它假设所有信息都可以通过搜索得到,如果中间问题本身需要推理(例如需要计算),它就无能为力了。另外,它不会使用其他类型的工具,只专注于搜索。

✅ 个人看法:这个 Agent 设计得很巧妙,但通用性太差。我更多是把它当成一个教学案例,实际业务中,我通常会用 ReAct 或 Functions Agent 配合自定义的搜索工具,效果更灵活。


⏱️ 怎样限制 Agent 的最大迭代步数?设置这个参数有什么讲究?

限制方法:在 AgentExecutor 初始化时,通过 max_iterations 参数设置。

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    max_iterations=5,   # 最多5步
    verbose=True,
)

当 Agent 执行步数达到 max_iterations 时,AgentExecutor 会强制停止循环。此时,它可以根据 early_stopping_method 参数来处理:

  • "force"(默认):直接抛出异常(AgentFinish),不加额外处理。

  • "generate":停止后,让 LLM 基于当前已获得的所有信息,强行生成一个最终答案。这会额外调用一次 LLM,消耗 token,但能给用户一个结果。

设置这个参数的讲究:

  • 避免无限循环:这是最基本的安全措施。如果工具调用总是失败,或者 Agent 在循环中打转,没有 max_iterations 会导致 API 费用不断累积。

  • 平衡任务复杂度与成本:多数问题 3-5 步足够。如果设置太高,Agent 可能会进行不必要的过度探索,浪费时间和 token。如果太低,复杂问题可能无法完成。

  • 结合工具特性:如果某个工具执行速度慢(比如调用外部 API 需数秒),更应限制步数,防止用户体验极差。

  • 对结果的影响:步数不足可能导致最终答案不完整。因此,我通常设置 early_stopping_method="generate",至少给用户一个基于现有信息的答复,而不是报错。

  • 调试时:可以临时设高一些,观察 Agent 的行为;生产环境则保守设置。

✅ 经验:我一般设置 max_iterations=5early_stopping_method="generate"。这样既能应对大部分问题,又不会在失败时冷冰冰地报错。如果 Agent 频繁达到上限,说明要么工具不够好,要么问题太复杂,需要优化工具或拆分任务。


🛑 Agent 的终止条件有哪些?除了最大步数,还有哪些方式?(如特定 token、早停)

除了 max_iterations,Agent 的终止还有以下几种方式:

  1. LLM 主动输出终止标记 在 ReAct 模式中,LLM 会输出 Final Answer: 来表示任务完成。这是最自然的终止方式。AgentExecutor 检测到这个标记后,会停止循环并提取答案。

  2. 特定 Token 终止 可以为 LLM 设置 stop 参数,当生成到某个字符串时提前停止。例如设置 stop=["Observation:"],这可以避免 LLM 在思考时胡编乱造观察结果。但这对 Agent 终止本身不是主要方式。

  3. 时间超时(Timeout) 可以设置整个 Agent 执行的总时间限制。例如,如果 30 秒内未完成,则强制终止。这对于在线服务很重要,防止单个请求占用过久。LangChain 目前没有内置这个功能,但可以在外层用 asyncio.wait_for 实现。

  4. 早停(Early Stopping) 如前所述,当达到最大步数时,可以通过 early_stopping_method 让 Agent 强制生成一个答案。这相当于一种“软终止”,而不是直接报错。

  5. 回调函数中断 利用 LangChain 的回调系统,可以在 on_agent_action 等方法中根据自定义逻辑抛出异常,从而中断执行。例如,检测到工具连续返回相同错误时,主动终止。

  6. 自定义终止条件

在 Agent 的 Prompt 中可以定义额外的终止规则。例如:“如果你发现用户的问题与天气无关,直接回答‘我不知道’并终止。” 这样 LLM 可以在某些条件下提前结束,而不必调用工具。

  1. 输出解析失败重试次数限制 当 Agent 的输出格式不符合预期(解析错误),AgentExecutor 会尝试重新调用 LLM。max_execution_timemax_retries(通过 handle_parsing_errors 参数控制)可以限制重试次数,防止无限重试。

✅ 实践:我通常会组合使用 max_iterations + early_stopping_method="generate",并在外部设置一个总超时。对于可能失控的场景,自定义一个回调来监控状态,比如当发现 Agent 连续三次调用同一个工具且参数不变时,强制抛出异常并退出。

🔁 如果 Agent 陷入“反复调用同一个工具但无进展”的循环,你如何检测和中断?

Agent 陷入死循环是常见问题,通常表现为:连续多次调用同一个工具且参数几乎不变,或者 Thought 和 Observation 来回反复却始终不向最终答案迈进。要解决这个问题,需要检测和中断两步走。

🛠 检测方法

  1. 比较连续步骤的工具调用 在 Agent 执行循环中,维护一个最近 N 步的工具调用历史。如果发现连续 3 步(或自定义阈值)的 (tool_name, tool_input) 完全相同,就可以判定为重复。注意:不能只看工具名,因为 Agent 可能用不同参数调用同一工具(这是有意义的);只有参数也雷同时才算死循环。

  2. 检查 Observation 的信息增益

如果工具每次都返回相同的结果(例如搜索同一个错误关键词总是返回空),Agent 却仍继续尝试,说明它已经卡住。可以通过计算连续 Observation 的相似度(如文本余弦相似度)来检测。

  1. 自定义回调监控 利用 LangChain 的 on_agent_action 回调,记录每一步的 Action。可以在回调中维护状态,一旦检测到重复或停滞,就触发中断。
from langchain.callbacks import BaseCallbackHandler

class DeadLoopHandler(BaseCallbackHandler):
    def __init__(self, max_repeat=3):
        self.last_actions = []
        self.max_repeat = max_repeat

    def on_agent_action(self, action, **kwargs):
        self.last_actions.append((action.tool, action.tool_input))
        if len(self.last_actions) > self.max_repeat:
            self.last_actions.pop(0)
        if len(self.last_actions) == self.max_repeat and len(set(self.last_actions)) == 1:
            raise ValueError("Agent陷入死循环!")

🛑 中断方法

  1. 抛出异常强制退出

在检测到死循环时,抛出一个自定义异常,由外层的异常处理捕获,然后优雅地返回一个兜底回复。

  1. 修改 Agent 的 Prompt

在 Prompt 中加入指令:“如果你连续两次得到相同的结果,请不要重复调用,而是向用户说明你遇到困难”。这能从根源上减少死循环。

  1. 提前终止与 early_stopping_method 结合 max_iterationsearly_stopping_method="generate",即使死循环没有提前被检测,也会在步数上限强制总结回答。

  2. 动态调整温度

在检测到循环时,暂时提高 LLM 的温度(例如从 0 调到 0.5),增加输出随机性,帮助 Agent 跳出局部循环。这可以通过回调动态修改 LLM 参数实现。

✅ 实践经验:我通常会在回调里做一个简单的“连续相同 Action 计数器”,到了 3 次就直接抛异常,然后在外面捕获,返回一个友好的错误消息。这样做比依赖 max_iterations 更灵敏,因为有时 Agent 会在 5 步里重复 3 次,但还没达到步数上限,却已经浪费了大量时间和 token。


🎲 Agent 的决策过程是否总是确定性的?温度和采样参数如何影响 Agent 的行为?

Agent 的决策过程不是完全确定性的,它受 LLM 的采样参数(尤其是温度 temperature)和 top_p 的直接影响。

确定性 vs 随机性

  • temperature=0 时:LLM 几乎以贪心方式选择概率最高的 token,每次输出理论上一致(实际上由于 GPU 浮点运算的微小差异,可能略有不同,但基本确定)。此时 Agent 的行为也是高度可预测的,它会倾向于选择“最安全”的路径。这对需要稳定输出的任务(如数学计算)很有用,但可能使 Agent 在遇到困难时缺乏探索性,更容易陷入死循环。

  • temperature>0(如 0.5-1.0)时:LLM 的输出带有随机性,Agent 可能会在相同的情况下选择不同的工具或采取不同的推理路径。这有助于 Agent 探索更优解,但也可能导致结果不稳定,甚至产生奇怪的 Action。

其他参数的影响

  • top_p:与温度配合使用,限制采样池。较小的 top_p(如 0.1)会大幅减少多样性,增加确定性。

  • frequency_penaltypresence_penalty:影响重复用词,可以间接让 Agent 的思考更加简洁或避免重复调用同一个工具。

如何控制 Agent 的行为

  • 对于需要精确工具调用(如转账),我通常设置 temperature=0,确保 Agent 稳定执行。

  • 对于创意类或需要多步探索的任务,我会设置 temperature=0.3-0.5,并配合 early_stopping_method 来容忍一定的不确定性。

  • 还可以使用 LangChain 的 LLMChainmodel_kwargs 参数,或者在初始化 LLM 时统一配置。

✅ 个人经验:Agent 的行为不是单靠温度控制的,工具的描述清晰度和 Prompt 中的示例对确定性的影响更大。即使温度设为 0,如果工具描述模糊,Agent 还是会“不知所措”。我倾向于先用低温调试,确认工具和 Prompt 无误后,再按需提高温度。


⛔ 如何给 Agent 设定一个“停止词”,一旦模型输出该词就提前结束?

停止词(Stop Token)可以让 LLM 在生成过程中遇到特定字符串时立即停止。在 Agent 场景中,可以用来实现自定义的终止逻辑。

方法:在 LLM 初始化时设置 stop 参数

LangChain 的大多数 LLM 包装器都支持传入 stop 序列。例如:

from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-4",
    temperature=0,
    stop=["\nObservation:", "Final Answer:"]  # 提前停止点
)

但是,对于 Agent,我们需要的是控制推理循环的停止,而不是在单次生成中截断。更有效的方法是使用 Agent 的自定义输出解析器 或 Prompt 指令。

实际应用

  1. 利用 Agent 自身的格式 在 ReAct Agent 中,Final Answer: 就是天然的停止词。你可以自定义这个标记。例如,让 Agent 在遇到特殊条件时输出 STOP,然后在 AgentOutputParser 中识别这个标记并抛出 AgentFinish 异常,从而跳出循环。

  2. 在 Prompt 中预设提前终止规则

在 Agent 的 Prompt 中加入:

如果你认为用户的问题无法回答,或者你已经收集了足够信息,请输出 "Final Answer: 抱歉,我无法回答这个问题。"

这样 Agent 自然会在合适的时候终止。

  1. 使用 LangChain 的 EarlyStopping 机制 AgentExecutorearly_stopping_method 可以配合 max_iterations 使用,但这并非基于内容。

  2. 自定义回调中断 在 on_agent_actionon_llm_new_token 回调中,检查 LLM 的输出是否包含自定义停止词,如果包含则抛出异常提前结束。

✅ 实践:我很少直接设置 stop 参数来终止 Agent,因为这可能会在 LLM 生成过程中截断关键的 Action Input。更稳妥的方式是通过 Prompt 设计让 Agent 自己输出 Final Answer,或者用回调在检测到异常时主动终止。


📜 你如何向 Agent 传入系统指令(比如“你是友好的助手,永远不说粗话”)?

向 Agent 传入系统指令有三种常见方式,取决于你使用的 Agent 类型和 Prompt 结构。

方法一:在 Agent 的 Prompt 模板中直接添加

大多数 Agent 的 Prompt 都包含一个前缀(Prefix),你可以在这里植入系统指令。例如 ReAct Agent:

from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub

prompt = hub.pull("hwchase17/react")
# 修改 prompt 的 template,在开头加上系统指令
prompt.template = "你是友好的助手,永远不说粗话。\n\n" + prompt.template

对于 OpenAI Functions Agent,同样可以修改系统消息:

from langchain.agents import create_openai_functions_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是友好的助手,永远不说粗话。"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

方法二:使用 extra_prompt_messages 参数(部分 Agent 支持)

某些 Agent 的初始化方法接受额外的消息,可以方便地追加系统指令。

方法三:在 Memory 中注入系统消息

ConversationBufferMemory 中预先添加一条 SystemMessage,这样每次记忆加载时都会带上这条指令。

方法四:通过 AgentExecutor 的 prefix 参数(已废弃)

早期版本有 prefix,现在更推荐直接修改 Prompt。

✅ 最佳实践:我通常直接修改 Prompt 模板,因为这最灵活。如果需要在运行时根据不同用户或场景动态调整系统指令,我会在构建链之前,通过字符串替换或模板渲染来实现。


🔑 在 Agent 中,如何控制它可以使用的工具列表?是否可以根据用户权限动态调整?

控制工具列表是 Agent 安全性和灵活性的关键。

静态控制:在初始化时传入工具列表

创建 Agent 时,tools 参数就是最终的工具列表。例如:

tools = [search, calculator, weather]
agent = create_openai_functions_agent(llm, tools, prompt)

动态控制:根据上下文或用户权限在运行时调整

  1. 每次调用 AgentExecutor.run() 时传入不同的工具 虽然 AgentExecutor 的工具列表在初始化时固定,但你可以创建多个 AgentExecutor 实例,或者在自定义循环中动态选择工具。更灵活的方式是使用自定义的 Agent 类,它可以根据输入动态生成 Prompt 中的工具描述。

  2. 在 Prompt 中动态插入工具列表

不依赖 LangChain 的自动工具注入,而是自己构建 Prompt,根据用户权限动态拼接工具描述。例如:

def build_prompt(user):
    allowed_tools = get_tools_for_user(user)
    tool_descriptions = "\n".join([f"- {t.name}: {t.description}" for t in allowed_tools])
    return f"你可以使用以下工具:\n{tool_descriptions}\n\n{base_instructions}"
  1. 使用自定义 Agent 类 继承 BaseAgentLLMSingleActionAgent,重写 plan 方法,在其中根据用户信息过滤可用工具,并生成相应的 Prompt。这需要更多的代码,但可以实现最精细的控制。

  2. 工具内部的权限检查

即使工具被列出,也可以在工具函数内部进行权限校验,如果用户无权调用则返回错误信息。这样做的好处是简化 Agent 逻辑,但 Agent 可能因为反复调用无权工具而浪费步数。

✅ 实践:对于多租户应用,我通常采用“动态构建 Prompt”的方式。在用户请求进入时,根据其角色从工具注册表筛选出可用工具,然后渲染 Prompt,创建 AgentExecutor 实例(可以使用缓存来避免重复创建)。这样从源头限制了 Agent 的能力。


❌ 当 Agent 出错(如选择了不存在的工具),LangChain 有内建的错误处理吗?

LangChain 的 AgentExecutor 提供了一些内建的错误处理机制,但并非万能。

内建错误处理

  1. 解析错误(Output Parsing Error) 当 LLM 的输出不符合预期格式(例如 ReAct 中缺少 Action:)时,会抛出 OutputParserExceptionAgentExecutor 默认会捕获此异常,并将错误信息追加到 Prompt 中,要求 LLM 重新输出。这通过 handle_parsing_errors=True 参数控制。你也可以传入一个自定义的错误处理函数。

  2. 工具不存在 如果 LLM 调用了一个不在工具列表中的工具,AgentExecutor 会抛出一个异常,但不会自动修复。默认情况下,这个异常会直接传递给调用者。你可以通过设置 handle_parsing_errors 或自定义错误处理来捕获,并向 LLM 反馈“工具 X 不存在,请从可用工具中选择”。

  3. 工具执行错误

工具内部的异常不会被 AgentExecutor 自动捕获,而是会中断整个流程。为了避免这个问题,你应该在工具函数内部做好异常处理,并将错误信息作为 Observation 返回给 Agent,而不是抛出异常。例如:

def my_tool(input: str) -> str:
    try:
        # 执行操作
        return "成功"
    except Exception as e:
        return f"工具执行失败:{e}"

增强错误处理的方式

  • 使用 CallbackHandler 监听 on_tool_error 事件,记录日志或触发告警。

  • 在自定义 Agent 的 Prompt 中加入指令:“如果你尝试了一个不存在的工具,请根据错误信息选择正确的工具”。

  • 利用 LangSmith 追踪错误。

✅ 经验:对于工具不存在的问题,最有效的办法是确保 Prompt 中的工具描述与工具名称严格一致。我通常会在工具注册时自动生成描述,避免手动编写导致的不匹配。同时,所有工具都要做健壮的异常捕获,绝不让工具内部的错误“击穿”到 AgentExecutor。


📋 你如何记录 Agent 的每一步行动和观察,用于后续调试和监控?

记录 Agent 的每一步是调试复杂问题、监控成本和性能的基础。LangChain 提供了多种方式:

开启 verbose=True

最简单的方法,直接在控制台打印每一步的详细日志。适合本地开发。

使用 LangSmith

最强大的官方工具,自动追踪所有 Agent 运行,包括 Prompt、LLM 调用、工具调用和返回值。你可以在 UI 上回放每一步,非常适合团队协作和线上调试。

自定义 Callback 处理器

通过实现 BaseCallbackHandlerAsyncCallbackHandler,你可以精确控制日志的格式和存储位置。

class AgentLogger(BaseCallbackHandler):
    def on_agent_action(self, action, **kwargs):
        logger.info(f"Step {self.step}: Action - {action.tool}({action.tool_input})")
    def on_tool_end(self, output, **kwargs):
        logger.info(f"Observation: {output}")
    def on_agent_finish(self, finish, **kwargs):
        logger.info(f"Final Answer: {finish.return_values}")

保存到数据库或文件

在回调中,将每一步的 run_id, timestamp, action, observation 写入数据库(如 PostgreSQL)或日志文件,后续可以分析 Agent 的行为模式。

集成外部监控平台

将日志发送到 Prometheus、Grafana 或 ELK,实时监控 Agent 的性能和错误率。

✅ 我常用的组合:开发阶段使用 verbose,测试环境使用 LangSmith,生产环境使用自定义回调将关键事件写入结构化日志并发送到监控系统。特别注意要记录 session_iduser_id,以便追踪单个用户会话。


🤖 什么是 AgentExecutor?它在 Agent 外面包了一层做什么?

AgentExecutor 是 LangChain 中负责运行 Agent 的“执行引擎”。它把 Agent(负责思考)和工具(负责行动)连接起来,并管理整个 ReAct 循环。

AgentExecutor 的主要职责

  1. 循环控制:不断调用 Agent 的 plan 方法,直到 Agent 输出 AgentFinish 或达到最大步数。

  2. 工具执行:解析 Agent 返回的 AgentAction,调用对应的工具,并收集结果(Observation)。

  3. 错误处理:捕获输出解析错误,并可以选择性地重试或返回错误信息。

  4. 状态管理:维护 intermediate_steps(即 scratchpad),将所有 Thought/Action/Observation 记录并传递给下一轮。

  5. 早停策略:当达到 max_iterations 时,根据 early_stopping_method 决定是抛出异常还是生成最终答案。

  6. 回调触发:在合适的时机触发回调(如 on_agent_action, on_tool_end),方便外部监控。

简单说:AgentExecutor = Agent的运行时环境。

agent_executor = AgentExecutor(
    agent=agent,        # 决策核心
    tools=tools,        # 工具集
    max_iterations=5,
    early_stopping_method="generate",
    verbose=True,
    callbacks=[my_handler]
)

✅ 理解:在没有 AgentExecutor 之前,你需要自己写循环和错误处理,非常繁琐。AgentExecutor 把这些通用逻辑封装起来,让你能专注于 Agent 的 Prompt 和工具设计。它是 Agent 框架中最不可或缺的部分。


⏹️ 解释 Agent 的“early_stopping_method”参数,它可以设为哪些值?

early_stopping_methodAgentExecutor 的一个参数,决定当 Agent 达到 max_iterations 但尚未输出 Final Answer 时的行为。

可选值:

  • "force"(默认):直接返回一个 AgentFinish 对象,其 return_values 为空字典。这意味着 Agent 没有给出任何答案,通常会导致程序抛出异常或返回 None。在生产环境中,这会给用户带来不好的体验。

  • "generate":在超过最大步数后,额外调用一次 LLM,要求它根据当前已收集的所有信息(所有的 Thought/Action/Observation)强制生成一个最终答案。这次调用会使用一个特殊的 Prompt,明确告诉 LLM “你已经完成了最大步数,现在必须给出一个回答,哪怕基于不完整的信息”。这样至少能返回一个结果给用户,而不是一个错误。

选择建议:

  • 如果你希望 Agent 严格按预设步数执行,且对未完成的任务零容忍(例如金融交易),用 "force",然后在外层捕获异常并做补偿处理。

  • 对于大多数对话式或信息查询类应用,强烈推荐 "generate"。它能显著提升用户体验,避免因偶发的死循环或复杂问题导致“无响应”。

✅ 实际效果:我曾在客服 Agent 中使用 "force",结果用户经常在复杂问题上得到空白回复。改用 "generate" 后,虽然偶尔答案不够精确,但至少给出了一个尝试性的解答,用户满意度明显上升。


🌐 在多 Agent 场景下,LangChain 有没有提供原生支持?如果没有,你如何实现?

LangChain 本身没有像 AutoGen 或 CrewAI 那样开箱即用的多 Agent 协作框架,但它提供了构建这类系统所需的底层组件(自定义 Agent、工具、Memory、Prompt 等)。

实现多 Agent 的常用模式

  1. 分层 Agent (Hierarchical Agent) 一个“主管” Agent 负责接收任务,然后决定分配给哪个“专家” Agent,并汇总它们的结果。主管 Agent 本身也是一个 Agent,它的工具是一组子 Agent(包装成 Tool)。例如:
def legal_expert(query: str) -> str:
    return legal_agent_executor.run(query)

def medical_expert(query: str) -> str:
    return medical_agent_executor.run(query)

tools = [
    Tool(name="LegalExpert", func=legal_expert, description="处理法律问题"),
    Tool(name="MedicalExpert", func=medical_expert, description="处理医疗问题"),
]
manager_agent = create_openai_functions_agent(llm, tools, prompt)
  1. 顺序执行 (Sequential) 将多个 Agent 串行,前一个 Agent 的输出作为后一个的输入。这可以通过 LangChain 的 SequentialChain 或自定义脚本实现。

  2. 辩论/对话模式

让两个 Agent 以对话形式交流,直至达成一致。可以自定义一个“辩论”环境,利用 Memory 共享对话历史。

  1. 自定义调度器

编写一个调度循环,维护一个任务队列和一组 Agent 工作器,动态分配任务。

关键设计点

  • 记忆共享:多个 Agent 可能需要访问共同的记忆库(如用户信息)。可以使用同一个 RedisChatMessageHistory 或向量库来实现。

  • 工具隔离:每个 Agent 只暴露其需要的工具,避免越权操作。

  • 通信协议:定义清晰的输入输出格式,比如要求 Agent 的 Final Answer 必须包含结构化摘要。

  • 错误传播:子 Agent 的错误应被捕获并转化为可读信息,反馈给上层 Agent。

✅ 我的实践:如果需求简单,我会手动搭建分层 Agent。对于复杂协作,我更倾向于使用专门的多 Agent 框架(如 AutoGen),然后将其与 LangChain 的工具和检索能力结合。LangChain 的优势在于丰富的组件生态,但在多 Agent 调度方面仍需大量定制工作。未来 LangChain 可能会推出更高级的多 Agent 抽象,但目前还是需要自己“造轮子”或借用其他工具。