【hello-agent】langgraph最小实践,问答助手

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

LangGraph

LangGraph 作为 LangChain 生态系统的重要扩展,代表了智能体框架设计的一个全新方向。与前面介绍的基于“对话”的框架(如 AutoGen 和 CAMEL)不同,LangGraph 将智能体的执行流程建模为一种状态机(State Machine),并将其表示为有向图(Directed Graph)。在这种范式中,图的节点(Nodes)代表一个具体的计算步骤(如调用 LLM、执行工具),而边(Edges)则定义了从一个节点到另一个节点的跳转逻辑。这种设计的革命性之处在于它天然支持循环,使得构建能够进行迭代、反思和自我修正的复杂智能体工作流变得前所未有的直观和简单。

要理解 LangGraph,我们需要先掌握它的三个基本构成要素。

首先,是全局状态(State)。整个图的执行过程都围绕一个共享的状态对象进行。这个状态通常被定义为一个 Python 的 TypedDict,它可以包含任何你需要追踪的信息,如对话历史、中间结果、迭代次数等。所有的节点都能读取和更新这个中心状态。

from typing import TypedDict, List

# 定义全局状态的数据结构
class AgentState(TypedDict):
    messages: List[str]      # 对话历史
    current_task: str        # 当前任务
    final_answer: str        # 最终答案
    # ... 任何其他需要追踪的状态

其次,是节点(Nodes)。每个节点都是一个接收当前状态作为输入、并返回一个更新后的状态作为输出的 Python 函数。节点是执行具体工作的单元。

# 定义一个“规划者”节点函数
def planner_node(state: AgentState) -> AgentState:
    """根据当前任务制定计划,并更新状态。"""
    current_task = state["current_task"]
    # ... 调用LLM生成计划 ...
    plan = f"为任务 '{current_task}' 生成的计划..."
    
    # 将新消息追加到状态中
    state["messages"].append(plan)
    return state

# 定义一个“执行者”节点函数
def executor_node(state: AgentState) -> AgentState:
    """执行最新计划,并更新状态。"""
    latest_plan = state["messages"][-1]
    # ... 执行计划并获得结果 ...
    result = f"执行计划 '{latest_plan}' 的结果..."
    
    state["messages"].append(result)
    return state

最后,是边(Edges)。边负责连接节点,定义工作流的方向。最简单的边是常规边,它指定了一个节点的输出总是流向另一个固定的节点。而 LangGraph 最强大的功能在于条件边(Conditional Edges)。它通过一个函数来判断当前的状态,然后动态地决定下一步应该跳转到哪个节点。这正是实现循环和复杂逻辑分支的关键。

def should_continue(state: AgentState) -> str:
    """条件函数:根据状态决定下一步路由。"""
    # 假设如果消息少于3条,则需要继续规划
    if len(state["messages"]) < 3:
        # 返回的字符串需要与添加条件边时定义的键匹配
        return "continue_to_planner"
    else:
        state["final_answer"] = state["messages"][-1]
        return "end_workflow"

在定义了状态、节点和边之后,我们可以像搭积木一样将它们组装成一个可执行的工作流。

6.5.2 三步问答助手

在理解了 LangGraph 的核心概念之后,我们将通过一个实战案例来巩固所学。我们将构建一个简化的问答对话助手,它会遵循一个清晰、固定的三步流程来回答用户的问题:

  1. 理解 (Understand):首先,分析用户的查询意图。
  2. 搜索 (Search):然后,模拟搜索与意图相关的信息。
  3. 回答 (Answer):最后,基于意图和搜索到的信息,生成最终答案。

这个案例将清晰地展示如何定义状态、创建节点以及将它们线性地连接成一个完整的工作流。我们将代码分解为四个核心步骤:定义状态、创建节点、构建图、以及运行应用。

(1)定义全局状态

