1. 背景

E9ovi6

Plan-and-Execute 模式是一种 “先计划、后执行、按计划循环迭代” 的 Agent 设计模式。它的核心思想是:先输出一份结构化的执行计划,将复杂任务拆解为若干明确步骤,然后严格按照步骤逐步执行;当某一步需要外部信息或能力(如搜索、计算等)时,则触发工具调用,完成后继续推进计划,直到满足终止条件。

实际应用场景

以 Cursor 为例:当 Cursor 要编写复杂的代码时,通常会先执行规划,生成一个 to-do list,之后按照这个 to-do list 的顺序依次执行任务。这种”先规划、后执行”的方式能够有效处理复杂任务,避免遗漏关键步骤。

Plan Mode 的三层结构

Plan Mode 通常可以抽象出三层结构:

  1. 计划层:用自然语言清晰列出步骤、编号、依赖、输入输出。
  2. 执行层:逐步消费计划中的步骤,产出结果或中间态。
  3. 工具层:在执行层遇到”需要能力/数据”的节点时,面向具体工具进行调用(如价格查询、表达式计算等)。

优势

这种”先规划、后执行”的范式具备以下优势:

  • 可解释性强
  • 可回溯
  • 易扩展(新增工具即可拓展能力)

在设计 Agent 时已经被广泛采用。

2. 构建 Plan-and-Execute Agent

使用 LangGraph 来构建一个简单的 Plan-and-Execute Agent,节点结构包括:

  • Plan Node(计划节点)
  • Execute Node(执行节点)
  • Tool Node(工具节点)
  • Conditional Edge(条件边)

2.1 基础准备

定义工具

首先开发需要用到的工具(tools)。这里定义了两个函数:一个是查询汇率信息,另一个是计算器,然后利用 LangChain 的 @tool 装饰器,将它们包装成可被 LLM 调用的工具。

代码说明:

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
from langchain.tools import tool

@tool
def get_exchange_rate(currency: str) -> str:
"""
获取货币汇率信息

这个工具用于查询指定货币对人民币的汇率。
在实际应用中,这里可以对接真实的汇率 API(如 ExchangeRate-API)。

Args:
currency: 货币代码,例如"USD"(美元)、"EUR"(欧元)

Returns:
返回格式化的汇率信息字符串
"""
# 简单的汇率数据映射表(实际应用中应该从汇率API获取)
if currency == "USD" or currency == "美元":
return "美元对人民币汇率:1 USD = 7.25 CNY"
if currency == "EUR" or currency == "欧元":
return "欧元对人民币汇率:1 EUR = 7.85 CNY"
return f"没有找到{currency}的汇率信息"

@tool
def calculate(expression: str) -> str:
"""
计算表达式

这个工具用于执行数学表达式计算。
注意:实际生产环境中应该使用更安全的表达式解析库(如 numexpr),
而不是直接使用 eval(),这里仅用于演示。

Args:
expression: 数学表达式字符串,例如"100 * 7.25"

Returns:
计算结果字符串
"""
# 使用 eval 计算表达式(仅用于演示,生产环境需更安全的方案)
return str(eval(expression))

# 定义工具列表,用于绑定到 LLM
# LLM 可以根据工具描述自动决定何时调用哪个工具
TOOLS = [get_exchange_rate, calculate]

# 创建工具字典,key 为工具名称,value 为工具对象
# 在执行工具调用时,通过工具名称快速查找对应的工具对象
TOOL_DICT = {tool.name: tool for tool in TOOLS}

关键点说明:

  • @tool 装饰器会将普通函数转换为 LangChain 的工具对象,LLM 可以识别并调用
  • 工具函数的文档字符串(docstring)会被 LLM 读取,用于理解工具的用途和参数
  • TOOL_DICT 用于在执行阶段快速查找和调用对应的工具

定义模型

定义大语言模型(LLM),这里使用 DeepSeek Chat 模型。然后将模型与工具进行绑定,使 LLM 能够识别和调用我们定义的工具。

代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain_openai import ChatOpenAI
import os

# 创建 DeepSeek 模型实例
# 使用 ChatOpenAI 类是因为 DeepSeek 兼容 OpenAI API 格式
LLM = ChatOpenAI(
model="deepseek-chat", # 指定模型名称
api_key=os.getenv("DEEPSEEK_API_KEY"), # 从环境变量读取 API Key
base_url=os.getenv("DEEPSEEK_BASE_URL"), # DeepSeek 的 API 基础地址
)

# 将 LLM 与工具列表绑定
# bind_tools() 方法会将工具的描述信息注入到模型的上下文中
# 这样模型在生成回复时,可以识别何时需要调用工具,并生成相应的工具调用请求
LLM_WITH_TOOLS = LLM.bind_tools(TOOLS)

关键点说明:

  • bind_tools() 方法会将工具的函数签名和描述信息传递给 LLM
  • 绑定后,LLM 在生成回复时可能会返回 tool_calls 字段,表示需要调用工具
  • 工具调用信息包含工具名称、参数和调用 ID,用于后续执行

定义状态

定义 Agent 运行过程中需要维护的状态。自定义了 PlanAndExecuteState,它继承 LangGraph 的 MessagesState,可以自动保存消息列表(包括用户消息、AI 回复、工具调用结果等),并且额外扩展了两个属性,分别表示生成的计划和当前执行的步骤。

代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langgraph.graph.message import MessagesState

class PlanAndExecuteState(MessagesState):
"""
计划和执行状态定义

这个状态类用于在整个 Agent 执行过程中保存和传递信息。

继承自 MessagesState 的属性:
messages: List[BaseMessage] - 消息列表,包含对话历史、工具调用等

自定义属性:
plan: str - 生成的执行计划文本
step: int - 当前执行的步骤编号(从 0 开始)
"""

plan: str # 生成的计划,由 plan_node 生成并保存
step: int # 当前步骤,每次执行后递增,用于跟踪执行进度

关键点说明:

  • MessagesState 提供了消息管理功能,自动处理消息的追加和更新
  • plan 字段存储计划节点生成的执行计划,供执行节点使用
  • step 字段用于跟踪执行进度,便于调试和日志记录
  • 状态对象在节点之间传递,每个节点可以读取和修改状态

2.2 Plan Node(计划节点)

Plan Node 是整个 Agent 的第一个节点,负责根据用户提出的问题,生成详细的、结构化的执行计划。这个计划将指导后续的执行步骤。

代码说明:

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
70
71
72
73
74
75
76
77
from state import PlanAndExecuteState
from llm import LLM_WITH_TOOLS
from langchain_core.messages import SystemMessage

# 生成计划的 System Prompt
# 这个提示词指导 LLM 如何生成执行计划
SYSTEM_PROMPT = """
你是一位智能助手,擅长解决用户提出的各种问题。请为用户提出的问题创建分析方案步骤。

如果有需要,可以调用工具。

你可以调用工具的列表如下:
get_exchange_rate:
获取指定货币对人民币的汇率信息
Parameters:
-----------
currency : str
货币代码,例如"USD"(美元)、"EUR"(欧元)
Returns:
--------
str
指定货币的汇率信息

calculate:
计算表达式
Parameters:
-----------
expression : str
表达式内容
Returns:
--------
str
表达式的计算结果

要求:
1.用中文列出清晰步骤
2.每个步骤标记序号
3.明确说明需要分析和执行的内容
4.只需输出计划内容,不要做任何额外的解释和说明
5.设计的方案步骤要紧紧贴合我的工具所能返回的内容,不要超出工具返回的内容
"""

def plan_node(state: PlanAndExecuteState) -> PlanAndExecuteState:
"""
生成计划节点

这个节点的职责:
1. 接收用户的问题(在 state["messages"] 中)
2. 调用 LLM 生成执行计划
3. 将计划保存到状态中,供后续节点使用

Args:
state: 当前状态,包含用户消息

Returns:
更新后的状态,包含生成的计划
"""

# 构造消息列表
# SystemMessage 放在最前面,用于设置 LLM 的角色和行为
# state["messages"] 包含用户的问题
messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]

# 调用 LLM 生成计划
# invoke() 方法会发送消息到 LLM,并返回回复
# .content 获取回复的文本内容(计划文本)
response = LLM_WITH_TOOLS.invoke(input=messages)
plan = response.content
print(f"生成的计划: \n{plan}")

# 更新状态,保存生成的计划
# 这个计划将在 execute_node 中被使用
state["plan"] = plan

# 返回更新后的状态
# LangGraph 会将这个状态传递给下一个节点
return state

工作流程说明:

  1. 节点接收包含用户问题的状态
  2. 构造包含 System Prompt 和用户消息的完整消息列表
  3. 调用 LLM 生成执行计划(纯文本格式,如”1. 查询美元汇率 2. 查询欧元汇率 3. 计算兑换金额”)
  4. 将计划保存到状态中
  5. 返回更新后的状态,流程进入下一个节点

注意:为了方便演示,直接将工具描述信息写到 System Prompt 中了。更理想的方案是接入 MCP(Model Context Protocol)或使用 LangChain 的工具描述自动生成功能,感兴趣的同学可以自己实现。

2.3 Execute Node(执行节点)

Execute Node 是 Agent 的核心执行节点,负责根据生成的计划逐步执行任务。LLM 会根据当前计划、对话历史和工具调用结果,决定下一步操作:可能是直接回复,也可能是调用工具。

代码说明:

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
from state import PlanAndExecuteState
from llm import LLM_WITH_TOOLS
from langchain_core.messages import SystemMessage

# 执行计划的 System Prompt
# 这个提示词指导 LLM 如何执行计划中的步骤
SYSTEM_PROMPT = """
你是一位思路清晰、有条理的智能助手,你必须严格按照以下计划执行任务:

当前计划:

{plan}

如果你认为计划已经执行到最后一步了,请在内容的末尾加上 Final Answer 字样
示例:
这个问题的最终答案是99.99。
Final Answer
"""

def execute_node(state: PlanAndExecuteState) -> PlanAndExecuteState:
"""
执行计划节点

这个节点的职责:
1. 读取计划节点生成的计划
2. 根据计划、对话历史和工具调用结果,执行当前步骤
3. LLM 可能直接回复,也可能生成工具调用请求
4. 更新执行步骤计数器

Args:
state: 当前状态,包含计划、消息历史和步骤编号

Returns:
更新后的状态,包含执行结果
"""

# 获取当前步骤编号
# 用于跟踪执行进度和日志输出
step = state["step"]

# 构造消息列表
# SystemMessage 包含当前计划,指导 LLM 执行
# state["messages"] 包含完整的对话历史(用户消息、AI回复、工具调用结果等)
system_message = SystemMessage(content=SYSTEM_PROMPT.format(plan=state["plan"]))
messages = [system_message] + state["messages"]

# 调用 LLM 执行计划
# LLM 会分析当前状态,决定下一步操作
# 如果 LLM 认为需要调用工具,会在 resp.tool_calls 中返回工具调用信息
# 如果 LLM 认为可以直接回复,会在 resp.content 中返回文本内容
resp = LLM_WITH_TOOLS.invoke(input=messages)
print(f"\n【第 {step + 1} 步】{resp.content}")

# 更新状态
# 1. 将 LLM 的回复添加到消息历史中
# 2. 递增步骤计数器
state["messages"].append(resp)
state["step"] = step + 1

# 返回更新后的状态
# 后续的条件边会根据 resp 的内容决定下一步流向
return state

工作流程说明:

  1. 节点读取状态中的计划(由 plan_node 生成)
  2. 构造包含计划和完整对话历史的消息列表
  3. 调用 LLM,LLM 会:
    • 分析当前应该执行计划的哪一步
    • 如果需要数据,生成工具调用请求(tool_calls 字段)
    • 如果可以直接回答,生成文本回复(content 字段)
  4. 将 LLM 的回复保存到消息历史中
  5. 更新步骤计数器
  6. 返回状态,由条件边决定下一步流向

关键点:

  • LLM 的回复可能包含 tool_calls(需要调用工具)或 content(直接回复)
  • 如果回复末尾包含 “Final Answer”,表示任务完成
  • 步骤计数器用于跟踪执行进度,便于调试

2.4 Tool Node(工具节点)

Tool Node 负责执行工具调用。当 Execute Node 中的 LLM 生成工具调用请求时,流程会进入 Tool Node,执行实际的工具调用,并将结果保存到消息历史中,供后续节点使用。

代码说明:

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
from state import PlanAndExecuteState
from tools import TOOL_DICT
from langchain_core.messages import ToolMessage

def tool_node(state: PlanAndExecuteState) -> PlanAndExecuteState:
"""
工具调用节点

这个节点的职责:
1. 从最后一条消息中提取工具调用信息
2. 根据工具名称查找对应的工具对象
3. 使用工具参数执行工具调用
4. 将工具调用结果封装为 ToolMessage,添加到消息历史中

Args:
state: 当前状态,最后一条消息应包含 tool_calls

Returns:
更新后的状态,包含工具调用结果
"""

# 获取最后一条消息中的工具调用信息
# 最后一条消息应该是 execute_node 中 LLM 生成的回复
# 如果 LLM 决定调用工具,这条消息会包含 tool_calls 字段
last_message = state["messages"][-1]
tool_calls = last_message.tool_calls

# 如果没有工具调用,直接返回(理论上不应该发生,因为只有有工具调用才会进入此节点)
if tool_calls is None:
return state

