1. 从LangChain到LangGraph:为什么我们需要“图”来构建Agent?

如果你和我一样,在过去一两年里折腾过AI应用开发,尤其是想搞点能自主决策、有记忆、能执行复杂任务的智能体(AI Agent),那你大概率绕不开LangChain。LangChain确实是个好框架,它把大语言模型(LLM)和工具、记忆、提示词这些组件像乐高一样拼起来,让构建应用变得直观。但当你真的想做一个能处理多步骤、有状态、带循环和条件分支的复杂Agent时,用LangChain的 AgentExecutor 可能会感觉有点“力不从心”。代码里充斥着各种 if-else 来控制流程,状态管理变得混乱,调试起来像在走迷宫。

这就是LangGraph登场的原因。它不是要取代LangChain,而是它的一个超集,或者说,是一个更强大的“大脑”。LangGraph的核心思想非常直观: 用“图”(Graph)来定义Agent的工作流 。在这个图里,节点(Node)代表一个执行单元(比如调用一次LLM、执行一个工具),边(Edge)代表执行流的方向。这听起来有点抽象,但想象一下你规划一个项目:先调研(节点A),根据调研结果决定是写方案(节点B)还是直接开发(节点C),开发完要测试(节点D),测试不通过就返回修改。这就是一个典型的、带条件分支的工作流图。

LangGraph把这种思维模型代码化了。它让你能清晰地定义:

  1. 状态(State) :一个贯穿整个工作流的共享数据字典。比如当前的用户问题、已收集的信息、历史对话、工具执行结果等,都放在这里。
  2. 节点(Nodes) :每个节点是一个函数,它读取状态,执行操作(问LLM、查数据库、调API),然后更新状态。
  3. 边(Edges) :决定下一个执行哪个节点。可以是固定的(“总是执行节点B”),也可以是条件式的(“如果状态里的 quality 字段为‘high’,就去节点C,否则去节点D”)。

这种图结构带来的好处是革命性的:

  • 高可维护性 :工作流一目了然,不再是面条代码。新同事也能快速看懂这个Agent是怎么“思考”和“行动”的。
  • 复杂逻辑支持 :轻松实现循环(比如让Agent反复思考直到满意)、并行执行(同时调用多个工具)、条件分支,这些都是构建高级Agent的刚需。
  • 持久化与检查点 :因为状态是明确的,你可以随时把整个Agent的状态保存下来,中断后下次能从断点恢复。这对于运行时间长的任务(如自动化研究、长篇内容生成)至关重要。
  • 更好的可观测性 :你可以清晰地追踪到执行流经过了哪些节点,每个节点输入/输出了什么,调试和优化效率大大提升。

所以,简单来说, LangChain帮你组装了Agent的“身体”(工具、记忆),而LangGraph为你设计了Agent的“神经系统”和“决策流程” 。当你需要Agent不仅能回答问题,还能像人一样规划、执行、反思并完成一个复杂目标时,LangGraph几乎是目前最优雅和强大的选择。

1.1 核心概念快速解析:State, Node, Edge

在深入代码之前,我们得把LangGraph的三个核心概念掰扯清楚,这关系到你能否理解后续的所有设计。

1. State(状态) 这是LangGraph工作流的“共享内存”。它通常是一个Python字典( TypedDict )或者Pydantic模型。所有节点都读取和修改这个状态。设计一个好的状态结构是成功的第一步。

  • 关键原则 :状态应该包含工作流完成目标所需的所有信息。常见的字段包括:
    • messages : 一个消息列表,记录与LLM的对话历史。这是LangChain/LangGraph生态的标准做法。
    • question : 用户最初的问题。
    • intermediate_steps : 存放工具调用及其结果的历史记录。
    • research_findings : 专门存放研究结果。
    • final_answer : 存放最终答案。
    • 任何你自定义的中间数据。

2. Node(节点) 节点是实际干活的单元。每个节点是一个函数(或可调用对象),它接收当前 状态 作为唯一参数,并返回一个对状态的 更新 (也是一个字典,包含要修改的字段和值)。

