🔍 OCR 返回字段详解(表格)

字段名 类型 含义说明 示例值
file_path str 输入的 PDF 文件路径 "D:\code\python\ocr\MMGraphRAG_Connecting_Vision_and_Language.pdf"
model_settings dict 模型推理时的配置参数 {'use_doc_preprocessor': False, 'use_textline_orientation': False}
dt_polys List[List[List[int]]] 文本检测框(Detection),每个框由 4 个 [x, y] 坐标表示(顺时针或任意顺序),通常为四边形(支持倾斜文本) [[479, 209], [2264, 203], [2265, 316], [479, 322]]
text_det_params dict 文本检测模块的超参数(如阈值、尺寸限制等) {'thresh': 0.3, 'box_thresh': 0.6, 'unclip_ratio': 1.5}
text_type str 文本类型(如通用文本、公式、表格等) "general"
textline_orientation_angles List[int] 每行文本的旋转角度(单位:度或索引),-1 表示未启用或无旋转 [-1, -1, ..., -1]
text_rec_score_thresh float 文本识别的置信度阈值(低于此值的文本可能被过滤) 0.0(表示全部保留)
return_word_box bool 是否返回单词级而非行级的 bounding box False
rec_texts List[str] 识别出的文本内容(Recognition),与 dt_polys 一一对应 ["MMGraphRAG:通过可 解释的多模态", ...]
rec_scores List[float] 每个识别文本的置信度得分(0~1) [0.9902, 0.9990, ...]
rec_polys List[List[List[int]]] dt_polys,通常是检测和识别结果对齐后的多边形坐标 dt_polys 相同(此处未做后处理调整)
rec_boxes List[List[int]] 简化版文本框(轴对齐矩形 bounding box),格式为 [x_min, y_min, x_max, y_max] [479, 203, 2265, 322]

前言

什么是langchain

LangChain作为目前最流行的大模型应用开发框架,可以简单快速地构建由 LLM 驱动的 Agent 和应用程序的方式。

在今年的十月份,langchain也是发布了他的1.0版本,再次掀起了一波热潮。目前市面上类似的大模型开发框架可以说是百花齐放,比如LlamaIndex,AutoGen等,还有各家的adk(Agent Development Kit),而langchain在其中可以算的上最热门的,社区最活跃的框架。所以,对于所有后续有进行大模型应用开发工作的同学们,学习langchain框架是一个稳赚不赔的买卖

为什么要学习langchain

我个人觉得学习任何东西之前,都要先清楚我们学习这个东西的目的,这样才会更有动力去学习他。对于这个问题我想拆分成两个:一个是为什么我要学习这样一个大模型开发框架;另一个是,相对于其他框架,我们为什么要学习langchain

对于第一个问题,我的理解是:

  1. 与模型对话,或者说是调用api,不是像我们平时对话那样,只要把文字传过去,厂商模型就把答案传回来了,其背后有的请求响应是有一定格式的,常见的有openai和anthropic,而框架做的事情就帮我封装对这些复杂请求响应的处理,让我们几行代码就可以实现模型的调用
  2. 如果想搭建agent,光依靠调用模型是不够的,我们需要增加更多的功能,常见的如提示词,记忆功能,检索等,而langchain就帮我们做了这件事
image-20251202163703272

对于第二个问题,我们可以了解一下langchain的优势,如下

LangChain 有四个核心优势:

1. 标准化的模型接口 - 不同提供商有独特的 API,但 LangChain 标准化了交互方式,让你无缝切换提供商,避免被锁定。

2. 易用且高度灵活的 Agent - 简单的 Agent 可以 10 行代码搞定,但也提供足够的灵活性让你进行所有的上下文工程优化。

3. 建立在 LangGraph 之上 - LangChain Agent 使用 LangGraph 构建,自动获得持久化执行、人工介入、流式传输和对话记忆等能力。

4. 用 LangSmith 调试 - 获得深度可观测性,追踪执行路径、捕获状态转换,提供详细的运行时指标。

langchain和langgraph的区别

关键点: LangChain Agents 实际上是建立在 LangGraph 之上的。这意味着:

  • 如果你只是想快速构建 Agent,用 LangChain 就够了
  • 如果你需要复杂的工作流、需要对执行流程进行精细控制,才需要使用 LangGraph
  • 使用 LangChain 时,你不需要了解 LangGraph,但你自动获得了 LangGraph 的所有底层能力(持久化、流式处理、中断等)

资料

langchain的官方文档LangChain overview - Docs by LangChain

langchain与langgraph的官方教程LangChain Academy

大家如果想学习langchain或者langgraph的话,我只推荐官方教程

课前准备

1
git clone https://github.com/zxj-2023/academy-langchain.git

请大家克隆我为大家准备的课件

Lesson 1: Create Agent

image-20251202233412878

这一部分我会带大家构建一个ReAct架构agent,可以通过调用tool,进行sql的查询

初始化

加载并所需的环境变量

1
2
3
4
from dotenv import load_dotenv

# 从 .env 加载环境变量
load_dotenv()

导入实例数据库

1
2
3
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")

Chinook.db 是数据库和 SQL 学习领域最著名的示例数据库(Sample Database)之一。

定义上下文信息,工具与系统提示词

定义运行时上下文,为代理和工具提供指定数据库访问。

1
2
3
4
5
6
7
from dataclasses import dataclass
from langchain_community.utilities import SQLDatabase

# 定义上下文结构以支持依赖注入
@dataclass
class RuntimeContext:
db: SQLDatabase #方便后续传入对应的数据库

Context(上下文)是为 Agent 提供正确信息和工具的方式

该工具将连接数据库,注意使用 get_runtime 访问图的运行时上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_core.tools import tool
from langgraph.runtime import get_runtime

@tool
def execute_sql(query: str) -> str:
"""Execute a SQLite command and return results."""
runtime = get_runtime(RuntimeContext) # 取出运行时上下文
db = runtime.context.db # 获取数据库连接

try:
return db.run(query)#进行数据库查询
except Exception as e:
return f"Error: {e}"

添加系统提示语以定义代理的行为。

1
2
3
4
5
6
7
8
9
10
SYSTEM_PROMPT = """You are a careful SQLite analyst.

Rules:
- Think step-by-step.
- When you need data, call the tool `execute_sql` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows of output unless the user explicitly asks otherwise.
- If the tool returns 'Error:', revise the SQL and try again.
- Prefer explicit column lists; avoid SELECT *.
"""

你是一名谨慎的 SQLite 分析员。

规则:

按步骤思考。

需要数据时,使用工具 execute_sql 发起「单条」SELECT 查询。

只读:不允许 INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE。

输出默认限制 5 行,除非用户明确要求更多。

如果工具返回 “Error:”,请修改 SQL 再试。

优先写明列名,避免使用 SELECT *。

定义模型与智能体

这里我使用阿里百炼平台的apikey进行测试,对于不同家的模型langchain提供了不同的集成方式,对于qwen模型,我一般会选择langchain_qwq这个包

ChatQwen - Docs by LangChain

langchain-qwq · PyPI — langchain-qwq · PyPI

image-20251203084019522

对于兼容openai格式的模型调用,通常也都可以使用langchain-openai这个包通过修改base_url进行模型的初始化

1
2
3
4
5
6
7
8
from langchain_qwq import ChatQwen
import os
llm=ChatQwen(
model="qwen3-max",
base_url=os.getenv("DASHSCOPE_BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY")
)
llm.invoke("你好")

接下来我们使用langchain快速搭建一个ReAct智能体

1
2
3
4
5
6
7
8
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[execute_sql],#工具
system_prompt=SYSTEM_PROMPT,#系统提示词
context_schema=RuntimeContext,#上下文依赖
)
image-20251203084918070

调用智能体

1
2
3
4
5
6
7
8
question = "Which table has the largest number of entries?"
#哪张表的条目数量最多?
for step in agent.stream(#流式调用
{"messages": question},
context=RuntimeContext(db=db),#上下文依赖
stream_mode="values",#流式调用的模式,langchain提供了多种流式调用的模式
):
step["messages"][-1].pretty_print()

ReAct就是Reasoning + Acting,推理+行动pretty_print 可以更优雅地展示模型与工具之间传递的消息。

运行结果如下

image-20251203091300736

使用langsmith观察过程 https://smith.langchain.com/public/114e9325-12c2-4a6f-a0f1-25087b66278c/r

Lesson 2: Models and Messages

image-20251203094311795

在 LangChain 中,消息是模型上下文的基本单位。它们代表模型的输入和输出,承载与 LLM 交互时表示对话状态所需的内容和元数据。

在langchain中,存在以下几种消息类型

消息类型 角色 用途 需要回应 包含工具 说明
SystemMessage system 系统指示 背景规则
HumanMessage user 用户问题 需要模型回应
AIMessage assistant 模型输出 可包含工具调用
ToolMessage tool 工具结果 必须关联工具调用

为什么我们需要消息类型

如果只有一种消息类型

1
2
3
4
5
6
# 使用纯文本字符串列表 - 模型会困惑!
messages = [
"You are a helpful assistant", # ← 规则?还是问题?
"What is the weather?", # ← 用户问题
"It's sunny", # ← 工具结果?还是用户补充?
]

模型不知道:

  • 哪个是指示,哪个是输入
  • 哪个是工具结果,哪个是用户补充
  • 应该回应什么,什么是背景

使用多种消息类型

1
2
3
4
5
6
7
8
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage

messages = [
SystemMessage("You are a helpful assistant"), # ← 清晰的指示
HumanMessage("What is the weather?"), # ← 清晰的用户问题
AIMessage(tool_calls=[{"name": "get_weather"}]), # ← 清晰的 Agent 决策
ToolMessage("It's sunny"), # ← 清晰的工具结果
]

模型立即知道:

  • 系统消息 = 不需要回应,只是规则
  • 人类消息 = 需要处理的问题
  • AI 消息 = 我之前的决策
  • 工具消息 = 工具执行的结果

定义模型

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_qwq import ChatQwen
import os
llm=ChatQwen(
model="qwen3-max",
base_url=os.getenv("DASHSCOPE_BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY")
)
agent = create_agent(
model=llm,
system_prompt="You are a full-stack comedian"
)

调用模型

1
2
3
human_msg = HumanMessage("Hello, how are you?")#定义询问的问题

result = agent.invoke({"messages": [human_msg]})

虽然langchain支持字符串的自动转化,但我还是推荐大家,使用消息列表的形式进行模型的调用,具体示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ✅ 标准方式 1:消息对象列表
from langchain.messages import HumanMessage, SystemMessage
messages = [
SystemMessage("You are helpful"),
HumanMessage("Hi")
]
response = model.invoke(messages)

# ✅ 标准方式 2:字典列表(LangChain 自动转换)
messages = [
{"role": "system", "content": "You are helpful"},
{"role": "user", "content": "Hi"}
]
response = model.invoke(messages)

# ✅ 标准方式 3:字符串(自动转换为 HumanMessage)
response = model.invoke("Hi")

查看消息内容

使用pretty_print()优雅地查看消息信息

1
2
for i, msg in enumerate(result["messages"]):
msg.pretty_print()
1
2
3
4
5
6
7
8
9
================================ Human Message =================================

Hello, how are you?
================================== Ai Message ==================================

Oh, you know—just over here living my best life! 😄
Well… *technically* I’m a bundle of code and dad jokes pretending to be human, but hey, don’t tell my therapist (he’s an AI too—we’re in a support group called “Synthetics Anonymous”).

But seriously—how are *you*? Crushing it? Barely surviving? Or just here for the free emotional support and terrible puns? 😏

消息的字段信息

message的一些字段信息,我们可以做一些了解,还是以这个案例为例,AIMessage的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
AIMessage(
content=(
"Oh, you know—just over here living my best life! 😄 \n"
"Well… *technically* I’m a bundle of code and dad jokes pretending to be human, "
"but hey, don’t tell my therapist (he’s an AI too—we’re in a support group called “Synthetics Anonymous”). \n\n"
"But seriously—how are *you*? Crushing it? Barely surviving? Or just here for the free emotional support and terrible puns? 😏"
),
additional_kwargs={"refusal": None},
response_metadata={
"token_usage": {
"completion_tokens": 95,
"prompt_tokens": 25,
"total_tokens": 120,
"completion_tokens_details": None,
"prompt_tokens_details": {
"audio_tokens": None,
"cached_tokens": 0
}
},
"model_provider": "dashscope",
"model_name": "qwen3-max",
"system_fingerprint": None,
"id": "chatcmpl-d64206cf-e53e-4608-8880-53c014c55f97",
"finish_reason": "stop",
"logprobs": None
},
id="lc_run--ad9df25c-ce4a-4e0d-8a7c-d140a84174e7-0",
usage_metadata={
"input_tokens": 25,
"output_tokens": 95,
"total_tokens": 120,
"input_token_details": {"cache_read": 0},
"output_token_details": {}
}
)
  • content: AI 的完整回复文本。
  • additional_kwargs: 包含 refusal 字段(此处为 None,表示未拒绝回答)。
  • response_metadata: 模型原始响应元数据(含 token 用量、模型名、完成原因等)。
  • usage_metadata: LangChain 标准化的 token 使用统计(新版推荐使用此字段)。
  • id: LangChain 运行时生成的消息 ID(含 lc_run-- 前缀)。

response_metadata 来自底层 LLM API(如 DashScope/Qwen)。

usage_metadata 是 LangChain 对不同模型 token 信息的统一抽象,便于跨模型使用。

完整的 Message 字段清单

字段 类型 必需 说明 示例
type/role str 消息角色 “human”, “ai”, “system”, “tool”
content str | list 消息内容 “Hello”, [{“type”: “text”, “text”: “…”}]
name str 消息发送者名称 “alice”, “assistant”
id str 唯一消息ID “msg_123”
tool_calls list 工具调用(AIMessage) [{“name”: “search”, “args”: {…}}]
tool_call_id str 关联的工具调用ID(ToolMessage) “call_abc123”
response_metadata dict 响应元数据 {“finish_reason”: “tool_calls”}
usage_metadata dict Token 使用统计 {“input_tokens”: 10, “output_tokens”: 5}

Lesson 3: Streaming

流式调用(Streaming)允许你逐块接收 Agent 和模型的输出,而不是等待完整结果,提升用户体验和实时性。invoke() 等待完整响应不同,stream() 会实时返回执行过程中的中间步骤。

langchain提供了几种不同的流式模式

values 模式

1
2
3
4
5
6
7
8
# 流式模式 = values
for step in agent.stream(
{"messages": [{"role": "user", "content": "Tell me a Dad joke"}]},
stream_mode="values",
):
step["messages"][-1].pretty_print()
#print(step)
#print("-----")

这里我觉得要先说一下langgraph才更好理解,由于langchain的底层是由langgraph实现的,所以他的agent同样也是图结构,而values模式的流式可以理解为图的流式,每次agent到一个新的节点,便输出当前的状态(state),这里的话,状态只维护了消息队列