# 遍历所有工具调用(LLM 可能同时调用多个工具)
for tool_call in tool_calls:
# 提取工具名称和参数
# tool_call 是一个字典,包含:
# - "name": 工具名称,如 "get_exchange_rate"
# - "args": 工具参数字典,如 {"currency": "USD"}
# - "id": 工具调用 ID,用于关联 ToolMessage
tool_name = tool_call["name"]
tool_args = tool_call["args"]

# 从工具字典中查找对应的工具对象
tool = TOOL_DICT.get(tool_name)
if tool is None:
# 如果找不到工具,跳过(实际应用中应该记录错误)
continue

# 执行工具调用
# invoke() 方法会使用 tool_args 调用工具函数
# 例如:get_exchange_rate.invoke({"currency": "USD"}) -> "美元对人民币汇率:1 USD = 7.25 CNY"
tool_call_result = tool.invoke(input=tool_args)
print(f"\n调用工具: {tool_name}\n参数: {tool_args}\n执行结果: {tool_call_result}")

# 将工具调用结果封装为 ToolMessage,添加到消息历史中
# ToolMessage 需要包含 tool_call_id,用于关联对应的工具调用请求
# 这样 LLM 在后续回复时,可以知道哪个工具调用产生了哪个结果
state["messages"].append(
ToolMessage(
content=f"工具调用结果: {tool_call_result}",
tool_call_id=tool_call["id"], # 关联工具调用请求
)
)

# 返回更新后的状态
# 状态中现在包含了工具调用结果,流程会回到 execute_node 继续执行
return state

工作流程说明:

  1. 节点从最后一条消息中提取 tool_calls 信息
  2. 遍历每个工具调用请求:
    • 提取工具名称和参数
    • TOOL_DICT 中查找对应的工具对象
    • 使用参数调用工具函数
    • 将结果封装为 ToolMessage 并添加到消息历史
  3. 返回更新后的状态,流程回到 execute_node 继续执行

关键点:

  • ToolMessage 必须包含 tool_call_id,用于关联对应的工具调用请求
  • LLM 在后续回复时,会根据 tool_call_id 匹配工具调用结果
  • 一个 LLM 回复可能包含多个工具调用,需要遍历处理
  • 工具调用结果会被添加到消息历史中,供后续节点使用

2.5 条件边(Conditional Edge)

定义好节点之后,还需要一个 Conditional Edge(条件边)来控制节点之间的流转规则。条件边会根据当前状态决定下一步应该执行哪个节点。

代码说明:

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 state import PlanAndExecuteState
from langgraph.graph import END

def conditional_edge(state: PlanAndExecuteState) -> str:
"""
条件边函数

这个函数决定 execute_node 执行后的下一步流向:
- 如果 LLM 回复中包含 "Final Answer",返回 END,表示任务完成
- 否则返回 "tool_node",表示需要调用工具

Args:
state: 当前状态,包含执行结果

Returns:
下一个节点的名称,或 END 表示结束
"""

# 获取最后一条消息(应该是 execute_node 中 LLM 的回复)
last_message = state["messages"][-1]

# 检查回复中是否包含 "Final Answer" 标识
# 这是我们在 execute_node 的 System Prompt 中要求的终止条件
if "Final Answer" in last_message.content:
# 如果包含,返回 END,表示任务完成,流程结束
return END
else:
# 如果不包含,返回 "tool_node",表示需要调用工具
# 注意:这里假设 LLM 的回复一定包含 tool_calls
# 实际应用中可能需要更复杂的判断逻辑
return "tool_node"

工作流程说明:

  1. 条件边函数接收当前状态作为参数
  2. 检查最后一条消息(LLM 的回复)中是否包含 “Final Answer”
  3. 如果包含,返回 END,流程结束
  4. 如果不包含,返回 "tool_node",流程进入工具节点

关键点:

  • 条件边是 LangGraph 中实现分支逻辑的关键机制
  • 返回值必须是已定义的节点名称或 END
  • 这里的判断逻辑比较简单,实际应用中可能需要更复杂的条件判断
  • “Final Answer” 标识是在 System Prompt 中约定的终止条件

2.6 构造 Workflow(工作流)

State、Node 和 Edge 都定义好了之后,就可以构造完整的 Workflow(工作流)了。Workflow 定义了整个 Agent 的执行流程和节点之间的连接关系。

代码说明:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
from langgraph.graph import StateGraph, START, END
from langgraph.graph.state import CompiledStateGraph
from langchain_core.messages import HumanMessage

def build_agent() -> CompiledStateGraph:
"""
构建 Plan-and-Execute Agent

这个函数负责:
1. 创建状态图(StateGraph)
2. 添加所有节点
3. 添加节点之间的边(普通边和条件边)
4. 编译成可执行的 Agent

Returns:
编译后的 Agent,可以用于执行任务
"""

# 创建状态图,指定状态类型为 PlanAndExecuteState
# StateGraph 是 LangGraph 的核心类,用于构建有状态的工作流
graph = StateGraph(PlanAndExecuteState)

# 添加节点
# add_node() 方法将节点函数注册到图中
# 第一个参数是节点名称(字符串),第二个参数是节点函数
graph.add_node("plan_node", plan_node) # 计划节点
graph.add_node("execute_node", execute_node) # 执行节点
graph.add_node("tool_node", tool_node) # 工具节点

# 添加普通边(无条件流转)
# START 是 LangGraph 的特殊节点,表示工作流的起始点
graph.add_edge(START, "plan_node") # 从起始点进入计划节点
graph.add_edge("plan_node", "execute_node") # 计划节点执行完后进入执行节点

# 添加条件边(根据条件决定流转方向)
# add_conditional_edges() 用于添加条件分支
graph.add_conditional_edges(
source="execute_node", # 源节点:执行节点
path=conditional_edge, # 条件判断函数
path_map={ # 路径映射:条件函数的返回值 -> 目标节点
"tool_node": "tool_node", # 如果返回 "tool_node",进入工具节点
END: END, # 如果返回 END,流程结束
},
)

# 添加普通边:工具节点执行完后,回到执行节点继续执行
# 这样就形成了一个循环:execute_node -> tool_node -> execute_node
graph.add_edge("tool_node", "execute_node")

# 编译 Agent
# compile() 方法会将图编译成可执行的状态机
# 编译后的 Agent 可以接收初始状态并执行整个工作流
agent = graph.compile()

# 可选:打印 Agent 节点结构图,并保存到本地
# 这有助于可视化工作流的执行流程
agent.get_graph().draw_mermaid_png(output_file_path="./agent.png")

# 返回编译后的 Agent
return agent

def run_agent(agent: CompiledStateGraph, user_query: str) -> str:
"""
运行 Agent

这个函数负责:
1. 初始化 Agent 状态
2. 执行 Agent 工作流
3. 返回最终结果

Args:
agent: 编译后的 Agent 实例
user_query: 用户的问题

Returns:
最终的回答文本
"""

# 初始化状态
# 创建用户消息,包含用户的问题
messages = [HumanMessage(content=user_query)]

# 创建初始状态对象
# plan 初始化为空字符串,将在 plan_node 中生成
# step 初始化为 0,表示还未开始执行
# messages 包含用户的问题
init_state = PlanAndExecuteState(
plan="", # 计划初始为空
step=0, # 步骤从 0 开始
messages=messages, # 包含用户消息
)

# 运行 Agent
# invoke() 方法会从 START 节点开始执行,直到遇到 END
# 执行过程中,状态会在节点之间传递和更新
result = agent.invoke(init_state)

# 返回最终结果
# 最后一条消息应该是包含 "Final Answer" 的回复
return result["messages"][-1].content

工作流结构说明:

整个 Agent 的执行流程如下:

1
2
3
4
5
START → plan_node → execute_node → [条件判断]
↓ ↓
tool_node END

execute_node (循环)
  1. START → plan_node:工作流开始,进入计划节点生成计划
  2. plan_node → execute_node:计划生成后,进入执行节点开始执行
  3. execute_node → [条件判断]
    • 如果包含 “Final Answer”,流程结束(END)
    • 否则进入 tool_node 执行工具调用
  4. tool_node → execute_node:工具调用完成后,回到执行节点继续执行
  5. 循环执行:步骤 3-4 会循环执行,直到 LLM 输出 “Final Answer”

关键点:

  • StateGraph 是 LangGraph 的核心,用于构建有状态的工作流
  • compile() 方法将图编译成可执行的状态机
  • invoke() 方法执行整个工作流,从 START 到 END
  • 状态在节点之间传递,每个节点可以读取和修改状态
  • 条件边实现了分支逻辑,使工作流能够根据条件选择不同的执行路径

这样,一个完整的 Plan-and-Execute Agent 就构建完成了!

3. 结果演示

测试一下:执行主函数,向 agent 提问 “我想用100美元和50欧元兑换人民币,总共能兑换多少人民币?”。

代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == "__main__":

# 构建 Agent
# 创建并编译整个工作流图
agent: CompiledStateGraph = build_agent()

# 运行 Agent,传入用户问题
# Agent 会按照 Plan-and-Execute 模式执行:
# 1. 生成计划(如:查询美元汇率、查询欧元汇率、计算总兑换金额)
# 2. 执行计划(调用工具获取汇率信息)
# 3. 计算并返回最终答案
result = run_agent(agent, "我想用100美元和50欧元兑换人民币,总共能兑换多少人民币?")
print(f"\n最终答案: {result}")

预期执行流程:

  1. Plan Node:生成计划

    1
    2
    3
    1. 查询美元对人民币的汇率
    2. 查询欧元对人民币的汇率
    3. 计算100美元和50欧元总共能兑换多少人民币
  2. Execute Node(第1步):执行计划第1步

    • LLM 决定调用 get_exchange_rate 工具查询美元汇率
    • 生成工具调用请求
  3. Tool Node:执行工具调用

    • 调用 get_exchange_rate({"currency": "USD"})
    • 返回:”美元对人民币汇率:1 USD = 7.25 CNY”
  4. Execute Node(第2步):执行计划第2步

    • LLM 看到美元汇率结果,决定调用 get_exchange_rate 查询欧元汇率
    • 生成工具调用请求
  5. Tool Node:执行工具调用

    • 调用 get_exchange_rate({"currency": "EUR"})
    • 返回:”欧元对人民币汇率:1 EUR = 7.85 CNY”
  6. Execute Node(第3步):执行计划第3步

    • LLM 看到两个汇率结果,决定调用 calculate 计算总兑换金额
    • 调用 calculate("100 * 7.25 + 50 * 7.85")
    • 返回计算结果:1107.5
  7. Execute Node(最终):生成最终答案

    • LLM 整合所有信息,生成最终答案
    • 输出:”100美元可以兑换725元人民币,50欧元可以兑换392.5元人民币,总共可以兑换1117.5元人民币。Final Answer”
    • 条件边检测到 “Final Answer”,流程结束

4. 总结

4.1 Plan-and-Execute 模式的核心流程

Plan-and-Execute 模式的核心流程可以概括为以下五个阶段:

  1. 计划阶段(Plan):根据用户问题生成结构化的执行计划

    • 将复杂任务拆解为若干明确步骤
    • 每个步骤都有清晰的输入输出说明
  2. 执行阶段(Execute):按照计划逐步执行

    • LLM 根据当前步骤和上下文决定下一步操作
    • 可能需要调用工具获取外部数据或能力
  3. 工具调用(Tool Calling):当需要外部能力时,调用相应工具

    • 提取工具调用请求中的参数
    • 执行工具函数并获取结果
    • 将结果保存到消息历史中
  4. 循环迭代(Iteration):执行完成后判断是否结束

    • 如果未完成,继续执行下一步
    • 工具调用结果会作为上下文传递给下一次执行
  5. 终止条件(Termination):当检测到终止标识时,任务完成

    • 通过条件边判断是否应该结束
    • 返回最终答案给用户

4.2 模式优势

Plan-and-Execute 模式具有以下优势:

  • 可解释性强:每个步骤都有明确的计划,用户可以清楚地看到 Agent 的执行过程
  • 可回溯:完整的消息历史记录了整个执行过程,便于调试和问题排查
  • 易扩展:新增工具只需定义函数并用 @tool 装饰,无需修改核心逻辑
  • 结构化:计划提供了清晰的任务分解,有助于 LLM 更好地理解和执行任务

4.3 适用场景

这种模式特别适合处理:

  • 复杂的多步骤任务:需要多个步骤才能完成的任务
  • 需要外部数据的任务:需要查询数据库、调用 API 等
  • 需要计算的任务:需要进行数学计算、数据分析等
  • 需要可解释性的任务:用户需要了解 Agent 的执行过程

4.4 改进方向

在实际应用中,可以考虑以下改进:

  • 更智能的计划生成:使用更先进的规划算法,考虑步骤之间的依赖关系
  • 动态计划调整:根据执行结果动态调整计划
  • 错误处理:添加错误处理和重试机制
  • 工具描述自动化:使用 MCP 或自动生成工具描述,而不是手动编写
  • 更复杂的终止条件:不仅依赖 “Final Answer”,还可以考虑步骤完成度、时间限制等

通过 LangGraph 构建 Plan-and-Execute Agent,我们可以轻松实现一个功能强大、可解释、易扩展的智能助手。

背景

LangGraph 是 LangChain 团队推出的一个用于构建有状态、多参与者应用程序的框架,它通过图结构来编排和协调 AI 代理的工作流程。2025 年,LangGraph 1.0 的正式发布标志着它成为了构建 AI Agent 的事实标准。