首先,我们需要定义一个贯穿整个工作流的全局状态。这是一个共享的数据结构,它在图的每个节点之间传递,作为工作流的持久化上下文。 每个节点都可以读取该结构中的数据,并对其进行更新。

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class SearchState(TypedDict):
    messages: Annotated[list, add_messages]
    user_query: str      # 经过LLM理解后的用户需求总结
    search_query: str    # 优化后用于Tavily API的搜索查询
    search_results: str  # Tavily搜索返回的结果
    final_answer: str    # 最终生成的答案
    step: str            # 标记当前步骤

我们创建了 SearchState 这个 TypedDict,为状态对象定义了一个清晰的数据模式(Schema)。一个关键的设计是同时包含了 user_querysearch_query 字段。这允许智能体先将用户的自然语言提问,优化成更适合搜索引擎的精炼关键词,从而显著提升搜索结果的质量。

(2)定义工作流节点

定义好状态结构后,下一步是创建构成我们工作流的各个节点。在 LangGraph 中,每个节点都是一个执行具体任务的 Python 函数。这些函数接收当前的状态对象作为输入,并返回一个包含更新后字段的字典。

在开始定义节点之前,我们先完成项目的初始化设置,包括加载环境变量和实例化大语言模型。

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from tavily import TavilyClient

# 加载 .env 文件中的环境变量
load_dotenv()

# 初始化模型
# 我们将使用这个 llm 实例来驱动所有节点的智能
llm = ChatOpenAI(
    model=os.getenv("LLM_MODEL_ID", "gpt-4o-mini"),
    api_key=os.getenv("LLM_API_KEY"),
    base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
    temperature=0.7
)
# 初始化Tavily客户端
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))

现在,我们来逐一创建三个核心节点。

(1) 理解与查询节点

此节点是工作流的第一步,此节点的职责是理解用户意图,并为其生成一个最优化的搜索查询。

def understand_query_node(state: SearchState) -> dict:
    """步骤1:理解用户查询并生成搜索关键词"""
    user_message = state["messages"][-1].content
    
    understand_prompt = f"""分析用户的查询:"{user_message}"
请完成两个任务:
1. 简洁总结用户想要了解什么
2. 生成最适合搜索引擎的关键词(中英文均可,要精准)

格式:
理解:[用户需求总结]
搜索词:[最佳搜索关键词]"""

    response = llm.invoke([SystemMessage(content=understand_prompt)])
    response_text = response.content
    
    # 解析LLM的输出,提取搜索关键词
    search_query = user_message # 默认使用原始查询
    if "搜索词:" in response_text:
        search_query = response_text.split("搜索词:")[1].strip()
    
    return {
        "user_query": response_text,
        "search_query": search_query,
        "step": "understood",
        "messages": [AIMessage(content=f"我将为您搜索:{search_query}")]
    }

该节点通过一个结构化的提示,要求 LLM 同时完成“意图理解”和“关键词生成”两个任务,并将解析出的专用搜索关键词更新到状态的 search_query 字段中,为下一步的精确搜索做好准备。

(2)搜索节点

该节点负责执行智能体的“工具使用”能力,它将调用 Tavily API 进行真实的互联网搜索,并具备基础的错误处理功能。

def tavily_search_node(state: SearchState) -> dict:
    """步骤2:使用Tavily API进行真实搜索"""
    search_query = state["search_query"]
    try:
        print(f"🔍 正在搜索: {search_query}")
        response = tavily_client.search(
            query=search_query, search_depth="basic", max_results=5, include_answer=True
        )
        # ... (处理和格式化搜索结果) ...
        search_results = ... # 格式化后的结果字符串
        
        return {
            "search_results": search_results,
            "step": "searched",
            "messages": [AIMessage(content="✅ 搜索完成!正在整理答案...")]
        }
    except Exception as e:
        # ... (处理错误) ...
        return {
            "search_results": f"搜索失败:{e}",
            "step": "search_failed",
            "messages": [AIMessage(content="❌ 搜索遇到问题...")]
        }