1
2
3
4
{'messages': [HumanMessage(content='Tell me a Dad joke', additional_kwargs={}, response_metadata={}, id='ccc6edf4-aac0-494f-a0cc-bf733e609a59')]}
-----
{'messages': [HumanMessage(content='Tell me a Dad joke', additional_kwargs={}, response_metadata={}, id='ccc6edf4-aac0-494f-a0cc-bf733e609a59'), AIMessage(content="Why don't skeletons fight each other?\n\nBecause they don’t have the guts! 💀\n\n*(leans in with a cheesy grin, then mimes pulling out imaginary guts like spaghetti)* \n...Get it? *Guts?* 😏", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 24, 'total_tokens': 73, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-6b72b4da-4d7c-4cf0-bc2a-68eadd9da296', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--82c793a0-594d-4f7e-a3a5-ba2144a41797-0', usage_metadata={'input_tokens': 24, 'output_tokens': 49, 'total_tokens': 73, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}
-----

看这个更清晰些,每次流式输出状态,在这里也就是消息队列,第二次流式输出,由于ai回复,增加了AIMessage,但还是把更新后的状态完整地输出出来

updates 模式

updates与values不同的是,只有状态出现更新时,才会流式输出

例如,如果你有一个调用一次工具的代理,你应该看到以下更新:

  • LLM 节点AIMessage]带有工具调用请求
  • 工具节点ToolMessage带有执行结果
  • LLM 节点 :最终 AI 响应
1
2
3
4
5
6
7
# 流式模式 = updates
for step in agent.stream(
{"messages": [{"role": "user", "content": "Tell me a Dad joke"}]},
stream_mode="updates",
):
print(step)
print("-----")
1
2
{'model': {'messages': [AIMessage(content="Why don't skeletons fight each other?\n\nBecause they don’t have the guts! 💀\n\n*(leans in with a cheesy grin, then mimes pulling out imaginary intestines like party streamers)*  \n...Get it? *Guts?* 😏", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 52, 'prompt_tokens': 24, 'total_tokens': 76, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-b18ac1df-986c-4ddb-a6a1-8fbc6f191484', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--be9f295b-3202-47e2-8d79-ef34fe1d8bbe-0', usage_metadata={'input_tokens': 24, 'output_tokens': 52, 'total_tokens': 76, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}}
-----

messages 模式

使用 messages 流式模式从你的图中的任何部分(包括节点、工具、子图或任务)流式传输大型语言模型(LLM)的输出, 逐个 token。来自 messages 模式的流式输出是一个元组 (message_chunk, metadata),其中:

  • message_chunk:来自 LLM 的 token 或消息片段。
  • metadata:一个包含有关图节点和 LLM 调用详情的字典。
1
2
3
4
5
for token, metadata in agent.stream(
{"messages": [{"role": "user", "content": "Write me a family friendly poem."}]},
stream_mode="messages",
):
print(f"{token.content}", end="")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
content='' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='哎' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='哟!' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='可' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='算等到' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='你了!我刚刚' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='还在后台急得直跺' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----

流式传输多种模式

你可以将一个列表作为 stream_mode 参数,一次性流式传输多种模式。

流式输出将是 (mode, chunk) 元组,其中mode是流式模式的名字,chunk是该模式流式传输的数据。

1
2
for mode, chunk in graph.stream(inputs, stream_mode=["updates", "custom"]):
print(chunk)

(mode, chunk) 元组的示例结构如下

1
2
('values', {'messages': [HumanMessage(content='What is the weather in SF?', additional_kwargs={}, response_metadata={}, id='0f0b79db-5e22-419f-ab04-128853a7e84d')]})
('custom', 'Looking up data for city: SF')

custom 模式

要在 LangGraph 节点或工具内部发送 自定义用户定义数据 ,请按照以下步骤操作:

  1. 使用 get_stream_writer 访问流写入器并发出自定义数据。
  2. 在调用 .stream().astream() 时设置 stream_mode="custom",以便在流中获取自定义数据。您可以组合多种模式(例如,["updates", "custom"]),但至少必须有一个是 "custom"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from langchain.agents import create_agent
from langgraph.config import get_stream_writer

def get_weather(city: str) -> str:
"""获取指定城市的天气。"""
writer = get_stream_writer()
# 可流式输出任意自定义数据
writer(f"Looking up data for city: {city}") # 推送实时更新(会出现在 stream 的 custom channel)
# ...执行耗时操作(HTTP 请求、爬取等)
writer(f"Acquired data for city: {city}") # 再次推送进度
return f"It's always sunny in {city}!" # 常规返回(会作为 ToolMessage / Tool 的输出被捕获)


agent = create_agent(
model=llm,
tools=[get_weather],
)

# 调用端:同时订阅 values(完整状态)和 custom(工具 writer 输出)
for mode, chunk in agent.stream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode=["values", "custom"], # 同时请求两类流
):
if mode == "values":
# chunk 是当前完整 state 快照(通常包含 messages 列表)
print("LATEST:", chunk["messages"][-1])
print(type(chunk["messages"][-1]))
else:
# mode == "custom": chunk 是工具 writer 推送的文本(或自定义结构)
print("TOOL STREAM:", chunk)
print("-----")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LATEST: content='What is the weather in SF?' additional_kwargs={} response_metadata={} id='a30e826d-8ab9-4287-a28a-d5ce79617bcd'
<class 'langchain_core.messages.human.HumanMessage'>
-----
LATEST: content='' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 266, 'total_tokens': 287, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-23cb42ff-3d21-4a81-8ec5-44e0650307a6', 'finish_reason': 'tool_calls', 'logprobs': None} id='lc_run--b44564e9-3368-46ae-9e5d-46773a801329-0' tool_calls=[{'name': 'get_weather', 'args': {'city': 'SF'}, 'id': 'call_70a8894b68e744fca508db9c', 'type': 'tool_call'}] usage_metadata={'input_tokens': 266, 'output_tokens': 21, 'total_tokens': 287, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
<class 'langchain_core.messages.ai.AIMessage'>
-----
TOOL STREAM: Looking up data for city: SF
-----
TOOL STREAM: Acquired data for city: SF
-----
LATEST: content="It's always sunny in SF!" name='get_weather' id='d43e7aac-d1e1-4432-8709-666e1647c412' tool_call_id='call_70a8894b68e744fca508db9c'
<class 'langchain_core.messages.tool.ToolMessage'>
-----
LATEST: content="The weather in San Francisco (SF) is always sunny! Let me know if you'd like more details or updates. 😊" additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 308, 'total_tokens': 334, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-5b146ff6-cd99-493d-83d4-ac84a456c678', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--0daa8b02-2207-4acd-abc5-79c7cc79882f-0' usage_metadata={'input_tokens': 308, 'output_tokens': 26, 'total_tokens': 334, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
<class 'langchain_core.messages.ai.AIMessage'>
-----

流式消息在时间线上的典型顺序(示例):

  1. 模型生成 AIMessage 并发出工具调用(含 tool_call id)
  2. 流中先返回 values 快照(显示模型决定调用工具)
  3. 工具开始运行并用 stream_writer 推送若干 custom 更新(这些会以 mode="custom" 到达)
  4. 工具完成并返回 ToolMessage(最终的工具结果)
  5. 模型看到 ToolMessage 后生成最终 AIMessage(最终回答),最新 values 会包含这个结果

在 LangChain 的 AIMessage 对象中,只要 tool_calls 字段非空(即 len(message.tool_calls) > 0,就表示模型决定调用一个或多个工具。

Lesson 4: Tools

工具是代理调用来执行操作的组件。它们通过允许模型通过定义良好的输入和输出来与世界交互,从而扩展模型的功能。

工具封装了一个可调用函数及其输入模式。这些可以传递给兼容的聊天模型,允许模型决定是否调用工具以及使用什么参数。在这些场景中,工具调用使模型能够生成符合指定输入模式的请求。

工具的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from typing import Literal

from langchain.tools import tool


@tool(
"calculator",
parse_docstring=True,
description=(
"对两个实数执行基础算术运算。"
"当你需要对任何数字进行运算时都可以使用,即便是整数。"
),
)
def real_number_calculator(
a: float, b: float, operation: Literal["add", "subtract", "multiply", "divide"]
) -> float:
"""对两个实数执行基础算术运算。

Args:
a (float): 第一个操作数。
b (float): 第二个操作数。
operation (Literal['add','subtract','multiply','divide']): 要执行的操作。

Returns:
float: 计算结果。

Raises:
ValueError: 当操作无效或尝试被零除时抛出。
"""
print("🧮 调用计算器工具")
# 执行指定的运算
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
if b == 0:
raise ValueError("不允许被零除。")
return a / b
else:
raise ValueError(f"无效的操作: {operation}")

创建工具最简单的方法是使用 @tool 装饰器。默认情况下,函数的文档字符串成为工具的描述,帮助模型理解何时使用它

类型提示是必需的 ,因为它们定义了工具的输入模式。文档字符串应具有信息量且简洁,以帮助模型理解工具的用途。

LangChain 也支持更丰富的描述,下面的示例使用了一种方式:Google 风格的参数描述。配合 parse_docstring=True 使用时,会解析并将参数描述传递给模型。你可以重命名工具并修改其描述。

工具访问上下文

参数名称 用途
config 保留用于内部传递 RunnableConfig
runtime 保留用于 ToolRuntime 参数(访问状态、上下文、存储)

工具可以通过 ToolRuntime 参数访问运行时信息,该参数提供:

  • 状态State - 在执行过程中流动的可变数据(例如,消息、计数器、自定义字段)
  • 上下文Context - 不可变的配置,如用户 ID、会话详情或特定应用的配置
  • 存储Store - 跨对话的持久长期记忆
  • 流式写入器Stream Writer- 在工具执行时流式传输自定义更新
  • 配置Config - RunnableConfig 用于执行
  • 工具调用 ID Tool Call ID - 当前工具调用的 ID
image-20251203175228797

详细查看文档Tools - Docs by LangChain

调用工具运算

1
2
3
4
5
6
7
8
9
10
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[real_number_calculator],
system_prompt="You are a helpful assistant",
)

result = agent.invoke({"messages": [{"role": "user", "content": "what is 3.0 * 4.0"}]})
print(result["messages"][-1].content)
1
2
result = agent.invoke({"messages": [{"role": "user", "content": "what is 3.0 * 4.0"}]})
print(result["messages"][-1].content)

https://smith.langchain.com/public/d5162eac-47d1-421e-adb9-bf6b223b5618/r

Lesson 5: Tools with MCP

模型上下文协议(MCP)为 AI 代理连接外部工具和数据源提供了标准化方式。让我们用 langchain-mcp-adapters 连接一个 MCP 服务器。

mcp的相关知识这里就不介绍了

mcp工具的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_mcp_adapters.client import MultiServerMCPClient
import nest_asyncio

nest_asyncio.apply()

# 连接 mcp-time 服务器以进行时区相关操作
# 该 Go 服务器提供当前时间、相对时间解析、
# 时区转换、时长计算与时间比较等工具
mcp_client = MultiServerMCPClient(
{
"time": {
"transport": "streamable_http",
"url": "https://mcp.api-inference.modelscope.net/adcc1ca6e6d642/mcp"
}
},
)

# 从 MCP 服务器加载工具
mcp_tools = await mcp_client.get_tools()
print(f"Loaded {len(mcp_tools)} MCP tools: {[t.name for t in mcp_tools]}")
1
Loaded 2 MCP tools: ['get_current_time', 'convert_time']

这里看到可以获取两个mcp工具

调用

1
2
3
4
5
result = await agent_with_mcp.ainvoke(
{"messages": [{"role": "user", "content": "What's the time in SF right now?"}]}
)
for msg in result["messages"]:
msg.pretty_print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
================================ Human Message =================================

What's the time in SF right now?
================================== Ai Message ==================================
Tool Calls:
get_current_time (call_719ae124b2b242fd8648fb27)
Call ID: call_719ae124b2b242fd8648fb27
Args:
timezone: America/Los_Angeles
================================= Tool Message =================================
Name: get_current_time

{
"timezone": "America/Los_Angeles",
"datetime": "2025-12-03T02:27:23-08:00",
"day_of_week": "Wednesday",
"is_dst": false
}
================================== Ai Message ==================================

The current time in San Francisco is 2:27 AM on Wednesday, December 3, 2025.

Lesson 6: Memory

Memory 是让 Agent 记住先前交互信息的系统,分为 短时记忆(Short-term Memory)长时记忆(Long-term Memory) 两种。 它让 Agent 能记住用户偏好、对话历史,实现个性化交互。

LangGraph支持两种对于构建对话代理至关重要的内存类型:

  • 短期内存:通过在会话中维护消息历史来跟踪正在进行的对话。
  • 长期内存:在不同会话之间存储用户特定或应用程序级别的数据。
特性 Checkpointer Store
范围 单线程完整状态 跨线程 key-value
自动保存 每个 super-step 手动调用
用途 对话历史、时间旅行 用户偏好、配置
访问 thread_id namespace + key
image-20250715094855646

在LangGraph中

  • 短期内存也称为线程级内存
  • 长期内存也称为跨线程内存

添加记忆

1
2
3
4
5
6
7
8
9
10
11
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
from langchain_core.messages import SystemMessage

agent = create_agent(
model=llm,
tools=[execute_sql],
system_prompt=SYSTEM_PROMPT,
context_schema=RuntimeContext,
checkpointer=InMemorySaver(),
)

在langchain中,短期记忆的机制我们称之为checkpointer,以上代码中,我们添加了InMemorySaver作为checkpointer,其作用机制为将 checkpoint 数据保存在进程内存中(字典结构),进程结束即丢失。

如果想上生产环境,推荐使用如from langgraph.checkpoint.postgres import PostgresSaver 进行持久化存储

1
2
3
4
5
6
7
8
9
10
question = "This is Frank Harris, What was the total on my last invoice?"
steps = []

for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
stream_mode="values",
context=RuntimeContext(db=db),
):
step["messages"][-1].pretty_print()
steps.append(step)

在这个例子中,在添加checkpointer后,第一次调用时模型会进行tool call进行sql查询,第二次调用时,模型有了记忆,便直接进行回复

1
2
3
4
5
6
================================ Human Message =================================

我是弗兰克·哈里斯,我上一张发票的总金额是多少?
================================== Ai Message ==================================

弗兰克,您上一张发票的总金额是 **$5.94**。

Lesson 7: Structured Output

image-20251204090512625

结构化输出允许代理以特定的、可预测的格式返回数据。你无需解析自然语言响应,而是直接获得应用程序可以直接使用的 JSON 对象、Pydantic 模型或数据类等结构化数据。

LangChain 的 create_agent 会自动处理结构化输出。用户设置所需的结构化输出模式,当模型生成结构化数据时,它会被捕获、验证,并以代理状态中的 'structured_response' 键返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain.agents import create_agent
from pydantic import BaseModel, Field


class ContactInfo(BaseModel):
"""用户信息结构"""
name: str = Field(..., description="姓名")
email: str = Field(..., description="电子邮件")
phone: str = Field(..., description="电话号码")


agent = create_agent(model=llm, response_format=ContactInfo)

recorded_conversation = """ We talked with John Doe. He works over at Example. His number is, let's see,
five, five, five, one two three, four, five, six seven. Did you get that?
And, his email was john at example.com. He wanted to order 50 boxes of Captain Crunch."""

result = agent.invoke(
{"messages": [{"role": "user", "content": recorded_conversation}]}
)

# 访问结构化响应
result["structured_response"]

Pydantic 的 Field(..., description="...") 主要好处是 自动生成高质量的工具描述、API 文档和模型提示 ,让 LLM 更好地理解字段含义,提高工具调用准确率。

虽然langchain支持大家使用其他方式进行结构化输出,我还是最推荐使用pydantic,原因如下

特性 TypedDict Pydantic BaseModel
类型检查 运行时无验证 运行时验证 + IDE 提示
验证 ❌ 无 ✅ 内置验证(正则、长度、格式)
序列化 手动 model_dump() / model_dump_json()
文档生成 ❌ 手动 ✅ 自动(Field description)
错误处理 静默失败 ✅ 抛出 ValidationError

Lesson 8: Middleware

Middleware是插入 Agent 执行流程的 可插拔组件 ,用于在关键节点拦截、修改或增强数据流,实现 动态提示 状态管理 错误处理 工具控制 等功能。

LangChain 中间件的设计就是 “即插即用” 开发者在使用中间件的过程中,只需要关注中间件的功能与需求是否匹配,不需要关心中间件调用的位置。

middleware 参数是专为 langchain.agents.create_agent() 设计的LangGraph 用节点+边替代了中间件,提供更细粒度控制和更好的性能

image-20251204135208941

LangChain 中间件有 6种类型钩子 ,分为 节点式(Node-style) 包装式(Wrap-style) 两大类。

1️⃣ 节点式钩子(Node-style Hooks)

顺序执行,用于日志、验证、状态更新。

钩子名 执行时机 用途
beforeAgent Agent 开始前(一次) 加载状态、验证输入
beforeModel 模型调用前(每次) 修改提示、裁剪消息
afterModel 模型调用后(每次) 验证输出、防护栏
afterAgent Agent 结束后(一次) 保存结果、清理

2️⃣ 包装式钩子(Wrap-style Hooks)

嵌套执行,控制 handler 调用次数(0/1/多次)。

钩子名 执行时机 用途
wrapModelCall 包装模型调用 重试、缓存、转换
wrapToolCall 包装工具调用 错误处理、权限

wrapModelCall vs beforeModel 区别

特性 beforeModel wrapModelCall
控制粒度 只读:只能修改输入 完全控制:可修改输入输出
返回值 dict | None(状态更新) AIMessage(完整响应)
调用模型 必须调用 ✅ 可选调用
执行时机 模型前 模型前后(包装)
用途 预处理(裁剪、日志) 控制流(重试、缓存)

Dynamic Prompt 动态提示词

静态提示词无法适应不同的用户需求和运行时上下文,而动态提示词让你根据实际情况实时调整LLM的行为。

想象你有一个客服助手。如果你用静态提示词,每个用户都会得到完全相同的回复风格。但实际上:

  • 专家用户想要深入的技术细节
  • 新手用户需要简化的解释
  • VIP用户可能需要更高的优先级处理

动态提示词就是在这一刻决定如何指导LLM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SYSTEM_PROMPT_TEMPLATE = """You are a careful SQLite analyst.

Rules:
- Think step-by-step.
- When you need data, call the tool `execute_sql` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows unless the user explicitly asks otherwise.
{table_limits}
- If the tool returns 'Error:', revise the SQL and try again.
- Prefer explicit column lists; avoid SELECT *.
"""

from langchain.agents.middleware.types import ModelRequest, dynamic_prompt

@dynamic_prompt
def dynamic_system_prompt(request: ModelRequest) -> str:
if not request.runtime.context.is_employee:
table_limits = "- Limit access to these tables: Album, Artist, Genre, Playlist, PlaylistTrack, Track."
#仅限访问这些表:Album、Artist、Genre、Playlist、PlaylistTrack、Track。
else:
table_limits = ""

return SYSTEM_PROMPT_TEMPLATE.format(table_limits=table_limits)

构建动态提示词,根据is_employee的不同,构建不同的提示词

1
2
3
4
5
6
7
8
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[execute_sql],
middleware=[dynamic_system_prompt],
context_schema=RuntimeContext,
)

作为中间件加入create_agent

1
2
3
4
5
6
7
8
question = "Frank Harris 最昂贵的一次购买是什么?"

for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
context=RuntimeContext(is_employee=False, db=db),
stream_mode="values",
):
step["messages"][-1].pretty_print()

is_employee=False的回复

1
2
3
4
5
6
7
8
================================ Human Message =================================

Frank Harris 最昂贵的一次购买是什么?
================================== Ai Message ==================================

要确定 Frank Harris 最昂贵的一次购买,我们需要查看与他相关的购买记录。然而,在当前可访问的表(Album、Artist、Genre、Playlist、PlaylistTrack、Track)中,并没有包含客户信息或购买记录的表(如 Customer 或 Invoice)。因此,无法直接查询 Frank Harris 的购买信息。

如果您有其他相关表的信息或更多上下文,请提供进一步的细节!

is_employee=True的回复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
================================ Human Message =================================

Frank Harris 最昂贵的一次购买是什么?
================================== Ai Message ==================================

我需要找出 Frank Harris 最昂贵的一次购买。首先,我需要确认数据库中是否有包含客户姓名和购买信息的表。
Tool Calls:
execute_sql (call_9d0bb26b2b5b4d5ca2a24584)
Call ID: call_9d0bb26b2b5b4d5ca2a24584
Args:
query: SELECT name FROM sqlite_master WHERE type='table';
================================= Tool Message =================================
Name: execute_sql

[('Album',), ('Artist',), ('Customer',), ('Employee',), ('Genre',), ('Invoice',), ('InvoiceLine',), ('MediaType',), ('Playlist',), ('PlaylistTrack',), ('Track',)]
================================== Ai Message ==================================

我看到有 `Customer` 表和 `Invoice` 表,可能还有 `InvoiceLine` 表包含购买详情。首先,我会查看 `Customer` 表的结构,以确认如何找到 Frank Harris。
Tool Calls:
execute_sql (call_7bfbcb4928be435ab7bda4c0)
Call ID: call_7bfbcb4928be435ab7bda4c0
Args:
query: PRAGMA table_info(Customer);
================================= Tool Message =================================
Name: execute_sql
...
[(145, '2010-09-23 00:00:00', 13.86)]
================================== Ai Message ==================================

Frank Harris 最昂贵的一次购买是发票号 145,日期为 2010-09-23,总金额为 13.86。

Lesson 9: Human in the Loop

人机回路(HITL)中间件允许您为代理工具调用添加人工监督。当模型提议一个可能需要审核的操作时——例如,写入文件或执行 SQL——中间件可以暂停执行并等待决策。

它通过将每个工具调用与可配置的策略进行比对来实现这一点。如果需要干预,中间件会发出一个中断信号来停止执行。图状态会使用 LangGraph 的持久化层进行保存,因此执行可以安全地暂停并在稍后继续。

然后由人工决策决定下一步的操作:操作可以原样批准(approve)、在运行前进行修改(edit),或附带反馈被拒绝(reject)。

Interrupt 决策类型

中间件定义了三种人类可以响应中断的方式:

决策类型 描述 示例用例
approve 操作按原样批准并执行,无需更改。 按原文发送邮件草稿
✏️ edit 工具调用已修改后执行。 发送邮件前更改收件人
reject 工具调用被拒绝,并在对话中添加了说明。 拒绝邮件草稿并解释如何重写它

每种工具可用的决策类型取决于你在 interrupt_on 中配置的策略。当多个工具调用同时被暂停时,每个操作都需要单独的决策。决策必须按照中断请求中操作出现的顺序提供。

配置Interrupt

要使用 HITL,在创建代理时,将 中间件添加到代理的 middleware 列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
model=llm,
tools=[execute_sql],
system_prompt=SYSTEM_PROMPT,
checkpointer=InMemorySaver(),
context_schema=RuntimeContext,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"execute_sql": {#工具的名称。当agent尝试调用这个工具时,中间件会拦截
"allowed_decisions": ["approve", "reject"]#这个工具允许的人工决定类型
}
}
),
],
)