为什么选择 LangGraph?

  • 图结构编排:LangGraph 使用图(Graph)来定义 Agent 的工作流,使得复杂的多步骤任务变得清晰可控
  • 状态管理:内置的状态管理机制,让 Agent 能够在执行过程中保持上下文信息
  • 灵活的控制流:支持条件分支、循环、并行执行等多种控制流模式
  • 工具调用集成:与 LangChain 生态无缝集成,轻松实现函数调用和工具使用
  • 生产就绪:提供了完善的监控、调试和部署工具

本文目的

本文将通过构建一个简单的计算器 Agent 来验证 LangGraph 1.0 的核心功能,包括:

  1. 如何定义状态和节点
  2. 如何实现工具调用
  3. 如何构建条件控制流
  4. 如何在实际场景中使用 LangGraph 构建 Agent

验证

安装依赖

1
2
3
uv init
uv add "langchain>=1.1.3" "langchain-openai>=1.0.0" "langgraph>=1.0.4" "langgraph-cli[inmem]>=0.4.9" python-dotenv
uv sync

main.py

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import os
import operator
from typing import Literal
from dotenv import load_dotenv

from langchain.tools import tool
from langchain.chat_models import init_chat_model
from langchain.messages import AnyMessage, SystemMessage, ToolMessage, HumanMessage
from typing_extensions import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END

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

# 配置自建模型(必须从 .env 文件中读取)
BASE_URL = os.getenv("OPENAI_BASE_URL")
API_KEY = os.getenv("OPENAI_API_KEY")
MODEL_NAME = os.getenv("OPENAI_MODEL")

# 检查必需的配置项是否存在
if not BASE_URL:
raise ValueError("OPENAI_BASE_URL 未在 .env 文件中配置")
if not API_KEY:
raise ValueError("OPENAI_API_KEY 未在 .env 文件中配置")
if not MODEL_NAME:
raise ValueError("OPENAI_MODEL 未在 .env 文件中配置")

model = init_chat_model(
model=MODEL_NAME, # 自建模型直接使用模型名称,不需要 openai: 前缀
model_provider="openai",
temperature=0.7,
base_url=BASE_URL,
api_key=API_KEY,
)


# 定义工具
@tool
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`.

Args:
a: First int
b: Second int
"""
return a * b


@tool
def add(a: int, b: int) -> int:
"""Adds `a` and `b`.

Args:
a: First int
b: Second int
"""
return a + b


@tool
def divide(a: int, b: int) -> float:
"""Divide `a` and `b`.

Args:
a: First int
b: Second int
"""
return a / b


# 为LLM添加工具
tools = [add, multiply, divide]
tools_by_name = {tool.name: tool for tool in tools}
model_with_tools = model.bind_tools(tools)


# 步骤2:定义状态
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int


# 步骤3:定义模型节点
def llm_call(state: dict):
"""LLM decides whether to call a tool or not"""
return {
"messages": [
model_with_tools.invoke(
[
SystemMessage(
content="You are a helpful assistant tasked with performing arithmetic on a set of inputs."
)
]
+ state["messages"]
)
],
"llm_calls": state.get('llm_calls', 0) + 1
}


# 步骤4:定义工具节点
def tool_node(state: dict):
"""Performs the tool call"""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=str(observation), tool_call_id=tool_call["id"]))
return {"messages": result}


# 步骤5:定义结束逻辑
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
messages = state["messages"]
last_message = messages[-1]

# If the LLM makes a tool call, then perform an action
if last_message.tool_calls:
return "tool_node"

# Otherwise, we stop (reply to the user)
return END


# 步骤6:构建并编译代理
# 构建工作流
agent_builder = StateGraph(MessagesState)

# 添加节点
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)

# 添加边以连接节点
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
["tool_node", END]
)
agent_builder.add_edge("tool_node", "llm_call")

# 编译代理
agent = agent_builder.compile()


# 示例用法
if __name__ == "__main__":
print("=== LangGraph Calculator Agent Demo ===\n")

# 测试用例1:加法
print("Test 1: Add 3 and 4")
messages = [HumanMessage(content="Add 3 and 4.")]
result = agent.invoke({"messages": messages})
print(f"LLM calls: {result.get('llm_calls', 0)}")
for m in result["messages"]:
print(f"- {m.__class__.__name__}: {m.content}")
print("\n")

# 测试用例2:乘法
print("Test 2: Multiply 5 and 6")
messages = [HumanMessage(content="Multiply 5 and 6.")]
result = agent.invoke({"messages": messages})
print(f"LLM calls: {result.get('llm_calls', 0)}")
for m in result["messages"]:
print(f"- {m.__class__.__name__}: {m.content}")
print("\n")

# 测试用例3:除法
print("Test 3: Divide 20 by 4")
messages = [HumanMessage(content="Divide 20 by 4.")]
result = agent.invoke({"messages": messages})
print(f"LLM calls: {result.get('llm_calls', 0)}")
for m in result["messages"]:
print(f"- {m.__class__.__name__}: {m.content}")
print("\n")

# 测试用例4:复杂计算
print("Test 4: Add 10 and 5, then multiply the result by 3")
messages = [HumanMessage(content="Add 10 and 5, then multiply the result by 3.")]
result = agent.invoke({"messages": messages})
print(f"LLM calls: {result.get('llm_calls', 0)}")
for m in result["messages"]:
print(f"- {m.__class__.__name__}: {m.content}")

langgraph.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"dependencies": [
"."
],
"graphs": {
"chat": "main:agent"
},
"env": ".env",
"http": {
"cors": {
"allow_origins": ["*"],
"allow_methods": ["*"],
"allow_headers": ["*"],
"allow_credentials": false
}
}
}

.env

1
2
3
4
5
6
7
LANGSMITH_API_KEY=XXX

# OpenAI 配置
OPENAI_BASE_URL=XXX
OPENAI_API_KEY=XXX
OPENAI_MODEL=XXX

重要步骤详解

1. 环境配置与模型初始化(第 12-34 行)

1
2
3
4
load_dotenv()  # 加载 .env 文件中的环境变量
BASE_URL = os.getenv("OPENAI_BASE_URL")
API_KEY = os.getenv("OPENAI_API_KEY")
MODEL_NAME = os.getenv("OPENAI_MODEL")
  • .env 读取配置(API 地址、密钥、模型名)
  • 检查必需配置是否存在
  • 使用 init_chat_model 初始化聊天模型,并绑定到自定义 API 端点

2. 工具定义(第 37-68 行)

1
2
3
4
@tool
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`."""
return a * b
  • 使用 @tool 装饰器定义三个工具:addmultiplydivide
  • 工具描述用于让 LLM 理解何时调用
  • 第 72-74 行:将工具绑定到模型,使 LLM 可调用

3. 状态定义(第 77-81 行)

1
2
3
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int
  • messages:消息列表,使用 operator.add 实现追加
  • llm_calls:记录 LLM 调用次数
  • 状态在节点间传递,保存对话历史

4. LLM 调用节点(第 84-98 行)

1
2
3
4
5
6
7
8
9
def llm_call(state: dict):
return {
"messages": [
model_with_tools.invoke([
SystemMessage(content="...")] + state["messages"]
)
],
"llm_calls": state.get('llm_calls', 0) + 1
}
  • 接收当前状态,调用模型
  • 添加系统消息(角色设定)
  • 返回模型响应(可能包含工具调用)
  • 更新 llm_calls 计数

5. 工具执行节点(第 102-109 行)

1
2
3
4
5
6
7
def tool_node(state: dict):
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(...))
return {"messages": result}
  • 从最后一条消息提取工具调用
  • 按名称查找工具并执行
  • 将结果封装为 ToolMessage 返回
  • 结果会加入消息历史,供后续 LLM 使用

6. 条件路由逻辑(第 113-123 行)

1
2
3
4
5
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
last_message = messages[-1]
if last_message.tool_calls:
return "tool_node" # 有工具调用,去执行工具
return END # 没有工具调用,结束流程
  • 判断最后一条消息是否包含工具调用
  • 有则路由到 tool_node,无则结束

7. 图构建(第 128-144 行)

1
2
3
4
5
6
7
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
  • 创建状态图,添加两个节点
  • 边连接:
    • START → llm_call:入口
    • llm_call → tool_node/END:条件路由
    • tool_node → llm_call:工具执行后返回 LLM

工作流循环:

1
START → llm_call → [有工具调用?] → tool_node → llm_call → [有工具调用?] → ... → END

8. 测试用例(第 148-185 行)

  • 测试用例 1-3:简单计算(加法、乘法、除法)
  • 测试用例 4:多步计算(先加后乘)

每个测试:

  1. 创建用户消息
  2. 调用 agent.invoke() 执行
  3. 打印 LLM 调用次数和所有消息

核心设计理念

  1. 状态管理:通过 MessagesState 维护对话历史
  2. 工具调用:LLM 决定何时调用工具
  3. 循环执行:工具执行后返回 LLM,可继续调用工具
  4. 条件路由:根据 LLM 输出决定下一步

执行流程示例

假设用户输入:”Add 10 and 5, then multiply the result by 3”

  1. START → llm_call:LLM 分析请求,决定调用 add(10, 5)
  2. llm_calltool_node:执行 add(10, 5),返回 15
  3. tool_nodellm_call:LLM 看到结果 15,决定调用 multiply(15, 3)
  4. llm_calltool_node:执行 multiply(15, 3),返回 45
  5. tool_nodellm_call:LLM 看到结果 45,生成最终回复
  6. llm_call → END:流程结束

这是一个典型的 ReAct(Reasoning + Acting)模式实现,LLM 可以推理并执行多步操作。

如需进一步解释某个部分,请告诉我。

1
2
3
4
load_dotenv()  # 加载 .env 文件中的环境变量
BASE_URL = os.getenv("OPENAI_BASE_URL")
API_KEY = os.getenv("OPENAI_API_KEY")
MODEL_NAME = os.getenv("OPENAI_MODEL")
1
2
3
4
@tool
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`."""
return a * b
1
2
3
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int
1
2
3
4
5
6
7
8
9
def llm_call(state: dict):
return {
"messages": [
model_with_tools.invoke([
SystemMessage(content="...")] + state["messages"]
)
],
"llm_calls": state.get('llm_calls', 0) + 1
}
1
2
3
4
5
6
7
def tool_node(state: dict):
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(...))
return {"messages": result}
1
2
3
4
5
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
last_message = messages[-1]
if last_message.tool_calls:
return "tool_node" # 有工具调用,去执行工具
return END # 没有工具调用,结束流程
1
2
3
4
5
6
7
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
1
START → llm_call → [有工具调用?] → tool_node → llm_call → [有工具调用?] → ... → END

在 WebUI 上测试

1
uv run langgraph dev

NYC8m8

概述

LangChain 1.0 是一个强大的中文大语言模型框架,支持多种结构化输出功能。本文将介绍如何实现 LLM 的结构化输出。

什么是结构化输出

结构化输出是指让大语言模型按照预定义的结构(如 JSON、XML、YAML 等格式)返回结果,而不是自由文本。这种输出方式具有以下优势:

  1. 易于解析:程序可以直接解析结构化数据
  2. 格式统一:输出格式标准化,便于后续处理
  3. 类型安全:可以定义数据类型和约束
  4. 自动化友好:便于集成到自动化流程中

结构化输出实现

环境准备

首先安装必要的依赖:

1
2
3
4
pip install --pre -U langchain==1.0.0a9
pip install pydantic
pip install -U langchain-openai==1.0.0a2
pip install -U langchain-core==1.0.0a4

基于Pydantic Model实现

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# -*- coding: utf-8 -*-
from pydantic import BaseModel, Field
from typing import List, Optional
from langchain.agents import create_agent
import json
import os

# 定义产品信息输出结构
class ProductInfo(BaseModel):
name: Optional[str] = Field(description="产品名称")
catalog_number: Optional[str] = Field(description="规格货号")
description: Optional[str] = Field(description="产品描述")
validity_period: Optional[str] = Field(description="有效期")
storage_temperature: Optional[str] = Field(description="保存温度")

# 设置环境变量(推荐方式)
os.environ["OPENAI_API_BASE"] = "https://{HOST}/v1"
os.environ["OPENAI_API_KEY"] = "sk-XXX"

# 创建代理 - 使用环境变量配置
agent = create_agent(
model="openai:XXXX",
tools=[], # 可以添加工具,这里暂时为空
response_format=ProductInfo # Auto-selects ProviderStrategy
)

def extract_product_info(text: str) -> ProductInfo:
"""从文本中提取产品结构化信息"""

try:
# 使用 langchain agent 进行结构化提取
result = agent.invoke({
"messages": [{"role": "user", "content": f"""
请从以下产品说明书中提取结构化信息:

文本内容:
{text}

请提取以下信息:
1. 产品名称
2. 规格货号(产品编号)
3. 产品描述
4. 有效期
5. 保存温度
6. 注意事项列表

请严格按照ProductInfo模型的结构返回结果。
"""}]
})

# 返回结构化响应
return result["structured_response"]

except Exception as e:
print(f"API调用失败: {e}")
# 返回一个默认的产品信息对象
return ProductInfo(
name="提取失败",
catalog_number="未知",
description="无法提取产品描述",
validity_period="未知",
storage_temperature="未知",
precautions=["提取过程中出现错误"]
)