此节点通过 tavily_client.search 发起真实的 API 调用。它被包裹在 try...except 块中,用于捕获可能的异常。如果搜索失败,它会更新 step 状态为 "search_failed",这个状态将被下一个节点用来触发备用方案。

(3)回答节点

最后的回答节点能够根据上一步的搜索是否成功,来选择不同的回答策略,具备了一定的弹性。

def generate_answer_node(state: SearchState) -> dict:
    """步骤3:基于搜索结果生成最终答案"""
    if state["step"] == "search_failed":
        # 如果搜索失败,执行回退策略,基于LLM自身知识回答
        fallback_prompt = f"搜索API暂时不可用,请基于您的知识回答用户的问题:\n用户问题:{state['user_query']}"
        response = llm.invoke([SystemMessage(content=fallback_prompt)])
    else:
        # 搜索成功,基于搜索结果生成答案
        answer_prompt = f"""基于以下搜索结果为用户提供完整、准确的答案:
用户问题:{state['user_query']}
搜索结果:\n{state['search_results']}
请综合搜索结果,提供准确、有用的回答..."""
        response = llm.invoke([SystemMessage(content=answer_prompt)])
    
    return {
        "final_answer": response.content,
        "step": "completed",
        "messages": [AIMessage(content=response.content)]
    }

该节点通过检查 state["step"] 的值来执行条件逻辑。如果搜索失败,它会利用 LLM 的内部知识回答并告知用户情况。如果搜索成功,它则会使用包含实时搜索结果的提示,来生成一个有时效性且有据可依的回答。

(4)构建图

我们将所有节点连接起来。

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver

def create_search_assistant():
    workflow = StateGraph(SearchState)
    
    # 添加节点
    workflow.add_node("understand", understand_query_node)
    workflow.add_node("search", tavily_search_node)
    workflow.add_node("answer", generate_answer_node)
    
    # 设置线性流程
    workflow.add_edge(START, "understand")
    workflow.add_edge("understand", "search")
    workflow.add_edge("search", "answer")
    workflow.add_edge("answer", END)
    
    # 编译图
    memory = InMemorySaver()
    app = workflow.compile(checkpointer=memory)
    return app

6.5.3 LangGraph 的优势与局限性分析

任何技术框架都有其特定的适用场景和设计权衡。在本节中,我们将客观地分析 LangGraph 的核心优势及其在实际应用中可能面临的局限性。

(1)优势

  • 如我们的智能搜索助手案例所示,LangGraph 将一个完整的实时问答流程,显式地定义为一个由状态、节点和边构成的“流程图”。这种设计的最大优势是高度的可控性与可预测性。开发者可以精确地规划智能体的每一步行为,这对于构建需要高可靠性和可审计性的生产级应用至关重要。其最强大的特性在于对循环(Cycles)的原生支持。通过条件边,我们可以轻松构建“反思-修正”循环,例如在我们的案例中,如果搜索失败,可以设计一个回退到备用方案的路径。这是构建能够自我优化和具备容错能力的智能体的关键。
  • 此外,由于每个节点都是一个独立的 Python 函数,这带来了高度的模块化。同时,在流程中插入一个等待人类审核的节点也变得非常直接,为实现可靠的“人机协作”(Human-in-the-loop)提供了坚实的基础。

(2)局限性

  • 与基于对话的框架相比,LangGraph 需要开发者编写更多的前期代码(Boilerplate)。定义状态、节点、边等一系列操作,使得对于简单任务而言,开发过程显得更为繁琐。开发者需要更多地思考“如何控制流程(how)”,而不仅仅是“做什么(what)”。由于工作流是预先定义的,LangGraph 的行为虽然可控,但也缺少了对话式智能体那种动态的、“涌现”式的交互。它的强项在于执行一个确定的、可靠的流程,而非模拟开放式的、不可预测的社会性协作。
  • 调试过程同样存在挑战。虽然流程比对话历史更清晰,但问题可能出在多个环节:某个节点内部的逻辑错误、在节点间传递的状态数据发生异变,或是边跳转的条件判断失误。这要求开发者对整个图的运行机制有全局性的理解。