产生中断并恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from langgraph.types import Command

question = "What are the names of all the employees?"
#所有员工的姓名是什么?
config = {"configurable": {"thread_id": "1"}}

result = agent.invoke(
{"messages": [{"role": "user", "content": question}]},
config=config,
context=RuntimeContext(db=db)
)

if "__interrupt__" in result:# 检查执行结果中是否存在中断
# 获取最后一个中断对象的值
# result['__interrupt__'] 是一个列表,包含所有中断
# ['action_requests'] 获取需要审核的操作列表
description = result['__interrupt__'][-1].value['action_requests'][-1]['description']
print(f"\033[1;3;31m{80 * '-'}\033[0m")
print(
f"\033[1;3;31m Interrupt:{description}\033[0m"
)
print(result['__interrupt__'][-1])
# 调用agent继续执行,传入人工的审核决定
result = agent.invoke(
Command(# 使用Command对象恢复暂停的对话
resume={# resume字段包含人工的决定列表
"decisions": [
{
# 决定类型:拒绝该操作
"type": "reject",
# 拒绝的原因(会作为反馈发给agent)
"message": "the database is offline."#数据库已离线。
}
]
}
),
config=config, # 使用相同的线程 ID 以恢复暂停的对话
context=RuntimeContext(db=db),
)
print(f"\033[1;3;31m{80 * '-'}\033[0m")

print(result["messages"][-1].content)

输出

1
2
3
4
5
6
7
--------------------------------------------------------------------------------
Interrupt:Tool execution requires approval

Tool: execute_sql
Args: {'query': 'SELECT name FROM employees;'}
--------------------------------------------------------------------------------
The database is currently offline. Please try again later.

重要概念:__interrupt__ 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 中断对象 - 表示agent执行暂停,需要人工审核
Interrupt(
# value 字段包含中断的详细信息
value={
# action_requests 列表 - 包含所有需要人工审核的工具调用
'action_requests': [
{
# 工具的名称 - agent想要执行的工具
'name': 'execute_sql',

# args 字典 - 传递给工具的参数
'args': {
# SQL查询语句 - 具体要执行的数据库操作
'query': 'SELECT name FROM employees;'
},

# description 字符串 - 人类可读的完整描述(用于展示给审核员)
'description': "Tool execution requires approval\n\nTool: execute_sql\nArgs: {'query': 'SELECT name FROM employees;'}"
}
],

# review_configs 列表 - 包含审核的策略配置
'review_configs': [
{
# action_name 字符串 - 这个审核策略应用于哪个工具
'action_name': 'execute_sql',

# allowed_decisions 列表 - 人工审核员允许做出的决定类型,这里只允许两种决定:
'allowed_decisions': ['approve', 'reject']
}
]
},

# id 字符串 - 中断的唯一标识符(UUID)
# 用于追踪和关联这个特定的中断请求
# 在日志、审计等场景中用于识别具体是哪次中断
id='eee3e43d71fb8f3747759c8cbe9b5499'
)

MMGraphRAG

流程说明:

image-20251211192220984

索引阶段 (Indexing)

目标: 将原始的多模态数据(文本和图像)转化为结构化的多模态知识图谱(MMKG)。

Text2Graph: 对文本输入进行分块并提取实体,构建文本知识图谱 (Text-based KG)

Image2Graph: 图像通过场景图 (scene graphs) 精炼视觉内容。这包括使用 YOLOv8 进行语义分割、使用多模态大语言模型(MLLM)生成特征块描述、提取实体和关系(包括显式和隐式关系),并构建描述整个图像的全局实体。从而构建图像知识图谱 (Image-based KG)

跨模态知识融合 (Cross-Modal Knowledge Fusion): 这是核心步骤,通过跨模态实体链接 (CMEL) 将文本 KG 和图像 KG 融合。

​ ▪ 论文采用了基于谱聚类 (Spectral Clustering) 的优化策略来高效地生成候选实体对,该方法结合了实体间的语义和结构信息,从而增强了 CMEL 任务的准确性。

​ ▪ 融合步骤还包括:对未对齐的图像实体进行描述增强,以及对全局图像实体进行对齐。

输出: 多模态知识图谱 (MMKG)。该框架采用基于节点的 MMKG (N-MMKG) 范式,将图像视为独立的节点,以保留更丰富的语义信息和可扩展性。

检索阶段 (Retrieval)

目标: 根据用户查询,在 MMKG 中提取相关知识线索。

混合粒度检索 (Hybrid Granularity Retriever): 检索模块沿着 MMKG 中的多模态推理路径,提取相关的实体、关系和上下文信息。

生成阶段 (Generation)

目标: 整合检索到的多模态线索,生成最终答案。

混合生成策略 (Hybrid Generation Strategy):

​ ▪ 首先,大型语言模型 (LLM) 生成初步的文本响应。

​ ▪ 随后,多模态大语言模型 (MLLM) 基于视觉和文本信息生成多个多模态响应。

​ ▪ 最后,LLM 将这些响应整合并增强,输出一个统一且连贯的最终答案。

概念解析

场景图(Scene Graphs)

场景图是 MMGraphRAG 框架在索引阶段用于处理视觉信息的核心工具,d。

定义和作用: 场景图用于精炼视觉内容,将图像信息转化为实体和关系。通过构建场景图,MMGraphRAG 能够将原始视觉输入转换为结构化的图像知识图谱(Image-based KG),。

结构和信息捕获: 传统的场景图方法通常会忽略细粒度的语义细节和物体之间隐藏的信息,导致在下游推理任务中产生偏差。相比之下,MMGraphRAG 采用基于多模态大语言模型(MLLM)的方法来生成场景图。这种方法能够:

◦ 通过语义分割和 MLLM 的推理能力提取实体并推断出显式关系(例如:“女孩”——“拿着相机”——“相机”)和隐式关系(例如:“男孩”——“男孩和女孩看起来很亲密,可能是朋友或情侣”——“女孩”),。

◦ 为视觉实体提供更丰富的语义描述,例如将基本的标签“男孩”细化为更详细的表达,如“眼睛疲惫的大学生”。

构建过程: 在 MMGraphRAG 的 Img2Graph 模块中,场景图的构建流程包括图像语义分割、为每个特征块生成文本描述、提取实体和关系,以及构建描述整个图像的全局实体,。

跨模态实体链接(Cross-Modal Entity Linking, CMEL)

CMEL 是实现跨模态融合和构建统一 MMKG 的关键组成部分

定义: CMEL 的目标是对齐从图像和文本中提取的实体,即识别指代同一现实世界概念的图像实体和文本实体对,。

在 MMGraphRAG 中的作用: 它是跨模态融合模块的第一步,也是最关键的一步。它负责在文本知识图谱(Text-based KG)图像知识图谱(Image-based KG)之间建立连接。

与传统方法的区别:

传统实体链接(EL):仅将文本实体与知识库中的对应条目关联,忽略非文本信息。

多模态实体链接(MEL):将视觉信息作为辅助属性来增强实体与知识库条目之间的对齐,但无法建立超出这些辅助关联的跨模态关系,。

CMEL:更进一步,它将视觉内容视为独立的实体,并将这些视觉实体与其文本对应物对齐,从而构建 MMKG 并促进显式的跨模态推理

基于谱聚类(Spectral Clustering-Based)的方法

基于谱聚类的方法是 MMGraphRAG 针对 CMEL 任务候选实体生成这一挑战提出的优化策略,。

目标: 在 CMEL 任务中,由于文本实体数量通常大于视觉实体,需要高效且鲁棒地为每个视觉实体生成一组候选文本实体,。

优势: 现有的聚类方法(如 KMeans、DBSCAN)依赖语义相似性,但忽略图结构;而图聚类方法(如 PageRank、Leiden)关注结构关系,但在稀疏图上表现不佳。基于谱聚类的方法解决了这两个方面的问题,它同时捕获实体间的语义信息和结构信息,。

具体实现:

◦ 该方法通过重新设计加权邻接矩阵 A度矩阵 D 来实现语义和结构的整合。

邻接矩阵 A 的构建同时反映了节点间的余弦相似性(语义信息)和它们之间关系的重要性(结构信息),其中关系的重要性由 LLM 评估。