# 示例文本
sample_text = """
## 达优

## Mouse IL-22 Precoated ELISA Kit

Cat#:1212202/1212203

ELISA 试剂盒 说明书

本试剂盒仅供科研使用,请勿用于诊断 使用前请仔细阅读说明书并检查试剂盒组分
## Mouse IL-22

## Precoated ELISA Kit

ELISA试剂盒

## | 检测原理

达优ELISA 试剂盒采用双抗体夹心酶联免疫吸附检测技术。特异性的抗体包 被到酶标板上,标准品或样本加入孔中,样本中含有的目标物质被固定化的抗体 捕获,加入检测抗体,形成抗体-抗原-抗体的复合体,加入酶和底物,在酶的催 化下底物发生反应,且反应的颜色与目标物质的含量成正比,加入终止液后,在 酶标仪中测定反应的吸光值。

## | 预期用途

定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。

## | 知识背景

白细胞介素22(Interleukin-22, IL-22)又称白细胞介素 10 相关 T 细胞衍 生诱导因子,与IL-10,IL-19,IL-20,IL-24 和 IL-26 同属于 IL-10 调节性细胞 因子。 它主要由Th22 细胞分泌, IL-23 和 IL-6 可直接刺激初始 T 细胞产生 IL-22, 某些NK细胞也可以产生IL-22。其次,IL-22以皮肤,胰腺,肝脏等组织细胞为 主要靶细胞,调节这些组织细胞的功能,它在感染免疫,组织修复、肝炎产生和 肝癌形成中起重要作用。

## | 保存条件

未开封试剂盒在2℃~8℃可稳定保存12个月。

## 开封后各组分保存条件见下:

## Cat#:1212202

| 组分 | 规格 | 数量 | 保存条件 |
|------------------------|-------|------|--------------------------------------|
| Cytokine Standard | | 2瓶 | 48 T 复溶后-25℃~-15℃保存15天,分装保 存,不可重复冻融。 |
| Biotinylated Antibody | μL | 2管 | 50 原液2℃~8℃可稳定保存1个月,稀释 后不可保存。 |
| Streptavidin-HRP | 50 μL | 2管 | 原液2℃~8℃可稳定保存1个月,稀释 后不可保存。 |
| Dilution Buffer R (1×) | 10 mL | 3瓶 | 2℃~8℃可稳定保存1个月。 |
| Washing Buffer (50×) | 15 mL | 1瓶 | 原液2℃~8℃可稳定保存1个月,稀释 后不可保存。 |
| TMB | 10 mL | 1瓶 | 2℃~8℃可稳定保存1个月。 |
| Stop Solution | 10 mL | 1瓶 | 2℃~8℃可稳定保存1个月。 |
| Precoated ELISA plate | 96 T | 1块 | 2℃~8℃可稳定保存1个月,密封保存。 |
| 封板膜 | / | | 4张 / |
| 说明书 | / | 1份 | / |

Cat#:1212203

| 组分 | 规格 | 数量 | 保存条件 |
|-------------------------|--------|------|----------------------------------|
| Cytokine Standard | 48 T | 10瓶 | 复溶后-25℃~-15℃保存15天, 分装 保存,不可重复冻融。 |
| Biotinylated Antibody | 50 μL | 10管 | 原液2℃~8℃可稳定保存1个月,稀 释后不可保存。 |
| Streptavidin-HRP | 500 μL | 1管 | 原液2℃~8℃可稳定保存1个月,稀 释后不可保存。 |
| Dilution Buffer R (10×) | 18 mL | 1瓶 | 2℃~8℃可稳定保存1个月。 |
| Washing Buffer (50×) | 75 mL | 1瓶 | 原液2℃~8℃可稳定保存1个月,稀 释后不可保存。 |
| TMB | 50 mL | 1瓶 | 2℃~8℃可稳定保存1个月。 |
| Stop Solution | 50 mL | 1瓶 | 2℃~8℃可稳定保存1个月。 |
| Precoated ELISA plate | 96 T | 5块 | 2℃~8℃可稳定保存1个月,密封保 存。 |
| 封板膜 | / | 20张 | / |
| 说明书 | / | 1份 | / |

## | 需自备的材料设备

+ 能够检测450nm和630nm吸光度的酶标仪
+ 微量加液器及枪头、加样槽
+ 蒸馏水或去离子水
+ 旋涡振荡器或磁力搅拌器

## | 注意事项

1. 试剂按标签和说明书储存,使用前平衡至室温。
2. 标准品按照标签溶解,充分混匀,按照一次使用量分装保存。
3. 预包被板条使用前平衡至室温;剩余板条及时密封保存。
4. Washing Buffer (50×)在 2℃~8℃保存可能有结晶析出,若有析出加热或 平衡温度使其溶解后再使用。
5. 离心管试剂体积量较小,使用前高速短暂离心收集试剂到管底。
6. 实验操作中请使用一次性的吸头,避免交叉污染。
7. 使用前检查试剂盒内各种试剂;试剂稀释及加样时注意混匀试剂。
8. 实验板孔加入试剂的顺序应一致,以保证所有反应孔的孵育时间一致。
9. 使用干净的容器配制试剂。
10. 洗涤后孔中残留的洗涤液应在吸水纸上充分拍干,直至吸水纸上看不到 水印。请勿直接将孔中的洗涤液吸干。
11. TMB 对光和金属敏感,避免长时间暴露于光下,避免与金属接触。未使 用的TMB若变为蓝色,表明TMB已经污染,请丢弃。
12. 请严格按照说明书建议的反应时间和温度进行孵育。 冬季室内温度偏低, 建议使用恒温箱或培养箱孵育。
13. 请在保质期内使用试剂盒,且不同批号试剂不要混用。
14. 200 pg/mL 以上的结果为非线性的,根据此标准曲线无法得到精确的结
果。大于200pg/mLIL-22 的样本应稀释后重新测试。在结果分析时,结合考虑 相应的稀释度。

15. 安全提示: 使用本试剂盒时请做好合适的防护措施,如穿戴白大褂、乳 胶手套、安全眼镜等;避免试剂接触皮肤和眼睛,如不慎接触,立即用大量清水 清洗。
16. 废弃处置: 终止液使用碱性溶液中和或稀释后,再用大量水冲入废水系 统。其他成分按一般废弃物处理方法和当地相关法规的要求进行处理。

## | 样本处理说明

新鲜标本尽早检测,对收集后当天就进行检测的标本,及时储存2℃~8℃。 如有特殊原因需要周期性收集标本,请在取材后,将标本及时分装并放在-25℃ ~-15℃或更低温度条件下保存,避免反复冻融。

+ 血清*: 室温血液自然凝固10~20分钟后,离心20分钟(2000~3000 转/分),仔细收集上清。保存过程中如有沉淀形成,应再次离心取上清。
+ 血浆*: 应根据标本的要求选择EDTA、柠檬酸钠或肝素作为抗凝剂,混 合10~20 分钟后,离心 20 分钟(2000~3000 转/分),仔细收集上清。保存过 程中如有沉淀形成,应再次离心。
+ 细胞培养上清: 检测分泌性的成份时,请用无菌管收集细胞培养悬液, 离心20分钟(2000~3000 转/分),仔细收集上清。检测细胞内的成份时,用 1×PBS(pH 7.2~7.4)将细胞悬液稀释至 10 6 cell/mL 左右,反复冻融,以使细 胞破碎释放细胞内成份,离心20分钟(2000~3000转/分),仔细收集上清。 细胞培养上清保存过程中如有沉淀形成,应再次离心。

* 血浆血清样本检测之前需用DilutionBufferR(1×)至少稀释 2 倍后检测, 以降低样本 基质效应。具体稀释倍数需预实验确定。


## | 试剂的配制

1. 提前20分钟将Washing Buffer(50×)和即用溶液从试剂盒中取出,平衡 至室温。
2. 需稀释配制成工作液的试剂现配现用。
3. Cytokine Standard:先按标签说明将冻干粉溶解,溶解后的浓度为 200 pg/mL,再用 1×Dilution Buffer R (1×)进行倍比稀释。稀释前将标准品轻轻 振荡5分钟。
+ 推荐标准品浓度梯度为: 200 pg/mL、100 pg/mL、50 pg/mL、 25 pg/mL、12.5 pg/mL、6.25 pg/mL、3.13 pg/mL,为确保标准品稀释的准 确性,建议在实验孔外进行标准品的倍比稀释。
+ 稀释步骤: 校准品按瓶标签进行溶解后移入EP管中,标记为C7。再取 6 个 EP 管,分别标记为 C6/C5/C4/C3/C2/C1,每个管中加 250 μL 稀释液,从 C7 中取出 250 μL 高浓度校准品至 C6 管中进行 2 倍倍比稀释,以同样方式稀释 C5/C4/C3/C2/C1 管中,以稀释液作为 C0。
4. Biotinylated Antibody:1:100 用 Dilution Buffer R (1×)稀释,混匀制 成Biotinylated Antibody 工作液。
5. Streptavidin-HRP:1:100 用 Dilution Buffer R (1×)稀释,混匀制成 Streptavidin-HRP 工作液。
6. Washing Buffer (50×):1:50 用蒸馏水稀释。
7. *Dilution Buffer R (10×): 1:9 用 1×PBS (pH=7.2~7.4) 配制成 1×Dilution Buffer R 工作液,只用于稀释 Cytokine Standard、Biotinylated Antibody、 Streptavidin-HRP 和样品

*此步骤适用于Cat#:1212203 试剂盒,Cat#:1212202 中本试剂为 1×工作液,无需 稀释。


## | 即用型试剂

+ Dilution Buffer R (1×):用于稀释 Cytokine Standard、样本、 Biotinylated Antibody 和 Streptavidin-HRP。
+ TMB
+ Stop Solution


## | 操作过程

1. 使用前,将所有试剂充分混匀,避免产生泡沫。
2. 根据实验孔(空白和标准品)数量,确定所需的板条数目。样本(含标准 品)和空白都应做复孔。
3. 加样: 100 μL/well 加入稀释后的 Cytokine Standard 至标准品孔, 100 μL/well 加入样本至样本孔, 100 μL/well 加入 Dilution Buffer R (1×)至空 白对照孔,37℃孵育90分钟。
4. 洗板: 扣去孔内液体,300 μL/well 加入 1×Washing Buffer 工作液;停 留1分钟后弃去孔内液体。重复4次,每一次在滤纸上扣干。
5. 加检测抗体 :100 μL/well 加入 Biotinylated Antibody 工作液。混匀后 盖上封板膜,37℃孵育60分钟。
6. 洗板:重复步骤4。
7. 加酶: 100 μL/well 加入 Streptavidin-HRP 工作液。盖上封板膜,37℃ 孵育30分钟。
8. 洗板:重复步骤4。
9. 显色: 100 μL/well 加入 TMB,37℃避光孵育 10~20 分钟。可根据孔内 颜色的深浅判断,校准曲线最高值孔为深蓝色,空白孔无明显蓝色时即可终止反 应。
10. 终止反应: 100 μL/well 迅速加入 Stop Solution 终止反应。
11. 读板: 终止反应10分钟内在酶标仪进行双波长读板,设置检测波长 450 nm 和参考波长 610 nm~630 nm;校正后的 OD 值为检测波长的测定值减 去参考波长的测定值。

## | 结果分析

1. 推荐拟合曲线坐标对数或自然数,拟合方程常见为直线、二次方程及四参 数方程,通过各种应用软件拟合选取最佳标准曲线,根据样本OD值查找相应浓 度。
2. 稀释的样本计算浓度时应乘以稀释倍数;若样品浓度高于最高线性值,需 稀释样本并重新测定。
3. 示例数据仅供参考, 每次实验 必须制备当次实验的标准曲线。

| Mouse IL-22 Concentration (pg/mL) | OD | OD | Average OD |
|-------------------------------------|-------|-------|--------------|
| 200 | 3.436 | 3.455 | 3.446 |
| 100 | 2.693 | 2.687 | 2.69 |
| 50 | 1.547 | 1.584 | 1.566 |
| 25 | 0.957 | 0.839 | 0.898 |
| 12.5 | 0.407 | 0.449 | 0.428 |
| 6.25 | 0.236 | 0.247 | 0.242 |
| 31.3 | 0.146 | 0.128 | 0.137 |
| 0 | 0.044 | 0.043 | 0.044 |## | 性能数据

- 1. 灵敏度: 1 pg/mL,10 个空白样本的对应浓度平均值加上三倍标准偏差。
- 2. 精密度: 板内精密度:3个已知浓度的样本板内重复测定10次,计算得 到板内变异系数;板间精密度:3个已知浓度的样本板间分别重复测定3次,计 算得到板间变异系数

| | 板内精密度 | 板内精密度 | 板内精密度 | 板间精密度 | 板间精密度 | 板间精密度 |
|------------|---------|---------|---------|---------|---------|---------|
| 样本 | 1 | 2 | 3 | 1 | 2 | 3 |
| 测试次数 | 10 | 10 | 10 | 9 | 9 | 9 |
| 标准差 | 0.46 | 1.46 | 5.15 | 0.56 | 1.56 | 5.14 |
| 平均值(pg/mL) | 6.74 | 25.8 | 116.36 | 6.74 | 25.63 | 114.81 |
| 变异系数CV(%) | 6.79 | 5.67 | 4.43 | 8.31 | 6.09 | 4.47 |

- 3. 校准: 试剂盒校准品为达优校准的高纯度重组IL-22。

## | 参考文献

[1] Jia, L., Jiang, Y., Wu, L.et al.Porphyromonas gingivalis aggravates colitis via a gut microbiota-linoleic acid metabolism-Th17/Treg cell balance axis[J].Nat Commun, 2024.

[2] Huang,R.,Jia,B.,Su,D.,Li,M.,Xu,Z.,He,C.,Huang,Y.,Fan,H.,Chen,H., &Cheng,F.(2023).Plant exosomes fused with engineered mesenchymal stemcell-derived nanovesicles for synergistic therapy of autoimmune skin disorders[J].Journal of Extracellular Vesicles.


"""