def research_node(state: State):
    # 1. 从state中取出需要的信息,例如用户问题
    question = state[“question”]
    # 2. 执行核心逻辑,例如调用一个网络搜索工具
    findings = web_search_tool.run(question)
    # 3. 返回一个更新字典,LangGraph会自动将其合并到总状态中
    return {“research_findings”: findings}

LangGraph会调用这个函数,并将返回的 {“research_findings”: findings} 合并到全局状态里。 节点设计应遵循“单一职责”原则 ,一个节点最好只做一件事。

3. Edge(边) 边决定了工作流的走向。分为两种:

  • 普通边( add_edge :无条件地从源节点指向目标节点。执行完源节点后,必执行目标节点。
  • 条件边( add_conditional_edges :根据当前状态的值,动态决定下一个节点。这是实现分支和循环的关键。你需要定义一个“路由函数”来决定下一站。

理解了这三个概念,你就能在脑子里画出Agent的工作流程图了。接下来,我们动手搭建环境,准备开始“画图”。

2. 环境搭建与基础配置:从零开始一个LangGraph项目

理论说再多不如跑一行代码。让我们从一个干净的环境开始,一步步搭建一个可用的LangGraph开发环境。我强烈建议使用 conda venv 来管理Python环境,避免包冲突。

2.1 创建虚拟环境与安装依赖

首先,确保你的Python版本在3.8以上。然后,我们创建一个新的虚拟环境并安装核心依赖。

# 使用conda(推荐,方便管理不同版本的Python和CUDA)
conda create -n langgraph-agent python=3.10 -y
conda activate langgraph-agent

# 或者使用venv
python -m venv venv
# Windows: .\venv\Scripts\activate
# Mac/Linux: source venv/bin/activate

接下来安装LangGraph和LangChain。由于我们要构建Agent,通常需要连接LLM(如OpenAI GPT、 Anthropic Claude或本地模型)和可能的外部工具(如搜索、计算)。

# 安装LangGraph核心库及LangChain集成
pip install langgraph langchain langchain-openai langchain-community

# 如果你打算使用OpenAI的模型,还需要安装openai库并设置API密钥
# pip install openai
# 然后在代码中设置环境变量 OPENAI_API_KEY='your-key'

依赖选型说明

  • langgraph : 核心框架。
  • langchain : 提供了大量现成的组件(LLM封装、工具、记忆体),与LangGraph无缝集成。虽然LangGraph可以独立使用,但结合LangChain生态效率最高。
  • langchain-openai : 官方维护的OpenAI集成,比通用的 langchain 包里的更稳定。
  • langchain-community : 包含大量第三方工具和集成,比如网络搜索、维基百科查询等。

注意 :依赖管理是项目稳定的基石。建议使用 requirements.txt pyproject.toml 记录所有依赖及其版本。特别是 langchain langgraph 更新较快,锁定版本(如 langgraph==0.0.40 )可以避免未来因API变更导致的意外错误。

2.2 初始化LLM与基础工具

安装好后,我们写一个简单的脚本来测试环境,并初始化一些核心组件。这里以OpenAI GPT-4为例,你也可以替换为其他兼容的模型。

# config.py
import os
from langchain_openai import ChatOpenAI
from langchain_community.tools import DuckDuckGoSearchRun

# 设置你的OpenAI API Key。更安全的方式是从环境变量读取。
os.environ[“OPENAI_API_KEY”] = “your-api-key-here”

def get_llm(model_name=“gpt-4-turbo-preview”, temperature=0):
    “”“初始化LLM。
    Args:
        model_name: 模型名称,如‘gpt-4-turbo-preview’, ‘gpt-3.5-turbo’
        temperature: 创造性,0表示最确定,值越高越随机。
    ”“”
    return ChatOpenAI(model=model_name, temperature=temperature)

def get_search_tool():
    “”“初始化一个网络搜索工具。这里使用DuckDuckGo,无需API Key。
    注意:此工具可能受网络环境影响,生产环境建议使用更稳定的SerpAPI等。
    ”“”
    return DuckDuckGoSearchRun()

# 后续可以在这里添加更多工具,如计算器、数据库查询等。

这个配置文件做了两件事:

  1. 创建了一个 get_llm 函数,用于获取配置好的LLM实例。将 temperature 设为0,是为了让Agent的决策更稳定、可复现,这在调试阶段非常重要。
  2. 创建了一个 get_search_tool 函数,返回一个搜索工具实例。 DuckDuckGoSearchRun 是LangChain社区提供的一个免费搜索工具,对于学习和原型开发足够了。

实操心得 :在开发初期,我习惯将LLM的 temperature 设为0,以确保相同输入得到相同输出,方便调试逻辑。等核心流程跑通后,再根据需求调整 temperature 来增加创造性。另外,对于搜索工具,免费工具可能不稳定或有速率限制。在构建面向生产环境的Agent时,务必考虑使用付费的、可靠的API(如SerpAPI、Google Custom Search JSON API),并做好错误处理和重试机制。

3. 实战:构建一个具备“规划-执行-反思”循环的研究型Agent

现在,我们进入最核心的部分:用LangGraph构建一个真正能用的Agent。我们将打造一个“研究型Agent”,它的目标是: 回答一个复杂的、需要多步骤网络调研的开放性问题 。例如,“对比一下特斯拉Model 3和比亚迪汉EV在2024年的市场表现和核心技术差异”。

这个Agent的工作流将模拟人类研究员的思考过程:

  1. 规划 :分析问题,拆解成几个需要搜索的子问题。
  2. 执行 :并行或串行地搜索这些子问题。
  3. 反思 :评估收集到的信息是否足够、准确,是否需要进一步搜索或修正。
  4. 合成 :将所有信息整合成一份完整的、结构化的答案。

我们将把这个流程实现为一个LangGraph图。

3.1 定义状态与初始化图结构

首先,我们需要定义这个工作流的状态。我们将使用Pydantic来定义,这样有类型提示,更清晰。

# agent_state.py
from typing import TypedDict, List, Annotated
from langgraph.graph.message import add_messages
import operator

class AgentState(TypedDict):
    “”“研究型Agent的状态定义。
    关键字段:
        messages: 与LLM的对话历史,LangGraph内置了‘add_messages’操作符来简化列表追加。
        question: 用户的原始问题。
        research_plan: 由规划节点生成的调研计划(一个子问题列表)。
        research_findings: 一个字典,key是子问题,value是搜索到的答案。
        needs_refinement: 一个布尔值或列表,标记哪些子问题的答案需要进一步研究。
        final_answer: 最终合成的答案。
    ”“”
    messages: Annotated[List, add_messages] # 特殊注解,用于自动追加消息
    question: str
    research_plan: List[str]
    research_findings: dict
    needs_refinement: List[str]
    final_answer: str

这里用到了 Annotated add_messages 。这是LangGraph的一个语法糖,它允许你声明一个字段(这里是 messages 列表),然后指定一个“缩减器”(reducer)。 add_messages 是一个特殊的缩减器,它会自动将新消息追加到现有的消息列表中,而不是覆盖它。这对于维护对话历史非常方便。

接下来,我们初始化一个 StateGraph ,并传入我们定义的状态结构。

# research_agent.py
from langgraph.graph import StateGraph, END
from agent_state import AgentState

# 初始化工作流图,并指定状态结构
workflow = StateGraph(AgentState)

StateGraph 是LangGraph的核心类, END 是一个特殊的节点,表示工作流终止。

3.2 实现核心节点:规划、搜索、反思、合成

现在,我们来创建四个核心节点函数。

节点1:规划节点( plan_node 这个节点负责接收用户问题,并让LLM将其拆解成一个具体的调研计划(子问题列表)。

from config import get_llm
from langchain_core.prompts import ChatPromptTemplate

llm = get_llm()

def plan_node(state: AgentState):
    “”“规划节点:分析问题,生成调研子问题列表。”“”
    question = state[“question”]
    
    # 构建规划提示词
    planner_prompt = ChatPromptTemplate.from_messages([
        (“system”, “你是一个资深研究助理。请将用户的复杂问题拆解成3-5个具体的、可以通过网络搜索找到答案的子问题。请直接输出子问题列表,每行一个,不要有任何额外解释。”),
        (“human”, “用户的问题是:{question}”)
    ])
    
    # 调用LLM
    plan_chain = planner_prompt | llm
    response = plan_chain.invoke({“question”: question})
    
    # 解析LLM的回复,假设它返回一个用换行符分隔的列表
    sub_questions = [q.strip() for q in response.content.split(‘\n’) if q.strip()]
    
    # 更新状态:存入调研计划,并初始化findings字典
    updates = {
        “research_plan”: sub_questions,
        “research_findings”: {}, # 初始化空字典
        “needs_refinement”: [] # 初始化空列表
    }
    # 也可以将LLM的回复作为一条消息记录到历史中,方便后续追溯
    # new_messages = [response]
    # updates.update({“messages”: new_messages}) # 注意:如果用了add_messages注解,这里追加方式不同
    
    return updates

节点2:搜索节点( search_node 这个节点将并发地搜索调研计划中的所有子问题。这里为了简化,我们先实现串行搜索,但会介绍并发思路。

from config import get_search_tool
import asyncio # 为后续并发做准备

search_tool = get_search_tool()

def search_node(state: AgentState):
    “”“搜索节点:针对research_plan中的每个子问题进行搜索,并将结果存入research_findings。”“”
    findings = state.get(“research_findings”, {})
    plan = state[“research_plan”]
    
    for sub_q in plan:
        if sub_q not in findings: # 只搜索尚未有结果的问题
            print(f“正在搜索: {sub_q}”)
            try:
                # 执行搜索
                result = search_tool.run(sub_q)
                findings[sub_q] = result
            except Exception as e:
                findings[sub_q] = f“搜索失败: {str(e)}”
    
    return {“research_findings”: findings}

实现并发搜索 :在实际生产中,串行搜索太慢。我们可以利用 asyncio 来并发。但需要注意,很多LangChain工具是同步的。我们可以使用 langchain async 支持,或者将任务提交到线程池。一个更LangGraph的方式是使用 StateGraph add_node 支持异步函数,并在节点内使用 asyncio.gather 。这里先给出一个高级思路,后续可以优化。

节点3:反思节点( reflect_node 这是让Agent变“聪明”的关键。它评估已收集的信息,判断是否足够、准确,是否需要进一步搜索(细化)。

def reflect_node(state: AgentState):
    “”“反思节点:评估已收集的研究发现,判断是否需要进一步搜索或修正某些子问题。”“”
    question = state[“question”]
    findings = state[“research_findings”]
    plan = state[“research_plan”]
    
    # 构建反思提示词
    reflection_prompt = ChatPromptTemplate.from_messages([
        (“system”, “你是一个严格的质量检查员。请根据用户的原始问题和已收集的信息,判断哪些子问题的答案还不够充分、准确或相关。请只输出那些需要进一步搜索的子问题原文,每行一个。如果所有信息都已足够,请输出‘SUFFICIENT’。”),
        (“human”, “””
        原始问题:{question}
        
        调研计划:
        {plan}
        
        已收集信息:
        {findings}
        
        请指出需要进一步调研的子问题:
        “””)
    ])
    
    reflection_chain = reflection_prompt | llm
    response = reflection_chain.invoke({
        “question”: question,
        “plan”: ‘\n’.join(plan),
        “findings”: ‘\n’.join([f“Q: {k}\nA: {v}” for k, v in findings.items()])
    })
    
    refinement_list = []
    if “SUFFICIENT” not in response.content:
        refinement_list = [q.strip() for q in response.content.split(‘\n’) if q.strip() and q.strip() in plan] # 确保是原计划中的问题
    
    return {“needs_refinement”: refinement_list}

节点4:合成节点( synthesize_node 当信息被判定为足够后,这个节点负责将所有零散的信息整合成一份连贯、结构化的最终答案。

def synthesize_node(state: AgentState):
    “”“合成节点:根据所有研究发现,生成最终答案。”“”
    question = state[“question”]
    findings = state[“research_findings”]
    
    synthesis_prompt = ChatPromptTemplate.from_messages([
        (“system”, “你是一位专业的报告撰写人。请基于以下所有调研结果,为用户的原始问题撰写一份全面、结构清晰、客观的答案。答案应包含引言、主体(分点论述)和总结。”),
        (“human”, “””
        原始问题:{question}
        
        所有调研结果:
        {findings}
        
        请开始撰写最终答案:
        “””)
    ])
    
    synthesis_chain = synthesis_prompt | llm
    response = synthesis_chain.invoke({
        “question”: question,
        “findings”: ‘\n’.join([f“### {q}\n{a}” for q, a in findings.items()])
    })
    
    return {“final_answer”: response.content}

3.3 组装工作流:定义节点与边

有了节点函数,我们现在把它们添加到图中,并连接起来。

# 将节点添加到图中
workflow.add_node(“planner”, plan_node)
workflow.add_node(“researcher”, search_node)
workflow.add_node(“reflector”, reflect_node)
workflow.add_node(“synthesizer”, synthesize_node)

# 设置入口点:从规划开始
workflow.set_entry_point(“planner”)

# 添加边,定义基础流程:规划 -> 搜索 -> 反思
workflow.add_edge(“planner”, “researcher”)
workflow.add_edge(“researcher”, “reflector”)

# 添加条件边:反思后的路由
# 这是关键!根据‘needs_refinement’列表是否为空,决定是继续搜索还是合成答案。
def decide_after_reflection(state: AgentState):
    “”“路由函数:根据反思结果决定下一步。”“”
    if state.get(“needs_refinement”): # 如果列表不为空,需要继续研究
        return “researcher” # 跳回搜索节点
    else: # 信息已足够
        return “synthesizer” # 前往合成节点

workflow.add_conditional_edges(
    “reflector”, # 源节点
    decide_after_reflection, # 路由函数
    {“researcher”: “researcher”, “synthesizer”: “synthesizer”} # 可能的目的地映射
)

# 从合成节点到结束
workflow.add_edge(“synthesizer”, END)

# 编译图,得到可执行的应用
app = workflow.compile()

这段代码构建了一个完整的、带循环的工作流:

  1. planner 开始。
  2. 然后到 researcher 进行搜索。
  3. 再到 reflector 进行反思。
  4. reflector 之后,由 decide_after_reflection 函数决定下一步:如果 needs_refinement 列表里有内容,就返回 researcher 节点再次搜索(注意,搜索节点会基于已有 findings 跳过已完成的子问题,只搜索需要细化的);如果列表为空,就前往 synthesizer
  5. 合成答案后,流程到达 END ,工作流结束。

这个循环(研究 -> 反思 -> 再研究)是构建“深思熟虑”型Agent的核心模式,LangGraph通过条件边非常优雅地实现了它。

3.4 运行与调试你的第一个Agent

现在,让我们运行这个Agent,看看它如何工作。

# 定义初始状态
initial_state: AgentState = {
    “messages”: [], # 对话历史,初始为空
    “question”: “对比一下特斯拉Model 3和比亚迪汉EV在2024年的市场表现和核心技术差异”,
    “research_plan”: [],
    “research_findings”: {},
    “needs_refinement”: [],
    “final_answer”: “”
}

# 运行图应用
final_state = app.invoke(initial_state)

print(“\n=== 最终答案 ===\n”)
print(final_state[“final_answer”])
print(“\n=== 调研过程摘要 ===\n”)
for q, a in final_state[“research_findings”].items():
    print(f“问题: {q}”)
    print(f“答案摘要: {a[:200]}...”) # 只打印前200字符
    print(“-” * 50)

运行这段代码,你会看到控制台输出Agent的思考过程:先规划出几个子问题,然后依次搜索,反思,可能再搜索,最后合成答案。通过检查 final_state ,你可以看到完整的 research_findings 字典和 final_answer

注意事项 :首次运行可能会比较慢,因为要调用多次LLM和网络搜索。你可以通过以下方式优化体验:

  1. 使用更快的模型 :在开发调试时,可以将 get_llm 中的模型换成 gpt-3.5-turbo 以节省成本和时间。
  2. 模拟工具 :在测试逻辑时,可以创建一个“模拟搜索工具”,直接返回预设的文本,避免真实网络请求。这能极大加快迭代速度。
  3. 利用LangGraph Studio :LangGraph提供了一个可视化开发工具 LangGraph Studio ,可以让你以图形界面查看工作流的执行过程、检查每个节点的输入输出,是调试神器。可以通过 pip install langgraph-cli 安装,然后运行 langgraph dev 来启动本地服务。

4. 高级技巧与生产级优化:让你的Agent更可靠、更强大

基础Agent跑通了,但离“高可用”还有距离。一个生产级的Agent必须具备健壮性、效率和可观测性。下面分享几个我在实际项目中总结的关键技巧。

4.1 错误处理与状态回退

在网络调用、工具执行中,错误是常态。一个健壮的Agent不能因为一个工具调用失败就整个崩溃。

策略1:节点级错误处理 在每个节点函数内部使用 try-except ,并将错误信息作为正常结果的一部分存入状态,供后续节点处理。

def robust_search_node(state: AgentState):
    findings = state.get(“research_findings”, {})
    plan = state[“research_plan”]
    errors = []
    
    for sub_q in plan:
        if sub_q not in findings:
            try:
                result = search_tool.run(sub_q)
                findings[sub_q] = result
            except Exception as e:
                error_msg = f“搜索‘{sub_q}’时出错: {str(e)}”
                findings[sub_q] = error_msg
                errors.append(error_msg) # 记录错误
                # 可以选择重试、使用备用工具等
    
    updates = {“research_findings”: findings}
    if errors:
        updates[“errors”] = state.get(“errors”, []) + errors # 将错误累积到状态中
    return updates

策略2:使用LangGraph的 interrupt checkpointer 对于更严重的错误,你可能希望暂停整个工作流,等待人工干预或执行修复逻辑。LangGraph支持“中断”机制。你可以在节点中抛出一个特定的异常(如 langgraph.graph.interrupt ),然后在编译图时配置一个“检查点”管理器,它允许你保存当前状态,稍后从断点恢复。这对于处理长时间运行的任务和不可预知的错误非常有用。

4.2 实现真正的并行执行

我们之前的搜索节点是串行的。要并行化,需要将节点函数定义为 async ,并使用异步工具。

import asyncio
from langchain_community.tools import DuckDuckGoSearchRun
# 假设有异步版本的搜索工具,或者自己封装
# 这里以假想的async_search_tool为例

async def parallel_search_node(state: AgentState):
    plan = state[“research_plan”]
    findings = state.get(“research_findings”, {})
    
    # 找出所有需要搜索的新问题
    tasks_to_do = [q for q in plan if q not in findings]
    
    # 为每个问题创建异步任务
    tasks = [async_search_tool.arun(q) for q in tasks_to_do]
    
    # 并发执行所有任务
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # 处理结果
    for q, result in zip(tasks_to_do, results):
        if isinstance(result, Exception):
            findings[q] = f“搜索失败: {str(result)}”
        else:
            findings[q] = result
    
    return {“research_findings”: findings}

然后,在添加节点时使用 add_node ,它支持异步函数。同时,你需要在一个异步上下文中运行整个图( app.ainvoke )。

4.3 记忆与长期对话

我们的示例是单次任务。如果要构建一个能进行多轮对话的Agent(比如客服机器人),就需要持久化记忆。LangGraph与LangChain的记忆模块集成得很好。

核心思路 :将 messages 字段作为长期记忆。每一轮用户输入都被追加到 messages 中。在规划或合成节点,LLM可以看到完整的对话历史。此外,你还可以引入一个“记忆查询”节点,从向量数据库等外部存储中检索相关的历史信息,并将其注入到当前上下文中。

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# 假设有一个存储历史对话片段的向量库
vectorstore = Chroma(embedding_function=OpenAIEmbeddings())

def retrieve_memory_node(state: AgentState):
    “”“检索与当前问题相关的历史记忆。”“”
    current_question = state[“question”]
    # 从向量库中检索最相关的几条历史记录
    docs = vectorstore.similarity_search(current_question, k=3)
    relevant_memory = “\n”.join([doc.page_content for doc in docs])
    
    # 将检索到的记忆作为一个特殊的系统消息或字段加入状态
    new_memory_message = {“role”: “system”, “content”: f“相关历史信息:{relevant_memory}”}
    # 注意:这里需要根据你实际的消息列表格式来追加
    return {“retrieved_memory”: relevant_memory}

然后,在提示词模板中,加入 {retrieved_memory} 这个变量,让LLM在回答时参考这些历史信息。

4.4 可观测性与监控

当Agent部署到生产环境后,你需要知道它内部发生了什么。LangGraph提供了很好的钩子(Hooks)来监控。

from langgraph.graph import StateGraph

workflow = StateGraph(AgentState)

# ... 添加节点和边 ...

# 定义一个回调函数,在每次节点调用前后打印信息
def my_callback(node_name: str, inputs: dict, outputs: dict):
    print(f“节点 ‘{node_name}’ 开始执行...”)
    print(f“输入状态片段: {list(inputs.keys())}”)
    # 避免打印过长的内容
    # print(f“输出状态片段: {list(outputs.keys())}”)
    print(f“节点 ‘{node_name}’ 执行完毕。\n”)

# 编译应用时传入回调
app = workflow.compile(debug=True) # debug=True会打印更多信息
# 或者使用更精细的hooks配置
# from langgraph.checkpoint import MemorySaver
# app = workflow.compile(checkpointer=MemorySaver(), interrupt_before=[“reflector”])

对于更复杂的监控,你可以将节点的输入输出、执行时间、LLM的token消耗等信息发送到像Prometheus、Datadog这样的监控系统,或者存储到数据库供后续分析。

5. 避坑指南与常见问题排查

在开发和部署LangGraph Agent的过程中,我踩过不少坑。这里把最常见的问题和解决方案整理出来,希望能帮你节省时间。

5.1 状态更新不生效或覆盖

问题 :在节点函数里修改了状态字典,但发现后续节点读到的还是旧值。 原因与解决 :LangGraph要求节点函数 返回一个更新字典 ,而不是直接修改传入的 state 对象。这个更新字典应该只包含你 想要改变 的键值对。LangGraph会用这个字典和旧状态进行 浅合并 。如果你返回 {“research_findings”: new_findings} ,它会用 new_findings 完全替换掉旧的 research_findings 字段。如果你想追加到一个列表,需要使用 add_messages 这样的注解,或者手动处理。

# 错误做法(直接修改):
state[“research_findings”][“new_key”] = “value” # 可能不会生效
return state # 更糟,可能覆盖其他节点做的修改

# 正确做法(返回更新字典):
new_findings = state.get(“research_findings”, {}).copy() # 先复制
new_findings[“new_key”] = “value”
return {“research_findings”: new_findings} # 返回要更新的部分

5.2 条件边路由函数逻辑错误

问题 :工作流没有按预期进行分支或循环。 排查步骤

  1. 检查路由函数返回值 add_conditional_edges 中定义的路由函数,其返回值必须与 path_map 字典中的某个键 完全匹配 。例如,如果 path_map {“yes”: “node_a”, “no”: “node_b”} ,那么路由函数必须返回字符串 “yes” “no”
  2. 打印状态调试 :在路由函数开头打印 state ,确保你用来做判断的字段(如 needs_refinement )已经被正确设置,并且是你期望的数据类型(是空列表 [] 还是 None ?)。
  3. 使用LangGraph Studio :这是最直观的调试方式。在Studio里,你可以一步步执行,看到每个节点后的状态快照和边的走向。

5.3 LLM调用不稳定或超时

问题 :Agent在某个节点卡住,或者LLM返回了非预期的格式导致解析失败。 解决策略

  1. 增加重试与超时 :使用带有重试和超时机制的LLM封装。 langchain-openai ChatOpenAI 类支持 max_retries timeout 参数。
    llm = ChatOpenAI(model=“gpt-4”, temperature=0, max_retries=2, timeout=30)
    
  2. 强化提示词工程 :对于规划、反思等关键节点,LLM输出的格式必须稳定。在提示词中明确要求输出格式(如“请输出一个JSON列表”或“每行一个”),并在代码中做好防御性解析,比如使用 json.loads 并捕获异常,或者用正则表达式提取关键部分。
  3. 设置Fallback :对于关键工具(如搜索),准备一个备用工具。在主工具调用失败时,自动切换到备用工具。

5.4 工作流陷入无限循环

问题 :Agent在“研究-反思”循环中出不来。 原因 :通常是反思节点的逻辑有缺陷,或者状态没有正确更新,导致 needs_refinement 列表永远不为空。 解决方案

  1. 设置最大循环次数 :这是最有效的方法。在状态中引入一个计数器,如 iteration_count 。在每次进入循环的节点(如 researcher reflector )中将其加1。在路由函数中,检查该计数器是否超过阈值(如5次),如果超过,则强制路由到 synthesizer 或一个“失败处理”节点。
    def decide_after_reflection(state: AgentState):
        if state.get(“iteration_count”, 0) >= 5:
            return “synthesizer” # 或 “failure_handler”
        if state.get(“needs_refinement”):
            return “researcher”
        else:
            return “synthesizer”
    
  2. 改进反思逻辑 :让反思LLM不仅判断是否需要细化,还要给出理由。你可以将理由也记录到状态中,并在达到一定次数后,强制合成现有答案,并在答案中注明“部分信息可能因迭代限制未能进一步核实”。

5.5 性能优化与成本控制

问题 :Agent运行慢,且LLM API调用费用高。 优化建议

  1. 缓存LLM响应 :对于相同的输入,LLM的输出是确定的(当 temperature=0 时)。可以使用 langchain 的缓存功能(如 InMemoryCache , SQLiteCache )来缓存提示词-响应对,避免重复计算。
    from langchain.globals import set_llm_cache
    from langchain.cache import InMemoryCache
    set_llm_cache(InMemoryCache())
    
  2. 精简上下文 :传递给LLM的对话历史( messages )和工具结果可能很长,消耗大量token。定期总结或裁剪历史消息,只保留最相关的部分。
  3. 使用更便宜的模型 :在非核心节点(如初步规划、简单分类)使用 gpt-3.5-turbo ,只在需要深度思考或合成的节点使用 gpt-4
  4. 并行化 :如前所述,将可以并行的工具调用(如多个搜索)改为异步并发,能显著减少总耗时。

构建高可用的AI Agent是一个持续迭代的过程。LangGraph提供了强大的框架,但真正的挑战在于如何设计稳健的工作流、处理各种边界情况、优化性能和成本。从这个小型的“研究助手”开始,你可以逐步扩展它的能力,加入更多工具(数据库、API、代码解释器)、更复杂的决策逻辑,最终打造出能够自主处理复杂现实任务的智能体。记住,好的Agent不是一蹴而就的,它需要你在“设计-实现-测试-观察-优化”的循环中不断打磨。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