度矩阵 D 的对角线值表示节点与其所有其他节点之间的总加权相似度

◦ 随后,遵循标准的谱聚类步骤,构建拉普拉斯矩阵并进行特征分解,然后利用 DBSCAN 在特征向量空间(矩阵 Q 的行空间)上进行聚类,从而获得簇划分,。

效果: 实验结果显示,该方法在 CMEL 任务上的表现显著优于其他聚类和嵌入方法,将微观准确率提高了约 15%,宏观准确率提高了约 30%。

混合粒度检索(Hybrid Granularity Retrieval)

混合粒度检索是 MMGraphRAG 检索阶段的核心功能,。

定义: 在接收到用户查询后,检索模块在构建好的多模态知识图谱(MMKG)内部执行检索。

检索内容: 这种检索方式会提取不同粒度的相关信息,包括实体(Entities)关系(Relationships) 上下文信息/文本块(Contextual Information/Text Chunks),。

机制: 检索是沿着 MMKG 内的多模态推理路径进行的,。由于 MMKG 将图像建模为独立的节点,并明确地链接了视觉和文本实体,这种结构化的检索(基于推理路径)能够比传统的基于嵌入相似度的检索(如 NaiveRAG)更精确地检索出与问题相关的视觉内容,并支持复杂的跨模态推理,,。检索结果随后用于指导生成过程,。

核心贡献

论文的三个主要核心贡献如下:

提出首个基于知识图谱的多模态 RAG 框架 (MMGraphRAG)

MMGraphRAG 是第一个基于知识图谱(KG)的多模态 RAG 框架。它旨在实现深度跨模态融合和推理,其设计具有强大的可扩展性和适应性。

A. 创新性的索引阶段(构建 MMKG)

MMGraphRAG 的核心在于其索引阶段,它将原始的多模态数据(文本和图像)转化为统一的多模态知识图谱 (MMKG)

视觉内容精炼: 图像信息首先通过场景图(Scene Graphs) 显式和隐式关系,从而生成高精度和细粒度的场景图,将原始视觉输入转化为图像知识图谱

MMKG 范式: 论文采用了基于节点的 MMKG(Node-based MMKG, N-MMKG)范式。在这种范式中,图像被视为独立的节点,而非仅仅是文本实体的属性(Attribute-MMKG, A-MMKG)。这种设计避免了将视觉数据存储为属性时带来的信息损失,保留了更丰富的语义信息,并显着增强了跨模态推理能力和图的灵活性与可扩展性。

B. 结构化检索与生成

检索: 检索模块在 MMKG 内部沿着多模态推理路径执行混合粒度检索,从而提取相关的实体、关系和上下文信息,以指导生成过程。

混合生成策略: 生成阶段采用混合策略,首先由 LLM 生成初步的文本响应,然后由 MLLM 根据视觉和文本信息生成多模态响应。最后,LLM 将两者整合为一个统一且连贯的最终答案。这种方法有效缓解了当前 MLLM 在推理上的限制,确保了高质量、上下文适当的响应。

跨模态实体链接(CMEL)的创新方法和基准数据集

为解决构建 MMKG 中跨模态实体对齐的关键挑战,论文在 CMEL 任务上做出了重要贡献。

A. 提出基于谱聚类的 CMEL 优化策略

挑战: 准确地为每个视觉实体从文本实体池中生成一组候选实体对,是一个高效且鲁棒性的挑战。

解决方案: 论文设计了基于谱聚类的优化策略,用于高效生成候选实体。这种方法通过重新设计邻接矩阵 A 和度矩阵 D同时捕获实体间的语义信息和结构信息。这极大地增强了 CMEL 任务的准确性,实验结果表明其在微观准确率上提升了约 15%,在宏观准确率上提升了约 30%,显著优于其他聚类和嵌入方法。

B. 构建并发布 CMEL 数据集

目的: 为了解决该领域缺乏统一基准评估的问题。

内容: 构建并发布了 CMEL 数据集,这是一个专门针对细粒度多实体对齐设计的新型基准,其在实体多样性和关系复杂性上都高于现有基准。该数据集包含来自新闻、学术和小说三个不同领域的文档,总共提供了 1,114 个对齐实例

实验验证:实现最先进性能和高鲁棒性

MMGraphRAG 框架在多模态文档问答(DocQA)任务上进行了全面的评估,验证了其优势:

达到 SOTA 性能: MMGraphRAG 在 DocBenchMMLongBench 这两个多模态 DocQA 基准数据集上均取得了最先进的性能,显着优于现有的 RAG 基线方法(包括 LLM、MLLM、NaiveRAG 和 GraphRAG)。

跨域适应性: MMGraphRAG 表现出强大的跨领域适应性,尤其在具有高视觉结构复杂性的领域(如学术金融)中,相比于纯文本的 RAG 方法有实质性提升。

处理“不可回答”问题的优势: 该框架在处理不可回答(Unanswerable, Una.)的问题时显示出明显的优势。由于其通过 MMKG 进行结构化推理,MMGraphRAG 能够更可靠地评估问题是否可答,从而减少生成误导性答案,增强了在真实世界场景中的鲁棒性。

提供可解释性: 该框架通过可追溯的推理路径来指导多模态推理

GraphRAG

多模态 GraphRAG 的尝试: 针对多模态数据,HM-RAG 提出了一个分层多智能体多模态 RAG 框架。该框架通过协调分解智能体、多源检索智能体和决策智能体,从结构化、非结构化和基于图的数据中动态地合成知识。

HM-RAG 的不足: 尽管 HM-RAG 在多模态处理方面有所进步,但它仍然依赖于通过多模态大语言模型(MLLMs)将多模态内容转换为文本未能充分捕获跨模态关系,从而导致逻辑链不完整