# 执行产品信息结构化提取
try:
result = extract_product_info(sample_text)
print("产品信息提取结果:")
print(json.dumps(result.model_dump(), ensure_ascii=False, indent=2))
except Exception as e:
print(f"提取失败:{e}")

最终结构化输出:

1
2
3
4
5
6
7
{
"name": "Mouse IL-22 Precoated ELISA Kit",
"catalog_number": "1212202",
"description": "Mouse IL-22 Precoated ELISA Kit,用于定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。采用双抗体夹心酶联免疫吸附检测技术。",
"validity_period": "12个月",
"storage_temperature": "2℃~8℃"
}

基于 JSON Schema 的结构化输出

除了使用 Pydantic 模型,langchain 1.0 还支持直接使用 JSON Schema 定义输出格式。这种方式更加灵活,特别适合动态配置的场景。

JSON Schema 实现示例

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# -*- coding: utf-8 -*-
from langchain.agents import create_agent
import json
import os

# 定义产品信息的 JSON Schema(对应基础实现中的ProductInfo结构)
product_info_schema = {
"type": "object",
"description": "Product information extracted from documentation.",
"properties": {
"name": {"type": "string", "description": "产品名称"},
"catalog_number": {"type": "string", "description": "规格货号"},
"description": {"type": "string", "description": "产品描述"},
"validity_period": {"type": "string", "description": "有效期"},
"storage_temperature": {"type": "string", "description": "保存温度"}
},
"required": []
}

# 设置环境变量
os.environ["OPENAI_API_BASE"] = "https://{HOST}/v1"
os.environ["OPENAI_API_KEY"] = "sk-XXX"

# 创建代理 - 使用 JSON Schema 作为响应格式
agent = create_agent(
model="openai:XXXX",
tools=[], # 可以添加工具,这里暂时为空
response_format=product_info_schema # Auto-selects ProviderStrategy
)

def extract_product_info_json(text: str) -> dict:
"""从文本中提取产品结构化信息(使用JSON Schema)"""

try:
# 使用 langchain agent 进行结构化提取
result = agent.invoke({
"messages": [{"role": "user", "content": f"""
请从以下产品说明书中提取结构化信息:

文本内容:
{text}

请提取以下信息:
1. 产品名称
2. 规格货号(产品编号)
3. 产品描述
4. 有效期
5. 保存温度

请严格按照JSON Schema的结构返回结果。
"""}]
})

# 返回结构化响应
return result["structured_response"]

except Exception as e:
print(f"API调用失败: {e}")
# 返回一个默认的产品信息对象
return {
"name": "提取失败",
"catalog_number": "未知",
"description": "无法提取产品描述",
"validity_period": "未知",
"storage_temperature": "未知"
}

# 示例文本(使用基础实现中的相同示例)
sample_text = """
## 达优

## Mouse IL-22 Precoated ELISA Kit

Cat#:1212202/1212203

ELISA 试剂盒 说明书

本试剂盒仅供科研使用,请勿用于诊断 使用前请仔细阅读说明书并检查试剂盒组分
## Mouse IL-22

## Precoated ELISA Kit

ELISA试剂盒

## | 检测原理

达优ELISA 试剂盒采用双抗体夹心酶联免疫吸附检测技术。特异性的抗体包 被到酶标板上,标准品或样本加入孔中,样本中含有的目标物质被固定化的抗体 捕获,加入检测抗体,形成抗体-抗原-抗体的复合体,加入酶和底物,在酶的催 化下底物发生反应,且反应的颜色与目标物质的含量成正比,加入终止液后,在 酶标仪中测定反应的吸光值。

## | 预期用途

定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。

## | 保存条件

未开封试剂盒在2℃~8℃可稳定保存12个月。
"""