code

"""
智能搜索助手 - 基于 LangGraph + Tavily API 的真实搜索系统
1. 理解用户需求
2. 使用Tavily API真实搜索信息  
3. 生成基于搜索结果的回答
"""

import asyncio
from typing import TypedDict, Annotated
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
import os
from dotenv import load_dotenv
from tavily import TavilyClient

# 加载环境变量
load_dotenv()

# 定义状态结构
class SearchState(TypedDict):
    messages: Annotated[list, add_messages]
    user_query: str        # 用户查询
    search_query: str      # 优化后的搜索查询
    search_results: str    # Tavily搜索结果
    final_answer: str      # 最终答案
    step: str             # 当前步骤

# 初始化模型和Tavily客户端
llm = ChatOpenAI(
    model=os.getenv("LLM_MODEL_ID", "gpt-4o-mini"),
    api_key=os.getenv("LLM_API_KEY"),
    base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
    temperature=0.7
)

# 初始化Tavily客户端
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))

def understand_query_node(state: SearchState) -> SearchState:
    """步骤1:理解用户查询并生成搜索关键词"""
    
    # 获取最新的用户消息
    user_message = ""
    for msg in reversed(state["messages"]):
        if isinstance(msg, HumanMessage):
            user_message = msg.content
            break
    
    understand_prompt = f"""分析用户的查询:"{user_message}"

请完成两个任务:
1. 简洁总结用户想要了解什么
2. 生成最适合搜索的关键词(中英文均可,要精准)

格式:
理解:[用户需求总结]
搜索词:[最佳搜索关键词]"""

    response = llm.invoke([HumanMessage(content=understand_prompt)])
    
    # 提取搜索关键词
    response_text = response.content
    search_query = user_message  # 默认使用原始查询
    
    if "搜索词:" in response_text:
        search_query = response_text.split("搜索词:")[1].strip()
    elif "搜索关键词:" in response_text:
        search_query = response_text.split("搜索关键词:")[1].strip()
    
    return {
        "user_query": response.content,
        "search_query": search_query,
        "step": "understood",
        "messages": [AIMessage(content=f"我理解您的需求:{response.content}")]
    }

def tavily_search_node(state: SearchState) -> SearchState:
    """步骤2:使用Tavily API进行真实搜索"""
    
    search_query = state["search_query"]
    
    try:
        print(f"🔍 正在搜索: {search_query}")
        
        # 调用Tavily搜索API
        response = tavily_client.search(
            query=search_query,
            search_depth="basic",
            include_answer=True,
            include_raw_content=False,
            max_results=5
        )
        
        # 处理搜索结果
        search_results = ""
        
        # 优先使用Tavily的综合答案
        if response.get("answer"):
            search_results = f"综合答案:\n{response['answer']}\n\n"
        
        # 添加具体的搜索结果
        if response.get("results"):
            search_results += "相关信息:\n"
            for i, result in enumerate(response["results"][:3], 1):
                title = result.get("title", "")
                content = result.get("content", "")
                url = result.get("url", "")
                search_results += f"{i}. {title}\n{content}\n来源:{url}\n\n"
        
        if not search_results:
            search_results = "抱歉,没有找到相关信息。"
        
        return {
            "search_results": search_results,
            "step": "searched",
            "messages": [AIMessage(content=f"✅ 搜索完成!找到了相关信息,正在为您整理答案...")]
        }
        
    except Exception as e:
        error_msg = f"搜索时发生错误: {str(e)}"
        print(f"❌ {error_msg}")
        
        return {
            "search_results": f"搜索失败:{error_msg}",
            "step": "search_failed",
            "messages": [AIMessage(content="❌ 搜索遇到问题,我将基于已有知识为您回答")]
        }

