TellWorldAI 世界档案
问档案English

首页›实践›实践

实践

用 LangGraph 做一个文档分析智能体

本文用 LangGraph 搭一个小型文档分析智能体:用视觉模型从图片中提取文字,调用计算工具,并在助手节点和工具节点之间循环,直到给出回答。

这篇适合谁,读完能做到什么

如果你想做一个能读文档(比如笔记或纸质文件的照片)、会调用工具、并针对文档回答问题的智能体,这篇适合你。读完你应该能用 LangGraph 定义智能体状态,给模型配两个工具,把助手节点和工具节点连成循环,并分别用一个文字问题和一个图片问题跑通整个图。

这个智能体能做什么

示例中的智能体能处理图片形式的文档,用视觉语言模型提取文字,做计算,分析内容并给出简洁的摘要,还能执行与文档相关的具体指令[1]。示例把智能体设定成为韦恩先生服务的管家 Alfred,所以提示词的语气也是管家式的。你的项目可以换成自己的角色。

LangGraph 的图由状态、节点和边三部分组成。状态保存数据,节点完成工作,边决定控制流往哪里走,这是示例的基本结构[1]。

图里有两个节点。助手节点是绑定了工具的模型,工具节点负责运行模型要求调用的工具。只要模型还在调用工具,流程就在这两个节点之间循环[1]。

准备

示例在 Notebook 中运行。用下面的魔法命令安装依赖[1]:

%pip install langgraph langchain_openai langchain_core

示例通过 ChatOpenAI 使用 OpenAI 的 gpt-4o 模型,所以调用前需要先有 OpenAI 的 API 密钥。视觉模型和文本模型用的是同一个类[1]:

vision_llm = ChatOpenAI(model="gpt-4o")

第一步:定义状态

状态保存一次运行中的数据。这里有两个字段:输入文件的路径(PDF 或 PNG),以及消息列表[1]:

class AgentState(TypedDict):
    # The document provided
    input_file: Optional[str]  # Contains file path (PDF/PNG)
    messages: Annotated[list[AnyMessage], add_messages]

需要注意的是 add_messages 这个标注。AnyMessage 是 LangChain 定义消息的类,add_messages 则是一个操作符,它把最新的消息追加进去,而不是覆盖整个列表。LangGraph 允许给状态字段附加操作符,以此控制更新如何合并[1]。

第二步:准备工具

第一个工具用来从图片中读取文字。它把文件做 base64 编码后发给视觉模型,提示词要求模型只返回提取出的文字,不要任何解释。函数开头如下[1]:

def extract_text(img_path: str) -> str:
    """
    Extract text from an image file using a multimodal model.
    
    Master Wayne often leaves notes with his training regimen or meal plans.
    This allows me to properly analyze the contents.
    """
    all_text = ""
    try:
        # Read image and encode as base64
        with open(img_path, "rb") as image_file:
            image_bytes = image_file.read()

        image_base64 = base64.b64encode(image_bytes).decode("utf-8")

如果调用失败,工具会打印错误信息并返回空字符串。示例中的注释认为,管家应当妥善处理错误[1]。

第二个工具是一个普通的除法函数,用来处理 Mr. Wayne 偶尔需要的计算[1]。两个工具都绑定到模型上,并关闭了并行工具调用[1]:

def divide(a: int, b: int) -> float:
    """Divide a and b - for Master Wayne's occasional calculations."""
    return a / b

# Equip the butler with tools
tools = [
    divide,
    extract_text
]

llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools, parallel_tool_calls=False)

第三步:编写助手节点

模型需要知道每个工具是做什么的,所以助手节点会把工具的文字说明写进系统提示词。说明里列出了每个工具的名称、参数和返回值[1]:

def assistant(state: AgentState):
    # System message
    textual_description_of_tool="""
extract_text(img_path: str) -> str:
    Extract text from an image file using a multimodal model.

    Args:
        img_path: A local image file path (strings).

    Returns:
        A single string containing the concatenated text extracted from each image.
divide(a: int, b: int) -> float:
    Divide a and b
"""