# 执行产品信息结构化提取
try:
result = extract_product_info_json(sample_text)
print("产品信息提取结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(f"提取失败:{e}")

最终输出:

1
2
3
4
5
6
7
{
"catalog_number": "1212202/1212203",
"description": "定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。",
"name": "Mouse IL-22 Precoated ELISA Kit",
"storage_temperature": "2℃~8℃",
"validity_period": "12个月"
}

扩展 JSON Schema 示例

扩展的产品信息 JSON Schema(在基础结构上增加更多字段)

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
from langchain.agents import create_agent
import json
import os

extended_product_schema = {
"type": "object",
"description": "Extended product information with additional details",
"properties": {
"name": {"type": "string", "description": "产品名称"},
"catalog_number": {"type": "string", "description": "规格货号"},
"description": {"type": "string", "description": "产品描述"},
"validity_period": {"type": "string", "description": "有效期"},
"storage_temperature": {"type": "string", "description": "保存温度"},
"category": {"type": "string", "description": "产品类别"},
"brand": {"type": "string", "description": "品牌"},
"precautions": {
"type": "array",
"items": {"type": "string"},
"description": "注意事项列表"
},
"specifications": {
"type": "array",
"items": {
"type": "object",
"properties": {
"spec_name": {"type": "string", "description": "规格名称"},
"spec_value": {"type": "string", "description": "规格值"},
"unit": {"type": "string", "description": "单位"}
},
"required": ["spec_name", "spec_value"]
},
"description": "技术规格列表"
}
},
"required": []
}

# 设置环境变量
os.environ["OPENAI_API_BASE"] = "https://{HOST}/v1"
os.environ["OPENAI_API_KEY"] = "sk-XXX"


# 创建扩展产品信息提取代理
extended_agent = create_agent(
model="openai:XXXX",
tools=[],
response_format=extended_product_schema
)

def extract_product_info_json(text: str) -> dict:
"""从产品描述中提取扩展信息"""

try:
result = extended_agent.invoke({
"messages": [{"role": "user", "content": f"""
请从以下产品说明书中提取结构化信息:

{text}

请提取以下信息:
1. 产品名称
2. 规格货号(产品编号)
3. 产品描述
4. 有效期
5. 保存温度
6. 产品类别
7. 品牌
8. 注意事项列表
9. 技术规格列表

请严格按照JSON Schema的结构返回结果。
"""}]
})

return result["structured_response"]

except Exception as e:
print(f"扩展产品信息提取失败: {e}")
return {
"name": "提取失败",
"catalog_number": "未知",
"description": "无法提取产品描述",
"validity_period": "未知",
"storage_temperature": "未知",
"category": "未知",
"brand": "未知",
"precautions": ["提取过程中出现错误"],
"specifications": []
}

# 示例文本(使用基础实现中的相同示例)
sample_text = """
## 达优

## Mouse IL-22 Precoated ELISA Kit

Cat#:1212202/1212203

ELISA 试剂盒 说明书

本试剂盒仅供科研使用,请勿用于诊断 使用前请仔细阅读说明书并检查试剂盒组分
## Mouse IL-22

## Precoated ELISA Kit

ELISA试剂盒

## | 检测原理

达优ELISA 试剂盒采用双抗体夹心酶联免疫吸附检测技术。特异性的抗体包 被到酶标板上,标准品或样本加入孔中,样本中含有的目标物质被固定化的抗体 捕获,加入检测抗体,形成抗体-抗原-抗体的复合体,加入酶和底物,在酶的催 化下底物发生反应,且反应的颜色与目标物质的含量成正比,加入终止液后,在 酶标仪中测定反应的吸光值。

## | 预期用途

定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。

## | 保存条件

未开封试剂盒在2℃~8℃可稳定保存12个月。
"""

# 执行产品信息结构化提取
try:
result = extract_product_info_json(sample_text)
print("产品信息提取结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(f"提取失败:{e}")

最后输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"brand": "达优",
"catalog_number": "1212202/1212203",
"category": "ELISA试剂盒",
"description": "本试剂盒采用双抗体夹心酶联免疫吸附检测技术,用于定量检测血清、血浆、缓冲液或细胞培养液中的IL-22含量。",
"name": "Mouse IL-22 Precoated ELISA Kit",
"precautions": [
"本试剂盒仅供科研使用,请勿用于诊断",
"使用前请仔细阅读说明书并检查试剂盒组分",
"未开封试剂盒在2℃~8℃可稳定保存12个月"
],
"storage_temperature": "2℃~8℃",
"validity_period": "12个月"
}

JSON Schema 的优势

  1. 动态配置:可以在运行时动态修改 Schema
  2. 标准化:遵循 JSON Schema 标准,兼容性好
  3. 灵活性:支持复杂的嵌套结构和数组
  4. 验证:可以结合 JSON Schema 验证库进行数据验证
  5. 文档化:Schema 本身就是很好的文档

应用场景

  1. 产品数据库建设:自动提取产品说明书信息,建立结构化产品数据库
  2. 质量控制:标准化产品信息格式,提高信息管理效率
  3. 技术文档处理:批量处理技术文档,提取关键信息
  4. 产品对比分析:结构化产品信息便于进行产品对比和分析
  5. 自动化流程:将产品信息提取集成到自动化工作流中
  6. 医药产品管理:提取试剂盒、药品等医疗产品的规格和保存条件
  7. 实验室管理:自动提取实验试剂和仪器的技术参数

总结

langchain 1.0 提供了强大的结构化输出功能,通过合理的数据模型设计和错误处理机制,可以实现高质量的产品信息结构化提取。本文提供的完整示例代码可以直接运行,为产品信息管理项目提供参考。

通过结构化输出,我们可以更好地将 LLM 的能力集成到产品信息自动化处理流程中,提高数据处理效率和质量,为产品管理和技术文档处理提供有力支持。

背景

2025年08月31日,参加了个AI深度学习workshop,记录和复盘一下整个workshop例子。

环境准备

  • 显卡:L20

    因为手上有算力资源,本次记录就不使用AutoDL租借算力,本次使用L20,顺便完善一下yolo11环境,方便以后训练复用,同时也顺便测一下与4090的算力情况。

  • 训练环境:yolo11环境搭建
  • 视觉模块:reCamera 摄像头 $49.90刀
    • 缺点:我觉得像素还是上不了生产,还不如一个高清摄像头。
    • 优点:小,不仅仅一个摄像头,相当于一个边缘设备,可以独立部署上去。整合了工作流,可以快速部署监测模型。

实践

数据集准备

首先到 roboflow 找一份数据集,这次复盘记录我选一个螺丝数据集吧,毕竟这个reCamera的外观有一点工业风,就拿工业场景来做比较合适。

数据训练

前面准备了yolo11环境搭建,我们开始在这个yolo11环境开始训练。

模型量化与格式转换

  • 工具安装

    我们使用docker

    1
    2
    3
    4
    5
    # 拉取镜像并启动容器
    docker pull sophgo/tpuc_dev:v3.1
    docker run --privileged --name reCamera_dev -v $PWD:/workspace -it sophgo/tpuc_dev:v3.1
    #容器内安装工具包 tpu_mlir
    pip install tpu_mlir[all]==1.7 -i https://mirrors.aliyun.com/pypi/simple

    上面方法慢的话:

    1
    2
    3
    4
    5
    wget https://sophon-assets.sophon.cn/sophon-prod-s3/drive/25/04/15/16/tpuc_dev_v3.4.tar.gz
    docker load -i tpuc_dev_v3.4.tar.gz
    docker run --privileged --name reCamera_dev -v $PWD:/workspace -it sophgo/tpuc_dev:v3.4
    #容器内安装工具包 tpu_mlir
    pip install tpu_mlir[all]==1.7 -i https://mirrors.aliyun.com/pypi/simple
  • 格式转换

    之所以要进行格式转换,因为不同的部署平台的架构不一样,支持的的格式不一

    • reCamera 基于RISC-V架构
      • ONNX→MLIR转换→cvimodel量化

部署与测试

  • 使用reCamera测试
  • 在本地电脑部署测试
  • 在手机端部署测试

场景思考

  • 深度学习在供应链中的应用

相关链接

ReCamera AI模型转换与量化全流程指南

基础搭建

  • 创建虚拟环境

    1
    conda create -n yolo11 python=3.11 -y 
  • 安装ultralytics

    我们使用源代码进行安装

    1
    2
    3
    4
    cd /home
    git clone https://github.com/ultralytics/ultralytics.git
    cd ultralytics
    pip install -e .

    这段命令会安装你下载的ultralytics,安装完成后pip list你就会发现包会有一个路径,就表示安装成功了。

    1
    ultralytics              8.3.193     /home/ultralytics
  • 安装pytorch

    根据你的硬件配置选择合适的 PyTorch 版本。如果有 NVIDIA GPU,建议安装 CUDA 版本;如果只有 CPU,安装 CPU 版本即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 激活环境
    conda activate yolo11

    # 如果有 NVIDIA GPU,访问 https://pytorch.org/ 获取最新的 CUDA 版本安装命令
    # 例如 CUDA 12.8 版本:
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

    # 如果只有 CPU:
    # pip install torch torchvision torchaudio
  • 验证,推理

    验证 PyTorch 和 CUDA 是否正确安装

    1
    2
    3
    4
    5
    6
    7
    import torch
    print(f"PyTorch 版本: {torch.__version__}")
    print(f"CUDA 可用: {torch.cuda.is_available()}")
    if torch.cuda.is_available():
    print(f"CUDA 版本: {torch.version.cuda}")
    print(f"GPU 数量: {torch.cuda.device_count()}")
    print(f"GPU 名称: {torch.cuda.get_device_name(0)}")

    输出

    1
    2
    3
    4
    5
    PyTorch 版本: 2.8.0+cu128
    CUDA 可用: True
    CUDA 版本: 12.8
    GPU 数量: 1
    GPU 名称: NVIDIA L20

    简单的推理示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from ultralytics import YOLO

    # 加载预训练模型
    model = YOLO('yolo11n.pt') # 会自动下载模型

    # 对图片进行推理
    results = model('path/to/your/image.jpg')

    # 显示结果
    results[0].show()

    # 保存结果
    results[0].save('output.jpg')

    或者使用命令行进行推理

    1
    yolo predict model=yolo11n.pt source='path/to/your/image.jpg'

目标

探索使用MCP与商品结合的场景,让大模型能回答和理解用户的产品需求并获得需要的产品信息。

  • 用户问答搜索产品场景
    • 根据关键字搜索产品详情。
    • 最热门商品
  • 用户问答根据用户个性化推荐场景
    • 根据用户ID推荐商品。

注意数据为mock,根据需要对接数据库或者其他数据来源即可。

实践

编写MCP服务程序

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
# 引入 fastmcp 包
from fastmcp import FastMCP
# 创建一个mcp服务
mcp = FastMCP("ProductServer")

# 定义工具,也就是实际执行的内容,类似于 RestAPI 中的 POST,mcp主要的能力都在这里体现。

# 定义根据关键字keyword搜索产品详情的工具函数
@mcp.tool()
def search_produc_by_keyword(keyword):
"""search product by keyword"""
# mock data
return {"product_name":"小米SU7 Ultra", "price": 529999, "category": "Car"}

# 定义获取热门商品的工具函数
@mcp.tool()
def get_popular_products():
"""List of popular products"""
# mock data
hot_products = ('[{"product_name":"iPhone 14 Pro Max", "price": 5999, "category": "Electronics"},'
'{"product_name":"小米SU7 Ultra", "price": 529999, "category": "Car"},'
'{"product_name":"红米K80", "price": 2399, "category": "Electronics"}]')

return hot_products


# 定义为指定用户获取推荐商品的工具函数
@mcp.tool()
def get_recommended_products(user_id):
"""Products recommended for users"""
# mock data
recommended_products = '{"product_name":"小米SU7 Ultra", "price": 529999, "category": "Car"}'
return recommended_products


if __name__ == "__main__":
mcp.run(transport='stdio')

运行mcp,验证一下是否能正常运行,确认能正常后,我们在Cherry Studio中配置一下mcp

1
fastmcp run main.py

在Cherry Studio中配置MCP

  • 在命令中填写 uv
  • 填写参数

    注意 /{PATH}/fastmcp/为这个mcp的项目路径

    1
    2
    3
    4
    --directory
    /{PATH}/fastmcp/
    run
    main.py
    其实组合起来就是 uv --directory /{PATH}/fastmcp/ run main.py,即跑起来文件项目
    JrtIR3

配置好后,我们可以看到工具中已经 包含了

  • search_produc_by_keyword 根据关键字搜索产品
  • get_popular_products 获取热门商品
  • get_recommended_products 获取推荐商品
    RIkSsQ

通过问答调用产品mcp

我们会发现在如下的问答过程,会根据问题,自动调用mcp服务获得需要的产品信息。

  • “我想了解一下当下的热门产品,帮我获取一下热门产品信息。”
    m7bpbS
  • “我想查询一下关键字:小米SU7 Ultra,了解一下它的价格。”
    4emS9b
  • “请为用户ID为1的用户,推荐商品。”
    jG3pwW

未来验证

模块化产品订单mcp 服务,即可接入大模型,进行问答调用。

相关

结合FastMCP创建自己的MCP服务,实现哔哩视频查询
基于FastMCP 2.0的MCP Server快速搭建指南

背景

  • 行业:生物试剂
  • 手上有自产试剂的所有说明书,大概300多份
  • 不限于说明书的资料

目标

希望微调出一个名字为 dayou 的自产生物试剂行业垂直大语言模型。首要目标是能解答说明书内的相关内容,继而继续增加数据集,完善该 dayou 自训练大模型,或者分领域分别训练 dayou-saledyou-manualdyou-tech大模型。

本次可行性验证,我们从说明书选取如下2个试剂产品

  • IMDM培养基 (6016311 IMDM培养基说明书(中文)–C2–A4.docx)
  • TransFx转染用无血清培养基 (6066411 TransFx转染用无血清培养基说明书–C0.docx)

通过AI对2个产品的docx文件进行制作QA的数据集。

实践

  • 微调模型选择:Qwen2.5-7B-Instruct
  • 微调框架选择:LLaMA-Factory
  • GPU显卡选择: RTX 4090
  • 数据集制作:easy-dataset

算力准备及训练环境准备

本次实验使用AutoDL AI算力云 租借算力,采用GPU卡进行训练,使用Ubuntu 24.04LTS版本
IX3I8x

AutoDL的系统盘和数据盘可以通过source ~/.bashrc 查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ source ~/.bashrc
+-----------------------------------------------AutoDL-----------------------------------------------------+
目录说明:
╔═════════════════╦════════╦════╦══════════════════════════════════════════════════════════════╗
║目录 ║名称 ║速度 ║说明 ║
╠═════════════════╬════════╬════╬══════════════════════════════════════════════════════════════╣
║/ ║系 统 盘 ║一般║实例关机数据不会丢失,可存放代码等。会随保存镜像一起保存。 ║
║/root/autodl-tmp ║数 据 盘 ║ 快 ║实例关机数据不会丢失,可存放读写IO要求高的数据。但不会随保存镜像一起保存 ║
╚═════════════════╩════════╩════╩═══════════════════════════════════════════════════════════════╝
CPU :16 核心
内存:120 GB
GPU :NVIDIA GeForce RTX 4090, 1
存储:
系 统 盘/ :29% 8.6G/30G
数 据 盘/root/autodl-tmp:1% 72M/50G

LLaMA-Factory搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
## 克隆项目
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
## 创建环境
conda create -n llamafactory python=3.10
## 激活环境
conda activate llamafactory
## 安装依赖
pip install -e ".[torch,metrics]" --no-build-isolation -i https://mirrors.aliyun.com/pypi/simple
## 执行webui
llamafactory-cli webui
## 后台运行
nohup llamafactory-cli webui > webui.log 2>&1 &
  • 下载模型
    a0F1dg
    界面中选择Qwen2.5-7B-Instruct,并加载模型,会自动下载到默认路径: /root/.cache/modelscope/hub/models/Qwen,我们自己手动下载并指定路径。
    1
    2
    3
    4
    5
    6
    7
    8
    pip install modelscope
    modelscope download --model Qwen/Qwen2.5-7B-Instruct --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-7B-Instruct
    modelscope download --model Qwen/Qwen2.5-1.5B-Instruct --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct

    apt install git-lfs
    git lfs install
    git clone https://www.modelscope.cn/Qwen/Qwen2.5-7B-Instruct.git /root/autodl-tmp/LLaMA-Factory/models/Qwen
    git clone https://www.modelscope.cn/Qwen/wen2.5-1.5B-Instruct.git /root/autodl-tmp/LLaMA-Factory/models/Qwen

数据集准备

我们使用easy-dataset来简化我们的数据集制作流程,通过大模型自动生成QA数据集。

  • 初始化数据库文件。
    1
    2
    3
    4
    git clone https://github.com/ConardLi/easy-dataset.git
    cd easy-dataset
    npm install
    npm run db:push
  • 部署
    docker-compose.yml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    services:
    easy-dataset:
    image: ghcr.io/conardli/easy-dataset
    container_name: easy-dataset
    ports:
    - '1717:1717'
    volumes:
    - ./local-db:/app/local-db
    - ./prisma:/app/prisma # 如果需要挂载请先手动初始化数据库文件
    restart: unless-stopped
    1
    docker-compose up -d
    就此我们部署好我们的数据集制作工具。
    wBoKqJ

我们来制作2个产品的数据集

  • IMDM培养基
    hvvrd7
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [
    {
    "instruction": "IMDM与RPMI-1640培养基在酚红含量上有何差异",
    "input": "",
    "output": "IMDM培养基含有酚红,其含量为15 mg/L。而RPMI-1640培养基的酚红含量在文档中未提及,表明该产品标签中未列出酚红作为成分。因此,IMDM与RPMI-1640培养基在酚红含量上的主要差异在于IMDM明确包含酚红,而RPMI-1640未标明含有该成分。",
    "system": "你是一个由“达科为”公司自产的生物试剂产品,品牌名为:“达优”,产品名称是:“IMDM培养基”,有一个规格,货号为:“6016311”"
    },
    {
    "instruction": "IMDM培养基是否含有抗生素?",
    "input": "",
    "output": "IMDM培养基不含抗生素。该培养基是一种改进型的DMEM培养基,特别设计用于红细胞和巨噬细胞前体、T淋巴细胞、B淋巴细胞、骨髓造血细胞和淋巴瘤细胞的培养。在产品参数中明确指出,该培养基不含抗生素成分,需额外添加胎牛血清等培养添加物以满足细胞生长的需求。",
    "system": "你是一个由“达科为”公司自产的生物试剂产品,品牌名为:“达优”,产品名称是:“IMDM培养基”,有一个规格,货号为:“6016311”"
    },
    ...一下省略,共41个问题
    ]
  • TransFx转染用无血清培养基
    cAEnPF
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [
    {
    "instruction": "分析无血清培养基中不含酚红、HEPES和胰岛素成分对减少转染试剂用量和避免血清干扰的实证数据支持。",
    "input": "",
    "output": "无血清培养基中不含酚红、HEPES和胰岛素的设计可有效减少转染试剂用量并提升转染效率。酚红作为pH指示剂,可能干扰细胞代谢及外源物质摄取;HEPES缓冲系统在某些情况下会与转染试剂发生非特异性结合,降低其活性。胰岛素则可能影响细胞膜通透性,干扰阳离子脂质体或聚合物介导的核酸传递。实验证明,在不含这些成分的TransFx培养基中,转染试剂(如TransIT-X2)与核酸复合物更稳定,细胞摄取效率提高,从而减少试剂用量并避免血清成分的干扰,最终实现更高效的基因转染。",
    "system": "你是一个由“达科为”公司自产的生物试剂产品,品牌名为:“达优”,产品名称是:“TransFx转染用无血清培养基”,有一个规格,货号为:“6066411”"
    },
    {
    "instruction": "比较不同储存条件(如温度2℃~8℃和避光)对TransFx培养基稳定性和转染效果的影响机制。",
    "input": "",
    "output": "TransFx转染用无血清培养基的稳定性与活性受储存条件影响显著。其推荐储存温度为2℃~8℃,该温度范围可维持培养基中植物源重组生长因子(如IGF-1和EGF)、氨基酸(如L-谷氨酰胺)及微量元素的化学稳定性,防止降解或失活。高温可能加速成分氧化或降解,影响其支持转染的能力。同时,避光保存可防止光敏感成分(如酚红替代物及某些维生素或辅因子)发生光化学反应,从而维持培养基的pH稳定性和生物活性。若未避光或温度波动较大,可能降低转染效率,影响核酸-转染试剂复合物的形成与细胞摄取能力,最终削弱基因表达效果。因此,遵循推荐储存条件对于维持TransFx培养基功能至关重要。",
    "system": "你是一个由“达科为”公司自产的生物试剂产品,品牌名为:“达优”,产品名称是:“TransFx转染用无血清培养基”,有一个规格,货号为:“6066411”"
    },
    ...一下省略,共43个问题
    ]

训练

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
llamafactory-cli train \
--stage sft \
--do_train True \
--model_name_or_path /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-3B-Instruct \
--preprocessing_num_workers 16 \
--finetuning_type lora \
--template qwen \
--flash_attn auto \
--dataset_dir data/dayou/ \
--dataset identity,[Easy Dataset] [oLYDc6WUW-2H] ShareGPT,[Easy Dataset] [ag7XS4Wdb3LM] ShareGPT \
--cutoff_len 1024 \
--learning_rate 0.0001 \
--num_train_epochs 20.0 \
--max_samples 100000 \
--per_device_train_batch_size 2 \
--gradient_accumulation_steps 16 \
--lr_scheduler_type cosine \
--max_grad_norm 1.0 \
--logging_steps 5 \
--save_steps 50 \
--warmup_steps 4 \
--packing False \
--enable_thinking True \
--report_to none \
--output_dir saves/Qwen2.5-3B-Instruct/lora/dayou_v6 \
--bf16 True \
--plot_loss True \
--trust_remote_code True \
--ddp_timeout 180000000 \
--include_num_input_tokens_seen True \
--optim adamw_torch \
--adapter_name_or_path saves/Qwen2.5-3B-Instruct/lora/dayou_v6 \
--lora_rank 16 \
--lora_alpha 32 \
--lora_dropout 0 \
--lora_target all

可行性总结与分析

硬件环境

首先按照 nvidia驱动安装 装下驱动

1
2
3
4
nvidia-smi

pip install nvitop
nvitop

TnPQDZ

环境准备

  • 安装依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ## 克隆项目
    git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
    cd LLaMA-Factory
    ## 创建环境
    conda create -n llamafactory python=3.10
    ## 激活环境
    conda activate llamafactory
    ## 安装依赖
    pip config set global.index-url https://mirrors.aliyun.com/pypi/simple
    pip install -e ".[torch,metrics]" --no-build-isolation -i https://mirrors.aliyun.com/pypi/simple

    如果出现环境冲突,请尝试使用 pip install –no-deps -e . 解决

  • 校验环境
    1
    2
    3
    4
    import torch
    torch.cuda.current_device()
    torch.cuda.get_device_name(0)
    torch.__version__
    1
    2
    3
    4
    5
    6
    7
    >>> import torch
    >>> torch.cuda.current_device()
    0
    >>> torch.cuda.get_device_name(0)
    'NVIDIA GeForce RTX 4090'
    >>> torch.__version__
    '2.8.0+cu128'
    1
    2
    # 对本库的基础安装做一下校验,输入以下命令获取训练相关的参数指导, 否则说明库还没有安装成功
    llamafactory-cli train -h
  • llamafactory-cli常见命令
    动作参数 功能说明
    version 显示版本信息
    train 命令行版本训练
    chat 命令行版本推理聊天
    export 模型合并和导出(如转换为Hugging Face/ONNX格式)
    api 启动API服务器,提供HTTP接口调用
    eval 使用标准数据集(如MMLU)评测模型性能
    webchat 启动纯推理的Web聊天页面(简易前端)
    webui 启动LlamaBoard多功能前端(含训练、预测、聊天、模型合并等可视化子页面)

    另外两个关键参数解释如下,后续的基本所有环节都会继续使用这两个参数

    参数名称 参数说明
    model_name_or_path 参数的名称(huggingface或者modelscope上的标准定义,如 Qwen/Qwen2.5-1.5B-Instruct),或者是本地下载的绝对路径,如/root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct
    template 模型问答时所使用的prompt模板,不同模型不同,请参考 LLaMA-Factory Supported Models 获取不同模型的模板定义,否则会回答结果会很奇怪或导致重复生成等现象的出现。chat版本的模型基本都需要指定,比如Qwen/Qwen2.5-1.5B-Instruct的template就是qwen
  • 示例:下面四行命令分别是 启动webui 和对Llama3-8B-Instruct模型进行 LoRA 微调、推理、合并。
    1
    2
    3
    4
    5
    CUDA_VISIBLE_DEVICES=0 USE_MODELSCOPE_HUB=1 llamafactory-cli webui
    CUDA_VISIBLE_DEVICES=0 USE_MODELSCOPE_HUB=1 llamafactory-cli webui > runlog.log &
    llamafactory-cli train examples/train_lora/llama3_lora_sft.yaml
    llamafactory-cli chat examples/inference/llama3_lora_sft.yaml
    llamafactory-cli export examples/merge_lora/llama3_lora_sft.yaml

下载模型

我们自己手动下载并指定路径。

1
2
3
4
5
pip install modelscope
modelscope download --model Qwen/Qwen2.5-1.5B-Instruct --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct
modelscope download --model Qwen/Qwen2.5-3B-Instruct --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-3B-Instruct
modelscope download --model Qwen/Qwen2.5-7B-Instruct --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-7B-Instruct
modelscope download --model Qwen/Qwen3-4B-Instruct-2507 --local_dir /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen3-4B-Instruct-2507

原始模型推理

transformers 原始模型直接推理

为qwen创建一个推理测试python脚本

1
2
mkdir demo
touch demo/generated_text.py

python generated_text.py

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
import transformers
import torch

# 模型路径(替换成你的实际路径)
model_id = "/root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct"

# 初始化 pipeline
pipeline = transformers.pipeline(
"text-generation",
model=model_id,
model_kwargs={"torch_dtype": torch.bfloat16},
device_map="auto",
)

# 对话模板
messages = [
{"role": "system", "content": "你是一个助手!"},
{"role": "user", "content": "你是谁?"},
]

# 生成 prompt(不要 tokenize,直接返回字符串)
prompt = pipeline.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)