def generate_answer_node(state: SearchState) -> SearchState:
    """步骤3:基于搜索结果生成最终答案"""
    
    # 检查是否有搜索结果
    if state["step"] == "search_failed":
        # 如果搜索失败,基于LLM知识回答
        fallback_prompt = f"""搜索API暂时不可用,请基于您的知识回答用户的问题:

用户问题:{state['user_query']}

请提供一个有用的回答,并说明这是基于已有知识的回答。"""
        
        response = llm.invoke([HumanMessage(content=fallback_prompt)])
        
        return {
            "final_answer": response.content,
            "step": "completed",
            "messages": [AIMessage(content=response.content)]
        }
    
    # 基于搜索结果生成答案
    answer_prompt = f"""基于以下搜索结果为用户提供完整、准确的答案:

用户问题:{state['user_query']}

搜索结果:
{state['search_results']}

请要求:
1. 综合搜索结果,提供准确、有用的回答
2. 如果是技术问题,提供具体的解决方案或代码
3. 引用重要信息的来源
4. 回答要结构清晰、易于理解
5. 如果搜索结果不够完整,请说明并提供补充建议"""

    response = llm.invoke([HumanMessage(content=answer_prompt)])
    
    return {
        "final_answer": response.content,
        "step": "completed",
        "messages": [AIMessage(content=response.content)]
    }

# 构建搜索工作流
def create_search_assistant():
    workflow = StateGraph(SearchState)
    
    # 添加三个节点
    workflow.add_node("understand", understand_query_node)
    workflow.add_node("search", tavily_search_node)
    workflow.add_node("answer", generate_answer_node)
    
    # 设置线性流程
    workflow.add_edge(START, "understand")
    workflow.add_edge("understand", "search")
    workflow.add_edge("search", "answer")
    workflow.add_edge("answer", END)
    
    # 编译图
    memory = InMemorySaver()
    app = workflow.compile(checkpointer=memory)
    
    return app

async def main():
    """主函数:运行智能搜索助手"""
    
    # 检查API密钥
    if not os.getenv("TAVILY_API_KEY"):
        print("❌ 错误:请在.env文件中配置TAVILY_API_KEY")
        return
    
    app = create_search_assistant()
    
    print("🔍 智能搜索助手启动!")
    print("我会使用Tavily API为您搜索最新、最准确的信息")
    print("支持各种问题:新闻、技术、知识问答等")
    print("(输入 'quit' 退出)\n")
    
    session_count = 0
    
    while True:
        user_input = input("🤔 您想了解什么: ").strip()
        
        if user_input.lower() in ['quit', 'q', '退出', 'exit']:
            print("感谢使用!再见!👋")
            break
        
        if not user_input:
            continue
        
        session_count += 1
        config = {"configurable": {"thread_id": f"search-session-{session_count}"}}
        
        # 初始状态
        initial_state = {
            "messages": [HumanMessage(content=user_input)],
            "user_query": "",
            "search_query": "",
            "search_results": "",
            "final_answer": "",
            "step": "start"
        }
        
        try:
            print("\n" + "="*60)
            
            # 执行工作流
            async for output in app.astream(initial_state, config=config):
                for node_name, node_output in output.items():
                    if "messages" in node_output and node_output["messages"]:
                        latest_message = node_output["messages"][-1]
                        if isinstance(latest_message, AIMessage):
                            if node_name == "understand":
                                print(f"🧠 理解阶段: {latest_message.content}")
                            elif node_name == "search":
                                print(f"🔍 搜索阶段: {latest_message.content}")
                            elif node_name == "answer":
                                print(f"\n💡 最终回答:\n{latest_message.content}")
            
            print("\n" + "="*60 + "\n")
        
        except Exception as e:
            print(f"❌ 发生错误: {e}")
            print("请重新输入您的问题。\n")