接着,节点构造系统消息,并把模型的回复连同输入文件路径一起返回。系统消息告诉模型它是名叫 Alfred 的管家,并写明当前加载的图片[1]:

    image=state["input_file"]
    sys_msg = SystemMessage(content=f"You are a helpful butler named Alfred that serves Mr. Wayne and Batman. You can analyse documents and run computations with provided tools:\n{textual_description_of_tool} \n You have access to some optional images. Currently the loaded image is: {image}")

    return {
        "messages": [llm_with_tools.invoke([sys_msg] + state["messages"])],
        "input_file": state["input_file"]
    }

第四步:把节点连成循环

这个智能体遵循 ReAct 模式,这种模式常被概括为推理(Reason)、行动(Act)、观察(Observe)。模型先对文档和请求进行推理,再通过调用工具采取行动,观察结果,并视需要重复[1]。

在 LangGraph 中,用 StateGraph 构建图。示例注册了两个节点,从助手开始,并用 tools_condition 决定助手的输出走向[1]:

# The graph
builder = StateGraph(AgentState)

# Define nodes: these do the work
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))

# Define edges: these determine how the control flow moves
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
    "assistant",
    # If the latest message requires a tool, route to tools
    # Otherwise, provide a direct response
    tools_condition,
)
builder.add_edge("tools", "assistant")
react_graph = builder.compile()

# Show the butler's thought process
display(Image(react_graph.get_graph(xray=True).draw_mermaid_png()))

当助手的输出是工具调用时,tools_condition 把流程送到工具节点;否则流程进入 END,本次运行结束。工具节点总是返回助手节点,循环由此形成[1]。add_messages 让消息历史随着循环不断累积,助手每次都能看到之前的工具结果[1]。

运行一次计算

第一个测试是普通的计算题。下面的代码把一条消息送进图中,并逐条打印消息[1]:

messages = [HumanMessage(content="Divide 6790 by 5")]
messages = react_graph.invoke({"messages": messages, "input_file": None})

# Show the messages
for m in messages['messages']:
    m.pretty_print()

在这次运行中,助手调用 divide(a=6790, b=5),工具返回 1358.0,助手据此给出答案[1]。因为没有涉及文件,input_file 为 None[1]。pretty_print() 会按消息类型逐条打印,让你能看到每一次工具调用和返回值[1]。

运行一次图片问题

第二个测试是关于图片中便条的问题。图的输入里带上了文件名[1]:

messages = [HumanMessage(content="According to the note provided by Mr. Wayne in the provided images. What's the list of items I should buy for the dinner menu?")]
messages = react_graph.invoke({"messages": messages, "input_file": "Batman_training_and_meals.png"})

这次运行中,助手先用图片路径调用 extract_text,再根据拿到的文字作答[1]。读图是独立的一次工具调用,所以可以在打印出的消息里逐步检查。如果要换一张图,只需改动 input_file 的值,并确保文件路径能被 extract_text 打开[1]。

限制和常见问题

  • 这个设计的错误处理比较简单。extract_text 失败时返回空字符串,模型可能在没有便条内容的情况下作答,所以要先检查工具输出,再相信回答[1]。
  • 工具说明要准确。模型只能通过系统提示词里的文字了解工具[1]。
  • 示例需要 OpenAI 密钥,并调用付费模型,每次运行都会把图片发送给该模型。
  • 输入质量决定结果。图片模糊或文字太小时,提取出的文字会不完整,后面的回答也会受影响。
  • 图通过 add_messages 保留消息历史。对话变长后,状态也会变大,需要时要考虑裁剪[1]。

下一步

配套的 Notebook 在 Hugging Face Hub 上,可以在 Colab 中运行同样的代码[1]。想做自己的文档管家,可以先为具体的文档任务设计清楚的工具,再加上状态跟踪和错误处理[1]。做自己的版本时,可以先只保留一个工具,跑通之后再逐个加入,便于排查问题。add_messages 让历史消息不被覆盖,这是多轮工具调用能保留上下文的原因[1]。

本文改编自 Hugging Face 开源智能体课程第二单元中 LangGraph 文档分析的一节[1],原文以 Apache-2.0 许可证发布。

来源

  1. 1
    Document Analysis GraphHugging Face agents-course (Apache-2.0)

引用本页: 用 LangGraph 做一个文档分析智能体. TellWorld, https://www.tellworld.net/zh/practice/practice-langgraph-document-agent. 更新于 2026-11-14

相关动态