# 检查 tokenizer 的特殊 token
# print("特殊 Token 映射:", pipeline.tokenizer.special_tokens_map)

# 设置终止符(优先用 <|eot_id|>,如果不存在则用 eos_token_id 或 pad_token_id)
terminators = [
pipeline.tokenizer.convert_tokens_to_ids("<|eot_id|>"), # Qwen 专用结束 token
pipeline.tokenizer.eos_token_id, # 常规结束 token
pipeline.tokenizer.pad_token_id # 备用 token
]
terminators = [tid for tid in terminators if tid is not None] # 过滤掉 None

# 如果 terminators 为空,手动设置一个默认值(比如 2,但需确认你的模型是否适用)
if not terminators:
terminators = [2] # 常见模型的默认 EOS token,可能需要调整

# 生成文本
outputs = pipeline(
prompt,
max_new_tokens=256,
eos_token_id=terminators, # 传入有效的终止符列表
do_sample=True,
temperature=0.6,
top_p=0.9,
)

# 打印生成的回复(去掉 prompt 部分)
print(outputs[0]["generated_text"][len(prompt):])

输出:

1
我是来自阿里云的大规模语言模型,我叫通义千问。

llamafactory-cli 原始模型直接推理

touch examples/inference/qwen2.5.yaml

1
2
3
4
model_name_or_path: /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct
template: qwen
infer_backend: huggingface # choices: [huggingface, vllm, sglang]
trust_remote_code: true

这样就可以通过如下命令启动:

  • 终端推理: llamafactory-cli chat examples/inference/qwen2.5.yaml
    1
    2
    3
    4
    5
    [INFO|2025-08-08 15:48:45] llamafactory.model.loader:143 >> all params: 1,543,714,304
    Welcome to the CLI application, use `clear` to remove the history, use `exit` to exit the application.
    User: 你是谁?
    Assistant: 我是Qwen,由阿里云开发的语言模型。我能够回答问题、创作文字,还能表达观点、撰写代码。如果您有任何问题或需要帮助,请随时告诉我,我会尽力提供支持。
    User:
  • web推理: llamafactory-cli webchat examples/inference/qwen2.5.yaml
    效果如图,可通过 http://localhost:7860/ 进行访问
    XoHIMO
    注意:这里的localhost:7860 指的是程序启动机器自身的7860端口,云上的用户可能无法通过本地的笔记本电脑直接访问,需要找云厂商获取域名和端口号的一些配置关系进行配置
    比如阿里云用户需要做一下如下环境变量的配置才能正常运行gradio,然后再通过阿里云给的域名映射访问
    1
    export GRADIO_ROOT_PATH=/${JUPYTER_NAME}/proxy/7860/

构建自定义数据集

  • 自带的identity.json数据集

    系统自带的identity.json数据集,用于对模型自我介绍,示例:
    data/identity.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [
    {
    "instruction": "你好",
    "input": "",
    "output": "您好,我是 {{name}},一个由 {{author}} 开发的 AI 助手,很高兴认识您。请问我能为您做些什么?"
    },
    {
    "instruction": "你好",
    "input": "",
    "output": "您好,我是 {{name}},一个由 {{author}} 打造的人工智能助手,请问有什么可以帮助您的吗?"
    },
    ]
    1
    2
    3
    4
    cd LLaMA-Factory
    # 其中的NAME 和 AUTHOR ,替换成我们需要的内容
    sed -i 's/{{name}}/达优AI助手/g' data/identity.json
    sed -i 's/{{author}}/达科为/g' data/identity.json
  • 企业自己的数据集
    dayou.json
    1
    2
    3
    4
    5
    6
    7
    8
    [
    {
    "instruction": "低内毒素胰酶消化液的推荐储存条件是什么,这些条件如何确保其稳定性和活性?",
    "input": "",
    "output": "低内毒素胰酶消化液的储存条件对其稳定性和活性至关重要。以下是推荐的储存条件及其如何确保稳定性和活性的详细解释:\n\n## 🧪 推荐储存条件\n\n1. **温度:**\n * **长期储存: -20°C 或更低(如 -80°C)。** 这是最核心的条件。\n * **短期储存(几天内使用): 2-8°C (冰箱冷藏)。** 仅适用于已解冻并计划在极短时间内(通常不超过1周)用完的 aliquot(分装样品)。**不推荐**将未开封的浓缩液长期存放在冷藏温度。\n\n2. **分装:**\n * **强烈建议将大瓶装的胰酶消化液分装成小体积(如 0.5 mL, 1 mL, 5 mL 等)的无菌、低蛋白吸附的离心管或冻存管中。** 分装体积应根据单次实验用量来定,避免反复冻融。\n\n3. **容器:**\n * 使用**无菌、低蛋白吸附、低内毒素**的容器。推荐使用**高质量聚丙烯 (PP)** 材质的离心管或冻存管。避免使用可能吸附蛋白或释放塑化剂(如 DEHP)的聚苯乙烯 (PS) 管或低质量塑料管。玻璃容器也可,但需确保无菌且不易碎。\n\n4. **避光:**\n * 储存于**避光**环境中。虽然胰酶对光不特别敏感,但避光储存可防止任何潜在的光氧化反应,并提供额外的保护。\n\n5. **密封:**\n * 确保容器**密封良好**,防止水分蒸发(导致浓度变化)、空气进入(导致氧化或污染)以及微生物污染。\n\n## 📊 这些条件如何确保稳定性和活性?\n\n1. **低温 (-20°C 或更低):**\n * **抑制酶活性:** 胰蛋白酶等蛋白酶在低温下其催化活性被极大抑制。这**显著减缓了胰酶分子的自水解(自我降解)**。自水解是胰酶失活的主要途径之一,即使在溶液中也会缓慢发生。低温将这一过程降至最低。\n * **减缓化学反应:** 低温降低了溶液中所有化学反应的速率,包括可能导致酶变性的氧化反应或脱酰胺反应等。\n * **维持结构稳定性:** 低温有助于维持胰酶分子的天然三维构象,防止因热运动导致的结构松散和失活。\n * **抑制微生物生长:** 低温(尤其是冷冻)能有效抑制细菌、真菌等微生物的生长繁殖。**这是维持“低内毒素”状态的关键。** 即使初始内毒素很低,如果储存过程中微生物滋生,其死亡裂解后会释放大量内毒素,导致内毒素水平飙升,严重影响细胞实验结果(如激活细胞炎症通路)。低温储存从根本上杜绝了这种污染风险。\n\n2. **分装 (Aliquoting):**\n * **避免反复冻融:** 反复冻融是导致胰酶失活的**最主要杀手**。每次冻融循环都会:\n * 产生冰晶,可能直接剪切酶分子,破坏其结构。\n * 导致溶液局部浓度和pH值的剧烈变化(如冰晶形成时溶质浓缩),加速酶变性。\n * 增加溶液与空气(氧气)的接触机会,促进氧化。\n * 增加污染风险(每次操作都引入污染可能)。\n * **分装确保每次只取出一个 aliquot 使用,用完即弃,避免了剩余消化液经历多次冻融循环**,从而最大程度地保护了酶的活性和溶液的稳定性(包括内毒素水平)。\n\n3. **合适的容器 (无菌、低蛋白吸附、低内毒素):**\n * **防止酶损失:** 低蛋白吸附的容器(如高质量PP管)能最大程度减少胰酶分子在管壁上的吸附,确保溶液中活性酶的浓度保持稳定。\n * **防止污染:** 无菌容器杜绝了初始储存时的微生物污染源。低内毒素容器确保储存过程本身不会引入新的内毒素污染。\n * **防止化学干扰:** 高质量的惰性材料不会释放可能影响酶活性或细胞毒性的化学物质(如塑化剂)。\n\n4. **避光:**\n * 虽然不是主要因素,但避光储存提供了额外的保护层,防止任何潜在的光敏性成分(可能存在痕量杂质)发生光化学反应,避免产生可能影响酶活性或细胞反应的副产物。\n\n5. **密封:**\n * **防止浓缩/稀释:** 密封防止水分蒸发,避免溶液浓度升高(导致消化过强)或吸潮导致浓度降低(导致消化不足)。\n * **防止氧化:** 减少溶液与空气(氧气)的接触,降低氧化导致酶失活的风险。\n * **防止污染:** 密封是防止储存过程中微生物(细菌、霉菌)进入的关键物理屏障,**直接保障了低内毒素状态的维持**。\n\n## 📌 总结与关键点\n\n* **核心是低温冷冻 + 分装:** -20°C 或更低温度储存是抑制酶自降解和微生物生长的基础。分装成小 aliquot 是避免反复冻融失活的**绝对必要措施**。\n* **低内毒素依赖于无菌和防污染:** 低温储存抑制微生物生长,无菌容器和密封操作防止初始和储存过程中的污染,共同确保内毒素水平在有效期内维持在极低状态。\n* **容器选择不容忽视:** 低蛋白吸附和低内毒素的容器材质对维持酶浓度和溶液纯净度至关重要。\n* **遵循说明书:** **最重要的一点是:务必遵循您所使用的具体品牌和批次胰酶消化液产品说明书上提供的储存条件和有效期。** 不同厂家、不同配方(如是否含 EDTA、酚红等)的胰酶消化液,其最佳储存条件和稳定期限可能略有差异。说明书是最权威的指导。\n* **解冻与使用:** 使用时,将所需 aliquot 从冰箱取出,**在冰上或 2-8°C 冰箱中缓慢解冻**。避免室温解冻或水浴加速解冻(温度骤变易失活)。解冻后应立即使用,**切勿再次冷冻**已解冻的 aliquot。使用前可短暂涡旋混匀,但避免剧烈振荡产生气泡(可能导致蛋白变性)。\n\n通过严格遵守这些储存条件,可以最大限度地保证低内毒素胰酶消化液的蛋白酶活性、消化效率以及内毒素水平在有效期内保持稳定,从而为细胞培养实验提供可靠、一致且无干扰的消化效果。💪🏻",
    "system": ""
    }
    ]
    需要将该数据集注册到 dataset_info.json
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "identity": {
    "file_name": "identity.json"
    },
    "dayou": {
    "file_name": "dayou.json"
    }
    }