if __name__ == "__main__":
    asyncio.run(main())

终端输出问答

(.venv) MacBook-Air langgraph % python Dialogue_System.py
🔍 智能搜索助手启动!
我会使用Tavily API为您搜索最新、最准确的信息
支持各种问题:新闻、技术、知识问答等
(输入 'quit' 退出)

🤔 您想了解什么: 中东地区,美国军队最新欣慰

============================================================
🧠 理解阶段: 我理解您的需求:理解:用户意图查询美国军队在中东地区的最新动态或相关新闻(识别出关键词“欣慰”应为“新闻”或“消息”的笔误)。
搜索词:中东 美军 最新新闻, US military Middle East latest news
🔍 正在搜索: 中东 美军 最新新闻, US military Middle East latest news
🔍 搜索阶段: ✅ 搜索完成!找到了相关信息,正在为您整理答案...

💡 最终回答:
基于您提供的搜索结果,针对用户关于“美军在中东地区最新动态”的查询(已识别并修正关键词“欣慰”为“新闻”),以下是综合整理后的信息:

### 1. 核心动态概述
当前美军在中东地区的局势高度紧张,主要围绕**军事基地遭受攻击**以及**美军资产的紧急调动**展开。伊朗方面声称对美军基地进行了打击,而美军则面临在该地区维持防御和威慑力的压力。

### 2. 详细军事动态

*   **美军基地遭受袭击**
    根据搜索结果,位于中东地区的美军基地已成为攻击目标。
    *   **具体地点**:提及了位于**巴林**的第五舰队中心遭受攻击的情况。
    *   **攻击方**:伊朗声称对多个美军基地发动了打击。另有视频来源提及伊朗革命卫队导弹再次打击美国航母等说法 [2][3]。
    *   **防御系统**:报道中涉及“萨德”(THAAD)系统及“爱国者”导弹在防御中的角色,甚至提及伊朗声称击中了位于约旦的美军萨德系统 [3]。

*   **美军紧急调动与后勤**
    为了应对中东地区的紧张局势,美军似乎正在从其他战区抽调资源。
    *   **跨区域调运**:有新闻指出美军正紧急从韩国、日本及台湾(ROC)等地调运导弹前往中东,以补充当地需求 [1]。这表明当前冲突的烈度可能消耗了美军在当地的大量库存或需要增强防御能力。

*   **地区局势背景**
    目前的军事冲突与以色列及加沙的战争背景紧密相关。报道中提到了以色列总理内塔尼亚胡的政治处境(通缉令、贪污案)以及其如何影响地区局势 [1][3]。此外,也有观点讨论俄乌战争是否会随着中东冲突而结束 [3]。

### 3. 信息来源参考
以上信息综合自以下搜索结果:
*   **来源 1**: [Focus全球新闻 - 以色列制霸中東?!](https://www.youtube.com/watch?v=wFRdM2Agvlo) (提及美军从日韩台调运导弹)
*   **来源 2**: [中天新闻 - US military bases in the Middle East become targets...](https://www.youtube.com/watch?v=Mlskk7KFLVY) (提及美军基地遭伊朗攻击、第五舰队)
*   **来源 3**: [TVBS新闻 - 伊朗稱再擊中約旦美薩德...](https://www.youtube.com/watch?v=KasHjG1WSVI) (提及萨德系统、航母及地区冲突关联)

### 4. 补充说明与建议
*   **信息时效性提示**:目前的搜索结果多为YouTube视频新闻标题,具体的时间节点(如“昨天”或“上周”)需进入视频内部确认。建议用户关注主流新闻通讯社(如Reuters, AP)以获取精确到分钟的战况更新。
*   **笔误修正**:系统已自动将查询中的“欣慰”理解为“新闻”或“消息”,本次回答是基于“最新新闻”的语境生成的。

============================================================

🤔 您想了解什么: quit
感谢使用!再见!👋

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值