实体链接 (Entity Linking

传统实体链接(EL): 传统 EL 方法将文本实体与其在知识库中的对应条目关联起来,但忽略了非文本信息

多模态实体链接(MEL): MEL 扩展了 EL,它将视觉信息作为辅助属性纳入进来,以增强实体与知识库条目之间的对齐。

MEL 的局限性: 然而,MEL 并未在这些辅助关联之外建立跨模态关系,从而限制了真正的跨模态交互。

跨模态实体链接(CMEL): CMEL 更进一步,它将视觉内容视为实体,并将这些视觉实体与其文本对应物进行对齐,从而构建多模态知识图谱(MMKGs),并促进显式的跨模态推理

CMEL 研究现状与挑战: 当前,CMEL 领域的研究仍处于早期阶段,缺乏统一的理论框架和可靠的评估协议。例如,MATE 基准用于评估 CMEL 性能,但其合成的 3D 场景未能捕捉现实世界图像的复杂性和多样性

MMGraphRAG 对 CMEL 的贡献:

• 为了弥补这一差距,MMGraphRAG 构建了一个具有更高现实世界复杂性的 CMEL 数据集

• 同时,MMGraphRAG 提出了基于谱聚类的方法用于候选实体生成,旨在推动 CMEL 研究的进一步发展。

Image2Graph 模块

Img2Graph 模块通过一个五步流程将图像映射为知识图谱:

图像分割 (Image Segmentation):

◦ 这是第一步,使用 YOLOv8 等工具执行语义分割,将图像划分为具有独立语义意义的区域,这些区域被称为图像特征块(image feature blocks),。

◦ 分割的粒度会显著影响知识图谱中边缘描绘的精度。

图像特征块描述 (Image Feature Block Description):

◦ 接下来,使用 MLLM 为每个分割后的特征块生成文本描述

◦ 这些描述不仅为图像模态构建了独立的实体,也为后续与文本模态的对齐提供了桥梁。模型会根据特征块的类别(物体、生物或人物)来生成详细描述,例如描述人物的性别、发型、衣着和姿势等,。

实体和关系提取 (Entity and Relation Extraction):

◦ 此步骤利用 MLLM 识别图像中的显式关系隐式关系,并提取实体。

◦ 这些提取出的实体和关系为知识图谱的多模态扩展提供了结构化信息。

图像特征块与实体的对齐 (Alignment of Image Feature Blocks with Entities):

◦ 通过 MLLM 的识别和推理能力,将分割生成的特征块与它们对应的视觉实体进行对齐,。

◦ 例如,将“特征块 2”识别为“男孩”的图像,并在知识图谱中建立关系,这加强了模态间的关联。

全局实体构建 (Global Entity Construction):

◦ 最后,为整个图像构建一个全局实体,作为知识图谱中的全局节点。

◦ 该全局节点提供对图像整体信息的补充描述(例如:“在桥上相遇”),并通过与局部实体的连接,增强了知识图谱的完整性。

image-20251212102050866

基于谱聚类的候选实体生成(Spectral Clustering-Based Candidate Generation)

CMEL 的目标是识别指代同一现实世界概念的图像实体和文本实体对。由于文本实体的数量通常大于视觉实体,CMEL 任务被分解为两个阶段:

  1. 为每个视觉实体生成一组候选文本实体,以及 (2) 从该集合中选择最佳对齐的文本实体。

阶段一:候选实体的生成

基于谱聚类的方法主要解决了第一个阶段,即候选实体的生成。

方法创新

现有的候选实体生成方法存在局限性:

  1. 基于距离的聚类方法(如 KMeans 和 DBSCAN)依赖语义相似性,但忽略了图结构。
  2. 基于图的聚类方法(如 PageRank 和 Leiden)捕获结构关系,但在稀疏图上效果不佳。

为了解决这些问题,MMGraphRAG 提出了一种专门为 CMEL 定制的谱聚类算法,该算法能够同时捕获实体之间的语义信息和结构信息

核心机制

该方法重新设计了加权邻接矩阵 A度矩阵 D,以融合语义和结构信息:

  1. 邻接矩阵 A 的构建: 矩阵 A 反映了节点之间的相似性以及它们之间关系的重要性
    • 其定义为 Apq = sim(vp, vq) ⋅ weight(rpq)
    • 其中,vp 是实体 ep 的嵌入向量,sim(⋅) 表示余弦相似度。
    • weight(rpq) 是由大型语言模型(LLM)评估的关系 rpq 的重要性标量(如果两个实体之间没有关系,则权重设置为 1)。
  2. 度矩阵 D 的构建: D 是一个对角矩阵,对角线上的每个值 Dpp 表示节点 p 的总加权连接强度,即节点 p 与所有其他节点之间总的加权相似度。

随后,按照标准的谱聚类步骤,构建拉普拉斯矩阵并执行特征分解。利用最小的 m 个特征向量形成矩阵 Q

候选实体生成

最后,在矩阵 Q 的行空间上使用 DBSCAN 进行聚类,得到簇划分 C1, C2, …, Cn。对于每个视觉实体 ek(Ii),算法会根据其嵌入向量 vk(Ii) 与簇成员之间的余弦相似度来选择最相关的簇。该簇中的所有实体构成了最终的候选实体集 C(ek(Ii))

0. 场景设定

  • 输入(视觉实体):一张红富士苹果的照片 eimg
  • 数据库(文本实体池):我们有 5 个文本实体(节点),它们都包含“Apple”或相关概念,导致传统方法容易混淆。
    1. T1: “Fresh Fuji Apple”(新鲜红富士苹果 - 水果
    2. T2: “Red Delicious Apple”(红蛇果 - 水果
    3. T3: “Apple iPhone 15”(苹果手机 - 科技
    4. T4: “Apple MacBook Pro”(苹果电脑 - 科技
    5. T5: “Banana and Fruit Salad”(香蕉水果沙拉 - 水果,但没有 Apple 这个词)

第一步:构建邻接矩阵 A (融合语义与 LLM)

我们需要计算每两个文本实体之间的“亲密度”。公式是 Apq = sim(vp, vq) ⋅ weight(rpq)

  1. 语义相似度 (Sim)

假设我们计算出 T1 (“Fresh Fuji Apple”) 与其他词的向量相似度:

  • T2 (“Red Delicious”): 0.9 (都很像)
  • T3 (“Apple iPhone”): 0.7 (因为都有单词 “Apple”,向量空间里靠得较近,这是传统方法的陷阱)
  • T5 (“Banana…”): 0.5 (属于水果,但词不一样)
  • LLM 权重 (Weight)

这里 MMGraphRAG 的核心创新来了。我们问 LLM:“‘新鲜红富士’和‘iPhone 15’在现实世界中关系紧密吗?”

  • LLM 答:关系很弱,它们属于不同领域。 → Weight = 0.1
  • LLM 答:‘新鲜红富士’和‘红蛇果’都是水果。 → Weight = 1.0
  • 计算最终矩阵 A

让我们看看 T1T3 的连接发生了什么变化:

  • T1 vs T2 (水果 vs 水果): 0.9(Sim) × 1.0(LLM) = 0.9 (强连接)
  • T1 vs T3 (水果 vs 科技): 0.7(Sim) × 0.1(LLM) = 0.07 (连接被切断!)

关键点:LLM 成功把“水果苹果”和“科技苹果”原本虚高的相似度打压下去了。


第二步:构建度矩阵 D

计算每个节点与其他所有节点的总连接强度。

  • T1 (Fuji Apple): 连接 T2 (0.9) + 连接 T5 (0.4) + 连接 T3 (0.07)… 1.4
  • T3 (iPhone): 连接 T4 (MacBook, 强连接 0.9) + 连接 T1 (0.07)… 1.0

这反映了节点在各自簇内的“人缘”。


第三步:谱变换与特征分解 (生成矩阵 Q)

构建拉普拉斯矩阵并分解后,我们将这 5 个文本实体映射到一个新的坐标系(比如 2D 平面)。

在这个新空间里,因为我们在第一步切断了“水果”和“科技”的强联系:

  • T1, T2, T5 会紧紧聚在坐标系左下角。
  • T3, T4 会紧紧聚在坐标系右上角。
  • 它们之间的距离被拉得非常大,不再是原来混在一起的状态。

第四步:DBSCAN 聚类

在矩阵 Q 的坐标系上运行 DBSCAN。

  • 输入:上述分散的坐标点。
  • 过程
    1. DBSCAN 发现 T1, T2, T5 密度很高,划分为 簇 C1 (水果簇)
    2. DBSCAN 发现 T3, T4 密度很高,划分为 簇 C2 (科技簇)
    3. 如果有一个 T6 “SpaceX Rocket”,它离谁都远,DBSCAN 可能会把它标记为噪声并扔掉(优于 K-Means 的点)。

第五步:候选实体生成 (匹配图片)

现在我们有了两个干净的候选池:

  • C1: {Fuji Apple, Red Delicious, Banana Salad}
  • C2: {iPhone 15, MacBook Pro}

最终匹配:

  1. 输入图片的向量 vimg (红富士照片)。
  2. 计算 vimgC1 中成员的平均相似度 0.85 (很高)。
  3. 计算 vimgC2 中成员的平均相似度 0.15 (很低)。

输出结果:

算法选择 簇 C1 作为最终的候选集合。

阶段二:从筛选出的候选集中确定最佳对齐结果

  1. 背景: 在 CMEL 任务中,第二阶段是从为每个视觉实体(visual entity)生成的候选实体集中选出最匹配的文本实体。这个过程通过基于 LLM 的推理来实现,因为 LLM 在复杂的对齐场景中展示了高准确性和适应性。
  2. 提示(Prompt)内容: 为了指导 LLM 完成实体对齐,向其提供的提示中包含以下关键信息:
    • 视觉实体的名称和描述(the name and description of the visual entity)
    • 来自所选簇的候选实体的描述(descriptions of candidate entities from the selected cluster)。这些候选实体是通过前一步骤的基于谱聚类的候选生成方法(Spec)得到的。
    • 一套固定的对齐示例(a fixed set of alignment examples),用于指导 LLM。
  3. 最终输出: LLM 基于上述提示内容进行推理判断后,其输出被采纳为最终的对齐结果(The output is adopted as the final alignment result)

简而言之,这段文字是 MMGraphRAG 框架跨模态知识融合模块(Cross-Modal Fusion Module)*执行 CMEL 任务时,利用 **LLM 进行最终实体对齐**的*输入信息(Prompt)构成结果决定的说明。

CMEL (Cross-Modal Entity Linking) 数据集

您提供的这段文字是对 CMEL (Cross-Modal Entity Linking) 数据集 的详细介绍,该数据集是为了解决跨模态实体链接任务中缺乏评估基准而专门构建和发布的。

以下是对这段内容的详细解释:

1. CMEL 数据集的构成和领域多样性

CMEL 数据集是一个新颖的基准,专门用于评估复杂多模态场景下的细粒度跨实体对齐(cross-entity alignment)任务。

  • 数据来源和领域: CMEL 数据集包含来自三个不同领域的文件,确保了广泛的领域多样性和实际适用性:
    • 新闻(news)
    • 学术(academia)
    • 小说(novels)
  • 每个样本的内容: 数据集中的每个样本都包含三个核心组件:
    • (i) 基于文本块构建的文本知识图谱(text-based KG built from text chunks)
    • (ii) 源自每张图像的场景图的基于图像的知识图谱(image-based KG derived from per-image scene graphs)
    • (iii) 原始 PDF 格式文档(the original PDF-format document)
  • 对齐实例总数和分布: CMEL 数据集总共提供了 1,114 个对齐实例(alignment instances)。这些实例按领域分布如下:
    • 来自新闻文章的实例:87 个。
    • 来自学术论文的实例:475 个。
    • 来自小说的实例:552 个。

CMEL 数据集相比现有基准(如 MATE)具有更强的实体多样性和关系复杂性,并且支持通过半自动化流程进行扩展。

2. 评估指标(Evaluation Metrics)

为了全面评估跨模态实体链接(CMEL)的性能,该研究采用了两种不同的准确率指标:微观准确率(micro-accuracy)*和*宏观准确率(macro-accuracy)

指标 计算方式 目的/反映的性能
微观准确率 (Micro-accuracy) 按实体(per-entity)计算。即所有正确预测的实体数占总实体数的比例。 反映了整体预测的正确性,是全局性能的指标。
宏观准确率 (Macro-accuracy) 按文档(per document)计算平均准确率。即每个文档的准确率的平均值。 旨在减轻评估偏差,这种偏差由不同文档中实体分布不平衡引起。更好地突出了不同方法在不同领域的性能。

实验设置与结果(Experimental Setup and Results)

“实验设置与结果”(Experimental Setup and Results)部分详细介绍了 MMGraphRAG 框架的评估方法,主要分为两部分:针对 CMEL 任务的评估,以及针对多模态文档问答(DocQA)任务的整体框架性能评估。

1. 跨模态实体链接(CMEL)实验设置与结果

CMEL 实验的目的是验证 MMGraphRAG 提出的基于谱聚类的候选实体生成方法(Spec)在复杂多模态场景下的有效性。

实验设置

  • 数据集: 实验基于新构建和发布的 CMEL 数据集,该数据集专为细粒度多实体对齐设计,包含来自新闻、学术和小说三个不同领域的 1,114 个对齐实例。

  • 评估指标: 采用微观准确率 (micro-accuracy)宏观准确率 (macro-accuracy)

    • 微观准确率按实体计算,反映了整体预测的正确性(全局性能)。
    • 宏观准确率按文档计算平均准确率,旨在减轻实体分布不平衡导致的评估偏差,并更好地突出方法在不同领域中的性能。
  • 对比方法: 实验涵盖三类方法,并与主流聚类算法进行了全面比较:

    1. 基于嵌入的方法 (Emb): 使用预训练嵌入模型(如 stella-en-1.5B-v5),通过计算余弦相似度来确定候选实体。
    2. 基于 LLM 的方法 (LLM): 利用 LLM(如 Qwen2.5-72B-Instruct)直接基于上下文理解能力生成候选实体集。
    3. 聚类基线: 包括 DBSCAN (DB)、KMeans (KM)、PageRank (PR) 和 Leiden (Lei)。
    • 统一处理: 所有聚类方法和基线,其候选集内的最终实体对齐都是通过统一的基于 LLM 的推理完成的。

关键实验结果(CMEL)

  • 聚类方法的优势: 总体而言,基于聚类的方法在 CMEL 任务中的表现显著优于基于嵌入和基于 LLM 的方法。
  • Spec 性能最佳: MMGraphRAG 的基于谱聚类的 Spec 方法表现最佳。与其他聚类方法相比,Spec 将微观准确率提高了约 15%宏观准确率提高了约 30%
  • 具体结果(Table 1 所示最佳配置): Spec 在整体微观/宏观准确率上达到了 65.5%/56.9%,明显优于排名第二的 Leiden (54.8%/44.7%)。

2. 多模态文档问答(DocQA)实验设置与结果

DocQA 实验用于评估 MMGraphRAG 框架在多模态信息集成、复杂推理和领域适应性方面的整体性能。

实验设置

  • 评估任务: 选择 DocQA 作为主要评估任务,因为它能全面评估方法在处理长文档、集成多样格式以及跨领域适应性的能力。
  • 基准数据集:
    • DocBench: 包含 229 份 PDF 文档,涵盖学术、金融、政府、法律和新闻五个领域,问题类型包括纯文本 (Txt.)、多模态 (Mm.) 和不可回答 (Una.)。
    • MMLongBench: 包含 135 份长 PDF 文档,证据格式包括文本 (Txt.)、图表/表格 (C.T.)、布局 (Lay.) 和图 (Fig.)。
  • 评估基线:
    • LLM: 通过 MLLM 将图像转换为文本后,输入 LLM(例如 Qwen2.5-72B-Instruct)。
    • MLLM: 直接输入图像块和问题,评估其多模态推理能力(例如 InternVL2.5-38B-MPO)。
    • NaiveRAG (NRAG): 基于嵌入相似度的文本块检索。
    • GraphRAG (GRAG): 基于知识图谱的 RAG,使用局部模式查询。

关键实验结果(DocQA)

  • MMGraphRAG (MMGR) 表现: MMGraphRAG 在 DocBench 和 MMLongBench 数据集上都显著优于所有现有的 RAG 基线方法。
    • 在 DocBench 上的总体准确率达到 60.5%(对比 NRAG 的 43.6% 和 GRAG 的 39.6%)。
    • 在 MMLongBench 上的总体准确率达到 39.6%,F1 分数达到 34.1%(对比 NRAG 的 22.3%/20.9% 和 GRAG 的 18.2%/19.3%)。
  • 多模态融合优势: MMGraphRAG 在多模态问题上的准确率(DocBench 上 MMGR 88.7%)显著高于 GraphRAG(26.0%),证明了跨模态融合对于复杂推理至关重要。
  • 跨领域适应性: 相比纯文本 RAG 方法,MMGraphRAG 在学术和金融等具有高视觉结构复杂性的领域获得了显著提升,表明其在专业领域中具有出色的适应性和泛化能力。
  • 不可回答问题处理: MMGraphRAG 在处理不可回答问题(Una.)时表现出明显优势。这归因于其通过 CMEL 实现完整和细粒度的跨模态信息交互,并在 MMKG 上进行结构化推理,从而更可靠地评估问题是否可回答,减少了误导性答案的生成。

DocBench 数据集

DocBench 数据集是 MMGraphRAG 框架在多模态文档问答(DocQA)任务中用于评估其整体性能的主要基准之一

以下是关于 DocBench 数据集的详细介绍:

1. 目的与作用

DocBench 的主要作用是作为一个综合性基准,用于评估基于大型语言模型的文档阅读系统(LLM-based document reading systems)的性能,。

在 MMGraphRAG 的实验中,选择 DocQA(文档问答)作为主要评估任务,因为 DocBench 这类基准能够全面评估方法在以下方面的能力:

  • 多模态信息集成。
  • 复杂推理。
  • 领域适应性。
  • 处理长文档和集成多种格式的能力。

2. 数据构成与领域覆盖

DocBench 数据集包含来自公开在线资源的 229 份 PDF 文档

它涵盖了五个不同的领域(Domains),确保了评估的广泛性:

  1. 学术 (academia/Aca.),。
  2. 金融 (finance/Fin.),。
  3. 政府 (government/Gov.),。
  4. 法律 (laws/Law.),。
  5. 新闻 (news/News),。

3. 问题类型 (Question Types)

DocBench 数据集的问题涵盖了多种类型,以测试模型的不同能力。它包括四种类型的问题,但在 MMGraphRAG 的实验中,排除了其中一类:

  1. Txt. (Pure Text Questions):纯文本问题。
  2. Mm. (Multimodal Questions):多模态问题,需要整合文本和视觉信息才能回答。
  3. Una. (Unanswerable Questions):不可回答问题,文档中缺乏答案证据。
  4. Metadata Questions:元数据问题。

注意: 在 MMGraphRAG 的实验中,由于信息被转换成了知识图谱(KG),因此元数据问题被排除在统计之外

4. 评估机制

在实验中,DocBench 依靠大型语言模型(LLM)来确定答案的正确性。具体来说,在 MMGraphRAG 的实验中,Llama3.1-70B-Instruct 被用于评估 DocBench 上的答案正确性。

MMGraphRAG 在 DocBench 数据集上取得了显著的优势,其总体准确率达到了 60.5%,明显优于 NaiveRAG 和 GraphRAG 等现有 RAG 基线方法,,。特别是在处理多模态问题(Mm.)上,MMGraphRAG 的准确率高达 88.7%,。

DeepSeek-OCR

2D 光学映射(Optical 2D Mapping)

2D 光学映射(Optical 2D Mapping)*是 DeepSeek-OCR 提出的一种创新技术,旨在*利用视觉模态作为高效的压缩介质,将长文本上下文压缩为少量的视觉 Token

以下是基于来源对 2D 光学映射的详细解析:

1. 核心概念与动机

大语言模型(LLM)在处理长文本时面临巨大的计算挑战,因为其计算量随序列长度呈平方级增长。2D 光学映射的思路在于:一张包含文档文字的图片(视觉模态)所代表的信息量,通常远超同等数量的数字文本 Token。因此,通过将文本“映射”为视觉表示,可以实现极高的光学压缩(Optical Compression)率。

2. 技术实现方案:DeepSeek-OCR

DeepSeek-OCR 是实现 2D 光学映射的实验性模型,它建立了一套完整的“压缩-解压”映射机制:

  • 压缩(视觉编码器 - DeepEncoder): 这是核心引擎,它将高分辨率的输入图像(包含文字内容)通过 16 倍卷积压缩器进行处理。它能在保持极少视觉 Token 数量的同时,捕捉到图像中的关键文本信息。
  • 解压(解码器 - MoE Decoder): 采用 DeepSeek3B-MoE 架构,学习如何从 DeepEncoder 产生的压缩隐空间 Token 中重新构建原始文本表示。

2D 光学映射就像是将一叠厚厚的文字资料(长文本)拍摄成一张高像素的照片(视觉 Token)。虽然照片本身只占用了极小的存储空间(Token 数量少),但只要有一个视力极佳的观察者(解码器),依然能从照片中清晰地还原出原本所有的文字内容。

压缩率(Compression Ratio)

根据提供的来源,在 DeepSeek-OCR 的研究语境下,压缩率(Compression Ratio)*是指*视觉-文本 Token 压缩率(Vision-Text Token Compression Ratio)

以下是详细定义及其在技术中的重要性:

1. 核心定义与公式

压缩率用于衡量视觉 Token(Vision Tokens)作为压缩介质存储文本信息的效率。根据来源,其具体的计算方式为: 压缩率 = 原始文本的 Token 数量(Ground Truth Text Tokens) / 模型使用的视觉 Token 数量(Vision Tokens Used)

例如,如果一段包含 1000 个文字 Token 的文本被压缩成 100 个视觉 Token,其压缩率就是 10×

2. 性能表现与阈值

来源提供了 DeepSeek-OCR 在不同压缩率下的识别精度(Decoding Precision)指标:

  • 10倍以内(< 10×): 解码精度可达 97% 左右,被视为近乎无损的压缩。
  • 10-12倍(10-12×): 识别准确率仍能保持在 90% 左右。
  • 20倍(20×): 这是该技术的极限测试点,此时准确率下降至约 60%

3. 为什么需要这个指标?

  • 解决计算瓶颈: 大语言模型(LLM)处理长文本时,计算量随序列长度呈平方级增长。通过高压缩率,可以用极少的 Token 代表极其丰富的信息,从而大幅降低计算开销。
  • 探索记忆机制: 压缩率的调整可以模拟人类的遗忘机制。通过降低分辨率(即增加压缩率),可以让陈旧的信息变得模糊(消耗更少 Token),而让近期信息保持清晰(消耗更多 Token),从而实现理论上无限长的上下文处理。
  • 衡量模型效率: 相比其他模型(如使用近 7000 个 Token 的 MinerU2.0),DeepSeek-OCR 追求在更少的视觉 Token 下(如少于 800 个)实现同等或更优的解析效果,这直接体现在更高的压缩率上。

压缩率就像是行李箱的分层收纳效率。原本需要 10 个大箱子才能装下的散装衣服(长文本 Token),通过某种神奇的折叠技术(2D 光学映射),现在只需要 1 个箱子(视觉 Token)就能装走。压缩率越高,意味着这个箱子折叠衣服的技术越厉害,装下的东西越多。

什么是魔法方法

Python 的魔法方法(Magic Methods),也叫特殊方法(Special Methods)或双下方法(dunder methods,因为名字前后都有双下划线 __),是 Python 类中预定义的一类方法,用于定义对象在特定操作下的行为。

当你使用像 +len()str()in[] 等语法时,Python 其实是在背后调用对应的魔法方法。

常见魔法方法举例

1. __init__:初始化对象

1
2
3
4
5
class Person:
def __init__(self, name):
self.name = name

p = Person("Alice") # 调用 __init__

2. __str____repr__:控制对象的字符串表示

  • __str__ 用于 str(obj)print(obj),面向用户。
  • __repr__ 用于调试,面向开发者,通常返回可执行的代码字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
return f"Point({self.x}, {self.y})"

def __repr__(self):
return f"Point(x={self.x}, y={self.y})"

p = Point(1, 2)
print(p) # Point(1, 2) ← 调用 __str__
print(repr(p)) # Point(x=1, y=2) ← 调用 __repr__

3. __len__:支持 len(obj)

1
2
3
4
5
6
7
8
9
class MyList:
def __init__(self, items):
self.items = items

def __len__(self):
return len(self.items)

my_list = MyList([1, 2, 3])
print(len(my_list)) # 3

4. __getitem__ / __setitem__:支持 obj[key]obj[key] = value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vector:
def __init__(self, data):
self.data = data

def __getitem__(self, index):
return self.data[index]

def __setitem__(self, index, value):
self.data[index] = value

v = Vector([10, 20, 30])
print(v[1]) # 20
v[1] = 99
print(v[1]) # 99

5. __add__:支持 + 运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

def __repr__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)

6. __eq__:支持 == 比较

1
2
3
4
5
6
7
8
9
10
class Book:
def __init__(self, title):
self.title = title

def __eq__(self, other):
return self.title == other.title

b1 = Book("Python")
b2 = Book("Python")
print(b1 == b2) # True

7. __call__:让对象像函数一样被调用

1
2
3
4
5
6
7
8
9
class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
return x * self.factor

double = Multiplier(2)
print(double(5)) # 10

参考资料

【Python 特性】魔法方法:让你的类和原生类一样顺滑_哔哩哔哩_bilibili

🕰️ 什么是长轮询(Long Polling)?

长轮询 是一种在 没有 WebSocket 或 Server-Sent Events(SSE) 的环境下,模拟“服务器推送” 的技术。

客户端发起 HTTP 请求后,服务器不会立即响应,而是挂起连接,直到有新数据可返回超时才发送响应;客户端收到响应后立即重新发起请求,从而实现准实时的双向通信

❌ 但最坏情况是:

  • 前端在 t=0s 发起请求
  • 新数据在 t=0.1s 产生
  • 后端没检测到(比如检查间隔是 1 秒)
  • 后端一直等到 t=10s 超时,才返回“无数据”
  • 前端在 t=10s 收到响应,显示“没更新”
  • 然后立刻发起下一轮请求(t=10s)
  • 后端这次检测到数据(t=10.1s)→ 返回
  • 前端在 t=10.2s 才显示!

延迟 = 10.1 秒!

🧠 核心问题:检测粒度 + 请求对齐

长轮询的延迟主要来自两个地方:

延迟来源 说明
1. 后端检查间隔 如果后端每 1 秒检查一次数据,那么数据产生后最多要等 1 秒才被发现
2. 请求发起时机 如果数据在“上一轮请求刚结束、下一轮还没发”时产生,就要等下一轮超时结束

💡 即使后端“一有数据就返回”,前提是它在“当前这个请求还在挂起时”检测到了数据

🌐 什么是 WebSocket?

WebSocket 是一种全双工通信协议,建立在 TCP 之上,允许客户端和服务器之间实时、双向地传输数据。

对比传统的 HTTP:

  • HTTP 是请求-响应模式:客户端发请求,服务器响应,然后连接关闭。
  • WebSocket 是持久连接:连接建立后,双方可以随时主动发消息,适合聊天、实时通知、在线游戏等场景。

🔁 WebSocket vs HTTP 轮询(举例说明)

假设你要做一个实时股票价格更新页面:

  • HTTP 轮询:前端每 1 秒发一次请求问“价格变了吗?”,服务器回答。浪费带宽,延迟高。
  • WebSocket:连接一次,服务器价格一变就主动推给前端,高效实时。

🧱 WebSocket 通信流程(简化版)

  1. 客户端发起 HTTP 请求,带 Upgrade: websocket 头。
  2. 服务器同意升级协议,返回 101 Switching Protocols
  3. 连接升级为 WebSocket,之后双方通过这个连接发消息(不是 HTTP 了!)。
  4. 任意一方可主动关闭连接。

websocket通信图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sequenceDiagram
participant Client as 客户端 (React)
participant Server as 服务器 (FastAPI)

Note over Client,Server: 1. 建立 WebSocket 连接
Client->>Server: GET /ws + Upgrade: websocket
Server-->>Client: 101 Switching Protocols

Note over Client,Server: 2. 双向通信阶段
Client->>Server: send("Hello")
Server-->>Client: send("服务器回声: Hello")
Server->>Client: send("主动推送: 2秒过去了!")
Client-->>Server: send("收到推送,谢谢!")

Note over Client,Server: 3. 任意一方可关闭连接
Client->>Server: close()
Server-->>Client: 连接关闭

与长轮询对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sequenceDiagram
participant C as 客户端
participant S as 服务器

Note over C,S: WebSocket(高效)
C->>S: 握手 (HTTP Upgrade)
S-->>C: 101 Switching
loop 实时通信
S->>C: 数据 (任意时刻)
C->>S: 数据 (任意时刻)
end

Note over C,S: 长轮询(低效)
loop 每次都要新建请求
C->>S: GET /poll
S-->>C: 等待...(最多30秒)
S->>C: 响应(有/无数据)
C->>S: GET /poll (立刻重发)
end

参考资料

WebSockets原理,握手和代码实现!用Socket.io制作实时聊天室_哔哩哔哩_bilibili

为什么有http还要有websocket(上)_哔哩哔哩_bilibili

HTTP Cookie(通常简称为 Cookie)是由 服务器通过 HTTP 响应头 Set-Cookie 发送给用户代理(如浏览器)的一小段数据,用户代理随后会在后续的、满足条件的 HTTP 请求中,通过 Cookie 请求头自动将其发送回服务器。

  • 核心目的:在无状态的 HTTP 协议之上,实现 状态管理(State Management)客户端数据持久化

浏览器会:

  1. 保存它
  2. 在后续同域名的请求中,自动通过 Cookie 请求头发回给服务器

🔑 Cookie 的核心作用:在无状态的 HTTP 协议上实现“会话跟踪”

前端构建

使用 Vite 快速创建 React 项目

1
2
#用最新版 Vite 脚手架,在名为 react-app 的文件夹中,创建一个基于 React 的项目。
npm create vite@latest react-app -- --template react

Vite(发音同 “veet”,法语“快”)是一个现代化的前端构建工具,由 Vue.js 作者尤雨溪(Evan You)开发,但不仅限于 Vue——它对 React、Svelte、Lit 等主流框架都有官方支持。

1
2
3
4
#安装项目依赖
npm install
#启动本地开发服务器
npm run dev

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
react-app/
├── public/ # 静态资源目录
├── src/ # 源代码目录
│ ├── components/ # 组件目录
├── Login.jsx # 登录组件
├── Login.css # 登录样式
├── Profile.jsx # 用户信息组件
└── Profile.css # 用户信息样式
│ ├── App.jsx # 主应用组件
│ ├── main.jsx # 应用入口文件
│ └── *.css # 样式文件
├── package.json # 项目配置和依赖
├── index.html
└── vite.config.js # 构建工具配置

启动链条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 浏览器加载 index.html

2. 解析到 <script src="/src/main.jsx">

3. 加载并执行 main.jsx

4. main.jsx 中的 createRoot(document.getElementById('root'))

5. 找到 <div id="root"></div>

6. 将 <App /> 组件渲染到 root 容器中

index.html (HTML容器)

main.jsx (入口文件) ← 你当前查看的文件

App.jsx (主应用组件)

Login.jsx, Profile.jsx (子组件)

index.html

脚本标签指定入口

1
2
<script type="module" src="/src/main.jsx"></
script>
  • src=“/src/main.jsx” 明确指定 了入口文件路径
  • type=“module” 表示这是ES6模块,支持import/export语法
  • 浏览器加载这个脚本时,就会执行main.jsx中的代码

main.jsx 中的这行代码:

1
createRoot(document.getElementById('root'))

index.html 中的:

1
<div id="root"></div>

通过 id=“root” 建立了连接!

main.jsx

main.jsx 是整个React应用的 入口文件

1
2
3
4
5
6
7
8
9
10
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

创建React根节点

1
createRoot(document.getElementById('root'))
- 在HTML页面中找到id为’root’的DOM元素 - 创建一个React根渲染器,这是React应用挂载的地方

渲染应用

1
2
3
4
5
.render(
<StrictMode>
<App />
</StrictMode>,
)
  • 将整个React应用渲染到根节点
  • StrictMode 包裹应用,提供开发时的额外检查和警告
  • 是你的主应用组件

App.jsx

组件(Component) 是React的核心概念,可以理解为:

  • 可复用的UI构建块
  • 独立的功能单元
  • 像乐高积木一样可以组合的代码片段

以App组件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function App() {
// 1. 状态管理
const [isLoggedIn, setIsLoggedIn] =
useState(false)

// 2. 事件处理函数
const handleLoginSuccess = () => {
setIsLoggedIn(true)
}

// 3. 渲染UI
return (
<div className="app">
<header>...</header>
<main>
{!isLoggedIn ? (
<Login onLoginSuccess=
{handleLoginSuccess} />
) : (
<Profile isLoggedIn={isLoggedIn} /
>
)}
</main>
<footer>...</footer>
</div>
)
}

export default App

🔍 组件的核心特征

  1. 函数式组件
1
2
3
4
5
6
function App() {
// 组件逻辑
return (
// JSX - 描述UI结构
)
}
  • App是一个 JavaScript函数
  • 函数名就是 组件名 (必须大写开头)
  • 返回 JSX (类似HTML的语法)
  1. 状态管理(State)
1
const [isLoggedIn, setIsLoggedIn] = useState(false)

isLoggedIn - 状态变量 - 存储 当前的登录状态 - 初始值是 false (未登录) - 只能 读取 ,不能直接修改

setIsLoggedIn - 状态更新函数

  • 用来 更新 isLoggedIn 的值
  • 调用时会 触发组件重新渲染
  • 是修改状态的 唯一正确方式

useState(false) - Hook调用

  • false 是 初始值 ,表示默认未登录
  • 返回一个 数组 : [当前值, 更新函数]
  1. 事件处理
1
2
3
const handleLoginSuccess = () => {
setIsLoggedIn(true)
}
  • 组件可以 响应用户交互
  • 处理点击、输入等事件
  • 更新状态,触发UI更新
  1. 条件渲染
1
2
3
4
5
6
{!isLoggedIn ? (
<Login onLoginSuccess=
{handleLoginSuccess} />
) : (
<Profile isLoggedIn={isLoggedIn} />
)}
  • 根据 状态条件 显示不同内容
  • 动态决定渲染哪些子组件
  1. 组件组合
1
2
3
<Login onLoginSuccess={handleLoginSuccess} /
>
<Profile isLoggedIn={isLoggedIn} />
  • 大组件由 小组件组合 而成
  • 通过 Props 传递数据给子组件

组件组合完整的数据流

  1. 父组件(App.jsx)定义函数
1
2
3
4
5
6
7
8
9
10
11
12
13
function App() {
const [isLoggedIn, setIsLoggedIn] =
useState(false)

// 定义回调函数
const handleLoginSuccess = () => {
setIsLoggedIn(true) // 更新父组件的状态
}

// 将函数传递给子组件
return <Login onLoginSuccess=
{handleLoginSuccess} />
}
  1. 子组件(Login.jsx)接收并使用
1
2
3
4
5
6
7
8
9
10
11
12
function Login({ onLoginSuccess }) {  // 通过Props接收函数
const handleLogin = async () => {
// ... 登录逻辑

if (response.ok) {
// 登录成功时调用父组件传来的函数
onLoginSuccess() // 🔥 关键:通知父组件
}
}

return <button onClick={handleLogin}>登录</button>
}

Login.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Login({ onLoginSuccess }) {
// 1. 管理登录状态
const [isLoading, setIsLoading] = useState(false)
const [message, setMessage] = useState('')

// 2. 处理登录逻辑
const handleLogin = async () => {
// 调用后端API
const response = await fetch('http://localhost:8000/login', {
method: 'POST',
credentials: 'include', // Cookie认证
})

// 成功后通知父组件
if (response.ok) {
onLoginSuccess() // 调用父组件传来的回调函数
}
}

// 3. 渲染登录界面
return (
<div className="login-container">
<button onClick={handleLogin}>
{isLoading ? '登录中...' : '登录'}
</button>
</div>
)
}

Profile.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function Profile({ isLoggedIn }) {
// 1. 管理用户数据状态
const [userInfo, setUserInfo] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')

// 2. 获取用户信息
const fetchProfile = async () => {
const response = await fetch('http://localhost:8000/profile', {
credentials: 'include', // 使用Cookie认证
})
const data = await response.json()
setUserInfo(data)
}

// 3. 生命周期管理
useEffect(() => {
fetchProfile() // 组件加载时自动获取数据
}, [isLoggedIn])

// 4. 条件渲染
if (!isLoggedIn) {
return <p>请先登录查看用户信息</p>
}

return (
<div className="profile-container">
<h2>用户信息</h2>
<div>用户ID: {userInfo?.user_id}</div>
</div>
)
}

后端构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from fastapi import FastAPI, Response, Request
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有前端地址
allow_credentials=True, # 允许携带 Cookie
allow_methods=["*"],# 允许所有 HTTP 方法
allow_headers=["*"],# 允许所有 HTTP 头
)

@app.post("/login")
def login(response: Response):
response.set_cookie(key="user_id", value="123", httponly=True)
return {"msg": "Logged in"}

@app.get("/profile")
def profile(request: Request):
user_id = request.cookies.get("user_id")
return {"user_id": user_id}


def main():
import uvicorn
print("Starting FastAPI backend server...")
uvicorn.run(
"backend.app:app",
host="127.0.0.1",
reload=True,#支持热重载
port=8000,
log_level="info"
)


if __name__ == "__main__":
main()

CORS(Cross-Origin Resource Sharing) = 跨域资源共享

否则会出现

1
2
3
Access to fetch at 'http://localhost:8000/login' 
from origin 'http://localhost:5173'
has been blocked by CORS policy
  • 从 http://localhost:5173 (前端)
  • 访问 http://localhost:8000/login (后端)
  • 被CORS策略阻止了

cookie机制

  1. POST /login:登录成功,服务器“记住”用户
  2. GET /profile:获取用户资料,服务器“认出”用户

关键就靠 Cookie 在浏览器和服务器之间传递身份。

🔧 1. response.set_cookie(key="user_id", value="123", httponly=True)

✅ 作用:

让服务器告诉浏览器:“请保存一个叫 user_id 的 Cookie,值是 123

📡 实际发生了什么?

当 FastAPI 执行这行代码时,它会在 HTTP 响应头(Response Headers) 中添加一行:

1
Set-Cookie: user_id=123; HttpOnly

然后整个响应看起来像这样:

1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: user_id=123; HttpOnly

{"msg": "Logged in"}

🌐 浏览器收到后会:

  1. 解析 Set-Cookie
  2. 在本地(内存或磁盘)保存这个 Cookie:
    • 名字:user_id
    • 值:123
    • 属性:HttpOnly(JS 无法读取)
  3. 以后每次向该域名发请求,自动带上这个 Cookie

🔐 参数详解(你用的三个):

参数 作用 安全建议
key="user_id" Cookie 的名字 用语义化名称,如 session_id
value="123" Cookie 的值 不要直接存用户 ID! 应该存随机 session ID(后面讲)
httponly=True 禁止 JavaScript 读取 强烈建议开启,防 XSS 攻击

📥 2. user_id = request.cookies.get("user_id")

✅ 作用:

从当前请求中读取浏览器自动发送的 Cookie

📤 实际发生了什么?

当浏览器访问 /profile 时,它会自动在 HTTP 请求头(Request Headers) 中加上:

1
Cookie: user_id=123

FastAPI 的 request.cookies 是一个字典,.get("user_id") 就是读取这个值。

image-20251026164036642
1
2
3
4
5
6
sequenceDiagram
participant Client as 客户端(React)
participant Server as 服务器(FastAPI)

Client->>Server: 请求(Request)\n- URL: /login\n- Method: POST\n- Body: {}\n- Headers: ...
Server-->>Client: 响应(Response)\n- Status: 200\n- Set-Cookie: user_id=123\n- Body: {"msg": "Logged in"}

Session

Session(会话) 是一种 服务器端状态管理机制,用于在无状态的 HTTP 协议之上,维护客户端与服务器之间的持续交互状态

  • 核心思想:为每个客户端分配一个唯一的会话标识符(Session ID),服务器通过该 ID 查找对应的会话数据。
  • 存储位置:会话数据(如用户身份、权限、购物车等)存储在服务器端(内存、数据库、缓存等)。
  • 传输载体:Session ID 通常通过 Cookie(最常见)、URL 重写或 HTTP 头在客户端与服务器之间传递。

为什么需要session

HTTP 协议是无状态的:

  • 每次请求都是独立的,服务器不知道你是谁
  • 如果你登录后访问个人页面,服务器怎么知道你是谁?

解决方案

  • Cookie:浏览器存数据(但不安全,不能存敏感信息)
  • Session:服务器存数据,浏览器只存 ID(安全!)

💡 Session 是实现“登录状态”的标准方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant Browser
participant Server

Browser->>Server: POST /login (用户名+密码)
Server->>Server: 验证成功,创建 Session
Note right of Server: sessions["abc123"] = {"user_id": "123"}
Server-->>Browser: Set-Cookie: session_id=abc123;

Note over Browser: 浏览器保存 session_id

Browser->>Server: GET /profile<br/>Cookie: session_id=abc123
Server->>Server: 查 sessions["abc123"] → user_id=123
Server-->>Browser: {"user_id": "123"}

后端构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 简化的内存session存储
sessions = {}

@app.post("/login")
def login(response: Response):
# 创建新的session
session_id = str(uuid.uuid4())
session_data = {"user_id": "123", "username": "admin"}

# 将session数据存储到内存字典
sessions[session_id] = session_data

# 设置session cookie
response.set_cookie(
key="session_id",
value=session_id,
httponly=True
)

logger.info(f"用户登录成功,session_id: {session_id}")
return {"msg": "Logged in", "session_id": session_id}

@app.get("/profile")
def profile(request: Request):
# 从cookie中获取session_id
session_id = request.cookies.get("session_id")

if session_id is None:
logger.warning("未找到session cookie")
return {"error": "请先登录", "user_id": None}

# 从内存字典获取session数据
session_data = sessions.get(session_id)

if session_data is None:
logger.error(f"Session不存在: {session_id}")
return {"error": "Session已过期", "user_id": None}

logger.info(f"获取session数据成功: {session_data}")
return {"user_id": session_data.get("user_id"), "username": session_data.get("username")}

重复部分进行省略

JWT

jwt是什么

JWT(JSON Web Token) 是一种 开放标准(RFC 7519),用于在各方之间安全地传输声明(claims)。它是一种紧凑、自包含(self-contained) 的令牌格式,通常用于 身份认证(Authentication)信息交换(Information Exchange)

  • 核心特点
    • 无状态(Stateless):服务器无需存储令牌
    • 自包含:令牌本身包含用户身份和元数据
    • 可验证:通过数字签名确保完整性与真实性

✅ JWT 的本质是 “签名的用户声明”,而非会话 ID。

JWT 的结构

JWT 由三部分组成,用 . 连接:

1
xxxxx.yyyyy.zzzzz
部分 说明 内容示例
Header 令牌类型 + 签名算法 {"alg": "HS256", "typ": "JWT"}
Payload 声明(Claims) {"sub": "123", "name": "Alice", "exp": 1735689600}
Signature 签名(防篡改) HMACSHA256(base64UrlEncode(header)+'.'+base64UrlEncode(payload), secret)

🔐 只有签名部分能防止篡改,Header 和 Payload 只是 Base64Url 编码(可解码!勿存敏感信息)。

JWT 的 sub是什么

在 JSON Web Token (JWT) 中,subSubject(主题)的缩写。

它是 JWT 规范(RFC 7519)中定义的一个注册声明(Registered Claim)。简单来说,sub 用于回答这个问题:“这个 Token 是代表谁的?”

含义:它标识了该 JWT 所面向的主体(Principal)。在绝大多数应用场景中,这个主体就是用户

作用:当服务器收到一个 JWT 时,它通过读取 sub 字段来知道“当前请求是谁发起的”或“当前登录的是哪个用户 ID”。

在 JWT 的 Payload(载荷)部分,sub 通常是这样的:

1
2
3
4
5
6
{
"iss": "https://auth.example.com",
"sub": "1234567890", <-- 这里是 User ID
"name": "John Doe",
"iat": 1516239022
}

为什么用 JWT,相比较session优势在哪

  1. 无状态(Stateless) → 天然支持水平扩展
  • Session:服务器必须存储会话数据(内存/Redis),所有实例需共享存储。
  • JWT:服务器不存储任何状态,任意实例均可独立验证 Token。

🌐 适用场景:微服务架构、Serverless、高并发 API 网关
💡 优势:去中心化、无共享存储依赖、部署简单

  1. 跨域与跨平台原生支持
  • Session:依赖 Cookie,受同源策略限制,跨域需复杂 CORS 配置。
  • JWT:通过 Authorization: Bearer <token> 传输,无 Cookie 依赖

📱 适用场景: - 移动 App(iOS/Android) - 多前端(Web + 小程序 + 桌面端) - 第三方集成(如开放平台 API)

💡 优势:一套 API 服务所有客户端

  1. 自包含(Self-contained) → 减少数据库查询
  • Session:每次请求需查存储(如 Redis)获取用户信息。
  • JWT:Payload 可直接携带用户 ID、角色、权限等信息。
1
2
3
4
5
6
{
"sub": "user123",
"role": "admin",
"permissions": ["read", "write"],
"exp": 1735689600
}

优势减少 I/O,提升性能(尤其对权限频繁校验的系统)

  1. 标准化 & 生态成熟
  • JWT 是 IETF 标准(RFC 7519),各语言均有高质量库。
  • OAuth 2.0、OpenID Connect 深度集成,适合现代身份认证体系。

🔌 优势无缝对接 Auth0、Keycloak、Firebase 等身份提供商

1
2
3
4
5
6
7
8
9
10
11
12
13
sequenceDiagram
participant Client
participant Server

Client->>Server: 1. POST /login (用户名+密码)
Server->>Server: 2. 验证凭证
Server-->>Client: 3. 返回 JWT: {token: "xxxx.yyyy.zzzz"}

Note over Client: 4. 客户端保存 Token(内存/LocalStorage)

Client->>Server: 5. GET /profile<br/>Authorization: Bearer xxxx.yyyy.zzzz
Server->>Server: 6. 验证签名 + 检查 exp
Server-->>Client: 7. 返回受保护资源

JWT的缺点

  1. 无法主动撤销:一旦签发,在过期前始终有效,难以实现“立即登出”或“账号禁用”。
  2. XSS 风险高:通常需存于前端(如内存或 localStorage),若存在 XSS 漏洞,Token 易被窃取。
  3. Payload 可解码:Header 和 Payload 仅 Base64 编码,不是加密,不能存敏感信息。
  4. 体积较大:每次请求都要携带完整 Token,增加带宽开销(相比 Session ID)。

jwt三种签名算法

1. HS256 (HMAC with SHA-256)

类型:对称加密 (Symmetric)

这是最简单、最常见的算法,适合单体应用或内部受信任的服务之间通信。

具体原理

“共享密钥”模式。

签发 Token 的一方(认证服务器)和验证 Token 的一方(应用服务器)必须持有完全相同的密钥(Secret)。

  1. 签名过程: 将 Header 和 Payload 进行 Base64Url 编码,用 . 连接。然后使用 Secret 对这个字符串进行 SHA-256 哈希计算。
  2. 验证过程: 接收方收到 Token 后,用同一个 Secret 对 Header 和 Payload 再次进行同样的哈希计算。如果计算出的结果与 Token 中的签名一致,则验证通过。

核心公式

$$Signature = HMACSHA256(base64Url(Header) + "." + base64Url(Payload), secret)$$

举例说明

场景: 你是一个独自开发的网站站长。

Secret: "my_super_secret_password"(只有你的服务器知道)。

流程:

  1. 用户登录,你的代码生成 Payload {"user": "admin"}
  2. 代码混合 Secret 进行哈希,生成签名 abc123...
  3. 当用户下次带着 Token 请求时,你的代码再次用 "my_super_secret_password" 算一遍。如果算出来也是 abc123...,说明 Token 没被黑客改过。

优点: 速度极快,生成签名极其简单。

缺点: 密钥一旦泄露,黑客既能验证也能伪造任意 Token。不适合多方系统(因为要把密钥分发给所有验证者,风险扩散)。

2. RS256 (RSA Signature with SHA-256)

类型:非对称加密 (Asymmetric)

这是企业级应用、微服务架构和 OAuth2/OIDC(如 Auth0, Okta)中的行业标准

具体原理

“私钥签名,公钥验证”模式。 使用一对密钥:私钥 (Private Key)公钥 (Public Key)

  • 私钥: 只有认证服务器(Auth Server)拥有,绝不公开。用于生成签名
  • 公钥: 可以公开给任何服务(资源服务器、网关等)。用于验证签名

核心原理

RSA 利用了大数因数分解的数学难题。私钥加密的数据,只能用公钥解密(在签名场景下,这意味着只有公钥能验证是由私钥签名的)。

举例说明

  • 场景: Google 颁发 Token 给第三方 App(如 Notion)。
  • 私钥: Google 只有一把,锁在 Google 的保险柜里。
  • 公钥: 公布在网上(JWKS 端点),Notion 可以随时下载。
  • 流程:
    1. Google 用私钥给 Payload {"sub": "123"} 盖了一个“数字章”(签名)。
    2. Notion 收到 Token,去 Google 下载公钥
    3. Notion 用公钥验证这个“章”。如果验证通过,Notion 就能 100% 确定这个 Token 是 Google 签发的,而不是黑客伪造的。
  • 优点: 安全性高。即使公钥泄露,黑客也只能验证 Token,无法伪造 Token。非常适合微服务(Auth 服务发 Token,其他几十个微服务只拿公钥验 Token)。
  • 缺点: 签名速度比 HS256 慢,生成的签名字符串较长。

3. ES256 (ECDSA using P-256 and SHA-256)

类型:非对称加密 (Asymmetric - Elliptic Curve)

这是目前推荐的新兴标准。它在保持非对称加密安全性的同时,解决了 RSA 的性能和体积问题。

具体原理

“椭圆曲线”模式。 同样使用公钥和私钥,但基于椭圆曲线密码学 (ECC)。 与 RSA 相比,ECC 可以在密钥长度短得多的情况下,提供同等甚至更高的安全性。

核心原理

它利用了椭圆曲线上的离散对数问题。简而言之,在一个特定的数学曲线上做点运算非常容易,但反向推导非常困难。

举例说明

场景: 需要高并发、低带宽的物联网 (IoT) 设备或移动端 App。

对比 RSA:

  • 要达到 128 位安全级别,RS256 需要 3072 位的密钥(生成的 Token 会很长,占用流量)。
  • ES256 只需要 256 位的密钥(Token 非常短,传输快)。

流程: 逻辑与 RS256 一样(私钥签、公钥验),但数学计算过程不同。

优点:

  1. Token 更短:节省带宽,HTTP Header 更小。
  2. 生成速度快:在某些硬件上,签名速度比 RSA 更快。

缺点: 相对较新,极老旧的系统可能不支持;数学原理比 RSA 更复杂,排查问题稍难。

后端构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from pydantic import BaseModel
from datetime import datetime, timedelta
import jwt

# JWT配置
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"

# 简单的用户数据
users = {
"admin": {"user_id": "123", "username": "admin", "password": "admin123"}
}


def create_token(username: str) -> str:
"""创建JWT token"""
# 设置token过期时间为24小时
expire = datetime.utcnow() + timedelta(hours=24)
# 构建JWT载荷,包含用户名和过期时间
payload = {
"sub": username, # subject: 用户名
"exp": expire # expiration: 过期时间
}
# 使用密钥和算法对载荷进行编码生成token
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> str:
"""验证JWT token"""
# 解码JWT token获取载荷信息
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# 从载荷中提取用户名
username = payload.get("sub")
return username

@app.post("/login")
def login(response: Response):
"""用户登录"""
# 简化登录,直接使用默认用户
username = "admin"
# 从用户字典中获取用户信息
user = users.get(username)

# 为用户创建JWT token
token = create_token(username)
# 将token设置为httponly cookie,与现有前端兼容
response.set_cookie(key="session_id", value=token, httponly=True)
# 返回token信息和登录成功消息
return {"access_token": token, "token_type": "bearer", "msg": "Logged in"}

@app.get("/profile")
def profile(request: Request):
"""获取用户信息"""
# 首先尝试从cookie获取token(兼容现有前端)
token = request.cookies.get("session_id")

# 如果cookie中没有token,尝试从Authorization header获取
if not token:
auth_header = request.headers.get("authorization")
# 检查是否为Bearer token格式
if auth_header and auth_header.startswith("Bearer "):
# 提取Bearer后面的token部分
token = auth_header.split(" ")[1]

# 验证token并获取用户名
username = verify_token(token)
# 根据用户名获取用户信息
user = users.get(username)
# 返回用户ID和用户名
return {"user_id": user["user_id"], "username": user["username"]}
image-20251026164113650

参考资料

JWT身份认证算法、落地方案及优缺点_哔哩哔哩_bilibili

Cookie、Session、Token究竟区别在哪?如何进行身份认证,保持用户登录状态?_哔哩哔哩_bilibili

JWT身份认证算法、落地方案及优缺点_哔哩哔哩_bilibili

REST api

REST(Representational State Transfer) 是一种软件架构风格,用于设计网络应用程序的 API。
REST API 就是遵循 REST 原则的 Web 接口,通常基于 HTTP 协议。

✅ 核心思想:把一切看作“资源”(Resource),通过标准 HTTP 方法对资源进行操作。

REST 的六大约束(简化版理解)

约束 含义 举例
统一接口 所有操作通过标准 HTTP 方法(GET/POST/PUT/DELETE) /books 表示“图书资源”
无状态 服务器不保存客户端状态,每次请求必须包含全部信息 用 Token 认证,而不是 Session
可缓存 响应应标明是否可缓存 GET 请求通常可缓存
分层系统 客户端无需知道是否直接连服务器(可经过代理、网关) Nginx 反向代理 FastAPI
按需代码(可选) 可返回可执行代码(如 JS) 较少用
资源标识 每个资源有唯一 URI /books/123 唯一标识 ID 为 123 的书

常用 HTTP 方法与语义

方法 含义 幂等性 安全性 例子
GET 获取资源 ✅ 是 ✅ 是 获取所有图书
POST 创建资源 ❌ 否 ❌ 否 新增一本图书
PUT 完整更新资源 ✅ 是 ❌ 否 替换 ID 为 123 的图书全部信息
PATCH 部分更新资源 ❌ 否 ❌ 否 只修改图书的标题
DELETE 删除资源 ✅ 是 ❌ 否 删除 ID 为 123 的图书

🔔 幂等性:多次执行结果相同(如 DELETE /books/123,删一次和删十次效果一样)
🔔 安全性:不改变服务器状态(GET 是安全的,POST 不是)

举个 FastAPI 的完整例子

假设你正在开发图书管理系统,下面是典型 REST API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

app = FastAPI()

# 模拟数据库
books_db = [
{"id": 1, "title": "Python 入门", "author": "张三"},
{"id": 2, "title": "FastAPI 实战", "author": "李四"}
]

class BookCreate(BaseModel):
title: str
author: str

class Book(BookCreate):
id: int

@app.get("/books", response_model=List[Book])
def get_books():
return books_db

@app.post("/books", response_model=Book)
def create_book(book: BookCreate):
new_id = max(b["id"] for b in books_db) + 1 if books_db else 1
new_book = {"id": new_id, **book.dict()}
books_db.append(new_book)
return new_book

@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
for book in books_db:
if book["id"] == book_id:
return book
raise HTTPException(status_code=404, detail="Book not found")

@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, book: BookCreate):
for b in books_db:
if b["id"] == book_id:
b.update(book.dict())
return b
raise HTTPException(status_code=404, detail="Book not found")

@app.delete("/books/{book_id}")
def delete_book(book_id: int):
for i, b in enumerate(books_db):
if b["id"] == book_id:
books_db.pop(i)
return {"message": "Deleted"}
raise HTTPException(status_code=404, detail="Book not found")

启动后,你就可以通过:

  • GET /books → 获取所有书
  • POST /books → 创建新书
  • GET /books/1 → 获取 ID=1 的书
  • PUT /books/1 → 完全替换 ID=1 的书
  • DELETE /books/1 → 删除 ID=1 的书

REST API 常见响应格式(JSON)

1
2
3
4
5
6
7
8
9
10
11
// 成功创建
{
"id": 3,
"title": "LangChain 实战",
"author": "王五"
}

// 错误
{
"detail": "Book not found"
}

HTTP 状态码也很关键:

状态码 含义
200 OK(GET 成功)
201 Created(POST 成功)
204 No Content(DELETE 成功,无返回体)
400 Bad Request(参数错误)
404 Not Found(资源不存在)
500 Internal Server Error

GraphQL

GraphQL 是由 Facebook 开发的一种 API 查询语言运行时,用于客户端精确声明它需要什么数据,服务器则按需返回这些数据。

✅ 核心理念:“客户端要什么,服务端就给什么”,避免 REST 中常见的“过载”或“多次请求”问题。

场景

你想获取用户 ID=1 的 姓名 和他最新的 两篇文章标题

🔹 REST 方式(你熟悉的方式)

1
2
GET /users/1          → 返回 {id, name, email, avatar, ...} (可能含不需要的 email/avatar)
GET /posts?userId=1 → 返回所有文章(可能有 100 篇,但你只要 2 篇)

2 次请求 + 数据冗余

🔸 GraphQL 方式

客户端发送一个 查询(Query)

1
2
3
4
5
6
7
8
query {
user(id: 1) {
name
posts(first: 2) {
title
}
}
}

服务器返回:

1
2
3
4
5
6
7
8
9
10
11
{
"data": {
"user": {
"name": "张三",
"posts": [
{"title": "Python 入门"},
{"title": "FastAPI 实战"}
]
}
}
}

1 次请求 + 精确数据

1. Schema(类型系统)

定义 API 的能力:有哪些类型?有哪些字段?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User {
id: ID!
name: String!
posts(first: Int): [Post!]!
}

type Post {
id: ID!
title: String!
content: String
}

type Query {
user(id: ID!): User
}

2. Query(查询)

客户端请求数据(类似 REST 的 GET)

3. Mutation(变更)

客户端修改数据(类似 REST 的 POST/PUT/DELETE)

1
2
3
4
5
6
mutation {
createPost(userId: 1, title: "新文章", content: "内容") {
id
title
}
}

RPC

RPC(Remote Procedure Call) 是一种让程序调用另一个地址空间(通常是远程服务器)上的函数/过程,就像调用本地函数一样的机制。

✅ 核心思想:“像调用本地函数一样调用远程服务”

你不需要关心网络细节(HTTP、序列化等),框架会自动处理。

为什么有了 HTTP 还需要 RPC?

我们可以把 HTTP/REST 比作 “写信”,把 RPC 比作 “发电报”

A. 数据包的大小(信封 vs. 代码)

  • HTTP (REST+JSON): 为了让人看懂,JSON 极其啰嗦。
    • 比如传递一个数字 1000,JSON 需要写成字段 "balance": 1000。为了传这一个数,你得带上 "balance" 这个单词,还有大括号、冒号。这就像写信时要写满客套话。
  • RPC (Protobuf/二进制): 机器不需要看懂单词,它只需要知道位置。
    • RPC 协议会约定:“第2个位置放余额”。传输时,直接发二进制的 1000 即可。数据体积可以缩小 50%~80%。

B. 解析速度(阅读 vs. 直觉)

  • HTTP: 服务器收到 JSON 后,CPU 需要一行行去“读”文本,把字符串转换成对象。这非常消耗 CPU 资源。
  • RPC: 二进制数据到了服务器,几乎不需要转换,直接就能由计算机内存读取。解析速度比 JSON 快很多倍。

C. 约束力(口头约定 vs. 法律合同)

  • HTTP: REST API 的文档通常写在网页上(比如 Swagger)。如果后端改了字段名,前端没注意,上线可能就崩了。这属于“弱约束”。
  • RPC: 强依赖 IDL(接口定义语言)。在代码编译阶段,如果客户端和服务端定义的参数类型对不上,代码直接报错,编译不过。这大大减少了上线后的低级错误。

注意: 现代的 RPC(如 gRPC)其实底层往往也是基于 HTTP/2 协议传输的。所以准确地说,并不是“抛弃 HTTP 用 RPC”,而是“在 HTTP 之上通过 RPC 机制来优化传输效率”。

RPC 主要用于什么场景?

RPC 并不是为了取代 REST,而是为了在特定领域“称王”。

场景一:微服务架构的内部通信(这是绝对的主战场)

想象一个像淘宝或亚马逊这样的大型系统,一个“用户下单”的请求,在后台可能要触发 50 次 内部调用(查库存、算优惠、校验风控、写日志、通知物流…)。

  • 如果全用 REST:
    • 每次调用都要解析一遍 JSON,CPU 累死。
    • 每次传输都带一堆冗余的 HTTP 头,网络堵死。
    • 总延迟 = 50 次 HTTP 请求的累加,用户会感觉“卡顿”。
  • 如果用 RPC:
    • 二进制传输,极快。
    • 内部带宽占用极低。
    • 结论: 对外(给浏览器/App)用 REST,对内(服务器之间)用 RPC。

场景二:高频交易与实时系统

在股票交易、实时游戏同步等对延迟(Latency)极其敏感的场景。

  • 每一毫秒都决定盈亏。RPC 省去了繁琐的 HTTP 报文头解析,能把延迟压榨到极限。

场景三:多语言混合开发 (Polyglot)

公司里,算法团队用 Python(搞 AI),后端团队用 Java(搞业务),数据团队用 Go。

  • 使用 gRPC(Google 的 RPC 框架),只需要定义一份 .proto 文件,就能自动生成 Python、Java、Go 的代码。这三个团队不需要互相通过文档扯皮,直接调用生成的代码即

RPC 调用流程

1
2
3
4
5
6
sequenceDiagram
participant Client as 客户端
participant Server as RPC 服务端
Client->>Server: 调用 generate_summary(book_id=456)
Server->>Server: 执行本地函数(可能调用 LLM/数据库)
Server-->>Client: 返回 {summary: "...", task_id: "..."}

对比 REST 的资源操作:

1
2
3
4
5
6
sequenceDiagram
participant Client
participant REST_API
Client->>REST_API: POST /summaries {book_id: 456}
REST_API->>DB: 创建摘要任务
REST_API-->>Client: 201 Created {location: "/summaries/789"}

gRPC:跨语言的通用翻译官

gRPC 是由 Google 开发并开源的高性能 RPC 框架。它是目前微服务架构中的事实标准。

核心特点:

  1. 基于 HTTP/2: 它利用 HTTP/2 的特性(如多路复用、头部压缩),传输效率极高。
  2. Protocol Buffers (Protobuf): 这是 gRPC 的灵魂。它不使用 JSON,而是使用 Google 发明的 .proto 文件来定义接口和数据结构。
  3. 多语言支持 (Polyglot):这是它最大的杀手锏。

适用场景:

  • 微服务架构: 几十个服务,有的用 Java 写,有的用 Python 写,需要互相通信。
  • 移动端对接: 手机 App 与服务器通信(省流量、省电)。

tRPC:TypeScript 开发者的“心灵感应”

tRPC (TypeScript RPC) 是近年来在前端圈(特别是 React/Next.js 社区)爆火的库。

注意: tRPC 只能用于 TypeScript。如果你的后端是 Java 或 Go,那就不能用它。

核心特点:

  1. 端到端类型安全 (End-to-End Type Safety): 这是它存在的全部意义。
  2. 无代码生成 (No Code Gen): 不需要写 .proto 文件,也不需要运行脚本生成代码。
  3. 基于标准 HTTP: 底层通常还是普通的 HTTP 请求,但写代码的感觉是 RPC。

适用场景:

  • 全栈 TypeScript 项目: 比如使用 Next.js, Nuxt.js 等框架,前后端都在一个代码仓库(Monorepo)里。
  • 中小型快速开发: 一个团队同时负责前后端,追求极致的开发速度。

参考资料

【API技术核心原理】REST | GraphQL | gRPC | tRPC_哔哩哔哩_bilibili

RPC是什么?HTTP是什么?RPC和HTTP有什么区别?_哔哩哔哩_bilibili

RAG-Anything

RAG-Anything是一个综合性多模态文档处理RAG系统。该系统能够无缝处理和查询包含文本、图像、表格、公式等多模态内容的复杂文档,提供完整的检索增强(RAG)生成解决方案。

解析方式

process_document_complete执行流程

  • 初始化依赖: await self._ensure_lightrag_initialized() ,保证 LightRAG 已就绪。
  • 读取配置默认:对 output_dir / parse_method / display_stats 应用默认值。
  • 记录开始日志: Starting complete document processing: {file_path} 。
  • 第一步(解析):调用 parse_document(…) ,返回
    • content_list : 解析出的内容列表(混合文本块与多模态项的统一结构)。
    • content_based_doc_id : 基于内容生成的文档ID(用于未显式提供 doc_id 时)。
  • 文档ID确定:若未传入 doc_id ,用 content_based_doc_id 作为本次处理的文档标识。
  • 第二步(拆分): text_content, multimodal_items = separate_content(content_list) ,将纯文本与多模态项分离。
  • 第二步半(上下文源设置):若存在多模态项且类实现了 set_content_source_for_context ,调用它以建立“内容源 → 上下文”映射,用于后续多模态处理更好地提取上下文。
  • 第三步(插入文本):如果 text_content 非空:
    • 取 file_name = os.path.basename(file_path) ,作为 file_paths 元信息传入。
    • 调用 insert_text_content(…) 将文本写入 LightRAG 索引,支持 split_by_character 与 split_by_character_only 控制切分方式,并统一使用同一个 ids=doc_id 以保证该文档的索引一致性。
  • 第四步(处理多模态):
    • 若 multimodal_items 非空:调用 await self._process_multimodal_content(multimodal_items, file_path, doc_id) 用专用处理器写入图片、表格、公式等相关产物并建立索引/缓存。
    • 若为空:调用 _mark_multimodal_processing_complete(doc_id) 标记该文档的多模态处理阶段已完成(即使没有多模态项也要显式完成,以便状态一致)。
  • 记录完成日志: Document {file_path} processing complete! 。

存储方式

检索方式

功能区别

  • aquery :纯文本查询入口,直接把你的问题和检索参数封装到 QueryParam 并调用 LightRAG。若已配置视觉模型函数( vision_model_func ),默认会自动启用图像增强;可通过传入 vlm_enhanced=False 强制走纯文本。
  • aquery_with_multimodal :主动把你提供的多模态素材(图片、表格、公式等)先“描述压缩”为文本(用已注册的处理器生成说明),把这些说明拼接到查询中,然后再执行 aquery 做检索与问答;内置结果缓存(按素材和参数归一化生成 key)。
  • aquery_vlm_enhanced :在检索生成的原始提示( only_need_prompt=True )里扫描图片路径,保持原路径并插入标记,将图片转为 base64,与文本一并发给视觉模型生成最终答案;不把图转成文字摘要,而是让 VLM直接“看图”。需要你已提供 vision_model_func ,否则会报错。

aquery_vlm_enhanced执行流程

  • 检查 VLM 可用:若 vision_model_func 未配置,抛出 ValueError 。
  • 确保 LightRAG 就绪:调用 self._ensure_lightrag_initialized() ,保证你之前已处理并建好索引/图谱。
  • 清理图片缓存:删除上一次查询残留的 self._current_images_base64 。
  • 只取检索提示:构造 QueryParam(mode=mode, only_need_prompt=True, **kwargs) ,调用 self.lightrag.aquery(…) 拿到“原始提示”(包含检索到的上下文与可能的图片路径),此时不向 LLM发起最终回答。
  • 解析图片路径:调用 self._process_image_paths_for_vlm(raw_prompt) ,用正则匹配诸如 Image Path: …(.jpg|.png|…) 的行,对路径做 validate_image_file 验证并转 base64,往提示里插入供 VLM使用的标记;返回增强后的提示与图片计数。
  • 无图兜底:若 images_found == 0 ,直接退回普通查询( QueryParam(mode=mode, **kwargs) )走文本 LLM(即不做看图增强)。
  • 构建 VLM消息: self._build_vlm_messages_with_images(enhanced_prompt, query) 将“增强提示+图片(base64)”打包成 VLM需要的消息格式。
  • 调用视觉模型: self._call_vlm_with_multimodal_content(messages) ,由你的 vision_model_func 实际生成最终答案并返回。

为什么要转base64

心原因

  • 远端不可读本地路径:检索提示里只有 Image Path: C:... 或 ./xxx.png ,VLM(常在云端或独立进程)无法直接访问你的本地文件系统,必须把图像的字节随请求一起发送。
  • 标准化传输格式:多数 VLM 接口以 JSON/HTTP 交互,原生不支持二进制;Base64 是通用的文本编码,易于嵌入到消息体或 data:image/png;base64,… 这样的数据 URI。
  • 跨平台与健壮性:避免 Windows/中文路径、权限、相对路径等差异导致文件读取失败;编码后与工作目录无关,传到哪都能被解码。
  • 兼容主流 API 形态:OpenAI、LM Studio、部分本地/私有 VLM 都支持用 Base64 提供图像数据;RAGAnything 将其统一为 vision_model_func 可消费的消息格式。
  • 可缓存与复用:编码后可做哈希/去重与一次请求内复用( _current_images_base64 ),同时避免在日志或提示里暴露真实文件路径结构。

参考资料

RAG-Anything/README_zh.md at main · HKUDS/RAG-Anything

什么是 dotenv?

dotenv 是一个用于从 .env 文件中加载环境变量到程序中的工具。它广泛用于各种编程语言(如 Python、Node.js、Ruby 等),目的是把敏感信息(比如 API 密钥、数据库密码)和配置从代码中分离出来,避免硬编码,提高安全性与灵活性。

基本使用

1
2
#安装
pip install python-dotenv
1
2
3
4
5
6
7
#.env

db_host=localhost
db_port=3306
db_user=root
db_password=123456
db_name=test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from dotenv import load_dotenv
load_dotenv()

import os

db_host = os.getenv("db_host")
db_port = os.getenv("db_port")
db_user = os.getenv("db_user")
db_password = os.getenv("db_password")
db_name = os.getenv("db_name")

print(db_host)
print(db_port)
print(db_user)
print(db_password)
print(db_name)

在.gitignore增加

1
2
# Environment variables
*.env
写法 含义
.env 只忽略名为 .env 的文件(精确匹配)
*.env 忽略所有以 .env 结尾的文件(通配符匹配)
0%