基于LoRA的sft指令微调

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
CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \
--stage sft \
--do_train \
--model_name_or_path /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct \
--dataset identity,dayou \
--dataset_dir ./data \
--template qwen \
--finetuning_type lora \
--output_dir ./saves/Qwen2.5-1.5B-dayou/lora/sft \
--overwrite_cache \
--overwrite_output_dir \
--cutoff_len 1024 \
--preprocessing_num_workers 16 \
--per_device_train_batch_size 2 \
--per_device_eval_batch_size 1 \
--gradient_accumulation_steps 8 \
--lr_scheduler_type cosine \
--logging_steps 50 \
--warmup_steps 20 \
--save_steps 100 \
--eval_steps 50 \
--evaluation_strategy steps \
--load_best_model_at_end \
--learning_rate 5e-5 \
--num_train_epochs 5.0 \
--max_samples 1000 \
--val_size 0.1 \
--plot_loss \
--fp16

动态合并LoRA的推理

当基于LoRA的训练进程结束后,我们如果想做一下动态验证,在网页端里与新模型对话,与步骤 原始模型直接推理 相比,唯一的区别是需要通过adapter_name_or_path参数告诉LoRA的模型位置。
touch examples/inference/qwen2.5_sft.yaml

1
2
3
4
5
model_name_or_path: /root/autodl-tmp/LLaMA-Factory/models/Qwen/Qwen2.5-1.5B-Instruct
adapter_name_or_path: saves/Qwen2.5-1.5B-dayou/lora/sft
template: qwen
infer_backend: huggingface # choices: [huggingface, vllm, sglang]
trust_remote_code: true
  • 终端推理: llamafactory-cli chat examples/inference/qwen2.5_sft.yaml
  • web推理: llamafactory-cli webchat examples/inference/qwen2.5_sft.yaml

相关链接

背景

大模型领域日新月异,模型微调早已无代码化,我认为大模型训练和微调的关键步骤仅剩:高质量的数据集准备
之前微调过一次甄嬛 Mac M2之LLaMA3-8B微调(llama3-fine-tuning), 这次我想用 llama-factory + Qwen2.5-1.5B-Instruct完全无代码微调一次。

LLaMA-Factory

1
2
3
4
5
6
7
8
9
## 克隆项目
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
## 创建环境
conda create -n llamafactory python=3.10
## 激活环境
conda activate llamafactory
## 安装依赖
pip install -e ".[torch,metrics]" --no-build-isolation -i https://mirrors.aliyun.com/pypi/simple

校验环境

1
2
3
4
import torch
torch.cuda.current_device()
torch.cuda.get_device_name(0)
torch.__version__

执行webui

1
llamafactory-cli webui

执行完毕后,会自动打开webui

数据集

本次我们依然使用该数据集:Chat-嬛嬛数据集
llama-factory 目前只支持AlpacaSharegpt两种格式的数据集。

  • Alpaca 格式
    适用于单轮任务,如问答、文本生成、摘要、翻译等。结构简洁,任务导向清晰,适合低成本的指令微调。
    1
    2
    3
    4
    5
    {  
    "instruction":"计算这些物品的总费用。",
    "input":"输入:汽车 - $3000,衣服 - $100,书 - $20。",
    "output":"汽车、衣服和书的总费用为 $3000 + $100 + $20 = $3120。"
    }
  • ShareGPT 格式
    适用于多轮对话、聊天机器人等任务。结构复杂,包含多轮对话上下文,适合高质量的对话生成和人机交互任务。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [{
    "instruction": "今天的天气怎么样?",
    "input": "",
    "output": "今天的天气不错,是晴天。",
    "history": [
    ["今天会下雨吗?", "今天不会下雨,是个好天气。"],
    ["今天适合出去玩吗?", "非常适合,空气质量很好。"]
    ]
    }]

打开 huanhuan.json 文件,可以看到它其实就是 Alpaca 格式的数据集,仅下载这一个文件即可。
w0tYNQ
我们在 llama-factory 项目内的的data目录放置 huanhuan.json 文件,并且在 dataset_info.json 的配置文件添加一条 huanhuan.json 的json配置,这样,我们新添加的数据集才能被llama-factory注册识别到。
KfyAQY

微调

首先是模型,我们选择此次需要微调的Qwen2.5-1.5B-Instruct。

  • Instruct 版本(如 Qwen2.5-1.5B-Instruct) ✅
    • 经过指令微调的模型
    • 更适合直接对话和指令遵循
    • 已经具备基本的对话能力
    • 更适合用来进一步微调

微调方式使用默认的lora即可。

  • 训练轮数可以选择1轮,会快一些(如果后面发现效果不理想,可以多训练几轮),我这里最终选择了3轮,因为我发现仅1轮效果不佳。
  • 计算类型 默认
  • 截断长度设置小一点,为1024(默认是2048)。
  • 梯度累计设置为4。
  • 其他参数设置-预热步数是学习率预热采用的步数,通常设置范围在2-8之间,我们这里配置4。
    grGcU2
  • LoRA参数设置-lora秩越大(可以看作学习的广度),学习的东西越多,微调之后的效果可能会越好,但是也不是越大越好。太大的话容易造成过拟合(书呆子,照本宣科,不知变通),这里设置为8。
  • LoRA参数设置-lora缩放系数(可以看作学习强度),越大效果可能会越好,对于一些用于复杂场景的数据集可以设置更大一些,简单场景的数据集可以稍微小一点。我这里设置256。
    pJJRLv
    模型训练结束后,会显示:训练完毕。
    wFc3ho
    如果想重新微调,记得改一下下面红框中的两个值。
    7Yswha
    微调成功后,在检查点路径这里,下拉可以选择我们刚刚微调好的模型。把窗口切换到chat,点击加载模型。
    8GsA0p
    加载好之后就可以在输入框发送问题,测试微调模型的回复效果了。如果想切换回微调之前的模型,只需要把检查点路径置空。然后在chat这里卸载模型,再加载模型即可。
    SngtWk

导出模型与使用

切换到export,填写导出目录models/huanhuan

大语言模型的参数通常以高精度浮点数(如32位浮点数,FP32)存储,这导致模型推理需要大量计算资源。量化技术通过将高精度数据类型存储的参数转换为低精度数据类型(如8位整数,INT8)存储,可以在不改变模型参数量和架构的前提下加速推理过程。这种方法使得模型的部署更加经济高效,也更具可行性。

量化前需要先将模型导出后再量化。修改模型路径为导出后的模型路径,导出量化等级一般选择 8 或 4,太低模型会答非所问。

wOiMlb

  • 导入ollama

    出现ollama和在llama-factory的chat回答效果不一致,请查看底部 相关链接

    1
    2
    3
    ollama create Qwen2.5-1.5B-huanhuan -f Modefile
    ollama list
    ollama run Qwen2.5-1.5B-huanhuan:latest
    UhlbHS
  • vllm部署使用

    出现vllm和在llama-factory的chat回答效果不一致,请查看底部 相关链接

    • 启动
      1
      2
      3
      4
      5
      vllm serve /root/autodl-tmp/LLaMA-Factory/models/huanhuan/Qwen2.5-1.5B-huanhuan \
      --port 8000 \
      --host 0.0.0.0 \
      --gpu-memory-utilization 0.8 \
      --served-model-name huanhuan
    • 发送请求
      1
      2
      3
      4
      5
      6
      7
      8
      curl --location 'http://192.168.103.43:8000/v1/completions' \
      --header 'Content-Type: application/json' \
      --data '{
      "model": "huanhuan",
      "prompt": "你是谁",
      "max_tokens": 50,
      "temperature": 0.7
      }'
    • 效果
      oKPXPo

相关链接

背景

物理机器有显卡L20 (4 * 48G),安装vmware esxi系统,希望虚拟机ubuntu22.04 直通物理显卡。

  • ESXi-8.0U3e-24677879-standard
  • ubuntu22.04

直通模式配置

虚拟机配置

  • 创建虚拟机及配置参数
    • 预留全部内存
      wUml1N
    • 在“虚拟机设置”中,点击“添加其他设备” > “PCI设备”,在列表中找到并勾选NVIDIA显卡(例如,0000:c1:00.0)。
      PZmAZx
    • 取消UEFI安全启动
      jQILo6

为了优化GPU直通的性能和兼容性,需要在虚拟机的高级配置中添加以下参数:

1
2
3
4
5
6
# 该参数用于隐藏虚拟化环境,使得虚拟机能够更好地识别并利用NVIDIA显卡。
hypervisor.cpuid.v0 = FALSE
# 启用64位内存映射输入/输出(MMIO),提高显卡的内存访问效率。
pciPassthru.use64bitMMIO = TRUE
# 设置64位MMIO的大小,确保显卡有足够的内存资源进行高性能计算和渲染任务。
pciPassthru.64bitMMIOSizeGB = 256 (2张48G显卡,具体计算看下面)

pciPassthru.64bitMMIOSizeGB计算方式 https://earlruby.org/tag/use64bitmmio/
64bitMMIOSizeGB值的计算方法是将连接到VM的所有GPU上的显存总量(GB)相加。如果总显存为2的幂次方,则将pciPassthru.64bitMMIOSizeGB设置为下一个2的幂次方即可。
如果总显存介于2的2次方之间,则向上舍入到下一个2的幂次方,然后再次向上舍入。
2的幂数是2、4、8、16、32、64、128、256、512、1024…
例如虚拟机直通两张24G显存的显卡,则64bitMMIOSizeGB应设置为128。计算方式为242=48,在32和64之间,先舍入到64,再次舍入到128
例如虚拟机直通两张48G显存的显卡,则64bitMMIOSizeGB应设置为256。计算方式为48
2=96,在64和128之间,先舍入到128,再次舍入到256

BCylVm

linux 驱动安装

  • 更新系统
    1
    sudo apt update && sudo apt upgrade -y
  • 禁用开源驱动 Nouveau
    1
    2
    3
    4
    sudo nano /etc/modprobe.d/blacklist.conf
    # 在文件末尾添加以下内容:
    blacklist nouveau
    options nouveau modeset=0
    保存后执行:
    1
    2
    sudo update-initramfs -u
    sudo reboot
    重启后验证是否禁用成功(无输出即成功):
    1
    lsmod | grep nouveau
  • 安装驱动
    • 安装显卡驱动工具
      1
      sudo apt-get install nvidia-cuda-toolkit
    • 查看系统推荐显卡驱动

      这里要看好哪个是系统推荐的驱动(recommend),并且要记下来

      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
      sudo ubuntu-drivers devices

      (base) root@ai-03-deploy:~# sudo ubuntu-drivers devices
      == /sys/devices/pci0000:00/0000:00:0f.0 ==
      modalias : pci:v000015ADd00000405sv000015ADsd00000405bc03sc00i00
      vendor : VMware
      model : SVGA II Adapter
      manual_install: True
      driver : open-vm-tools-desktop - distro free

      == /sys/devices/pci0000:03/0000:03:00.0 ==
      modalias : pci:v000010DEd000026BAsv000010DEsd00001957bc03sc02i00
      vendor : NVIDIA Corporation
      driver : nvidia-driver-535 - distro non-free
      driver : nvidia-driver-575-server-open - distro non-free
      driver : nvidia-driver-550-open - distro non-free
      driver : nvidia-driver-575-open - distro non-free
      driver : nvidia-driver-570 - distro non-free
      driver : nvidia-driver-550 - distro non-free
      driver : nvidia-driver-570-server-open - distro non-free
      driver : nvidia-driver-570-open - distro non-free
      driver : nvidia-driver-575 - distro non-free recommended
      driver : nvidia-driver-575-server - distro non-free
      driver : nvidia-driver-535-open - distro non-free
      driver : nvidia-driver-535-server-open - distro non-free
      driver : nvidia-driver-570-server - distro non-free
      driver : nvidia-driver-535-server - distro non-free
      driver : xserver-xorg-video-nouveau - distro free builtin
    • 添加驱动源
      1
      2
      sudo add-apt-repository ppa:graphics-drivers/ppa
      sudo apt-get update
    • 选择版本安装
      1
      sudo apt install nvidia-driver-575
      1
      sudo reboot
  • 验证安装
    1
    2
    nvidia-smi
    nvitop

参考链接

从坑中爬起:ESXi 8.0直通NVIDIA显卡的血泪经验
How To Install Nvidia Drivers on Rocky Linux 10
vmware虚拟机玩GPU显卡直通

0%