LangGraph 07:接入真实 LLM——OpenAI 与本地/开源模型工程实践

前面的章节中,为了理清图拓扑、状态流动和分支机制,我们多次用简单的字符串拼接或占位函数模拟了 LLM 的返回结果。在真实的 Agent 系统中,节点的核心工作通常就是与模型交互。

在 LangGraph 中接入模型并不神秘:节点本身就是一个普通的 Python 函数,任何可以通过 Python SDK 或 HTTP 调用的模型,都能放在节点里执行。但要把商业 API(如 OpenAI)和本地/开源模型(如 Mistral、Qwen、Llama)稳定可靠地接进图结构,仍有一些不可忽视的工程细节:连接池生命周期管理、结构化输出解析、不同开源模型的 Prompt 模板对齐,以及网络抖动时的节点级容错。


环境准备与依赖安装

主流模型接入推荐安装对应的 LangChain 官方生态扩展包:

1
2
3
4
5
6
7
8
# OpenAI 驱动包
pip install langchain-openai

# HuggingFace 与开源模型生态驱动
pip install langchain-huggingface huggingface_hub

# LangGraph 核心与基础类型定义
pip install langgraph langchain-core python-dotenv

敏感凭证推荐配置在项目根目录的 .env 文件中,通过 python-dotenv 统一加载:

1
2
3
4
5
6
7
8
import os
from dotenv import load_dotenv

load_dotenv()

# 读取环境变量
openai_api_key = os.getenv("OPENAI_API_KEY")
hf_token = os.getenv("HUGGINGFACEHUB_API_TOKEN")

核心设计法则:LLM 实例必须在节点外部初始化

在编写节点函数时,常见的新手误区是在节点函数内部初始化客户端:

1
2
3
4
5
# 错误做法:在节点内部每次实例化
def bad_summary_node(state: dict) -> dict:
llm = ChatOpenAI(model="gpt-4o-mini") # 每次节点触发都重新创建连接、读取配置
res = llm.invoke(...)
return {"summary": res.content}

这种写法会导致每次节点被调度时都重新建立底层 HTTP 客户端,无法复用 HTTP 连接池(Connection Pool)和 Keep-Alive,同时还会增加内存抖动与鉴权开销。

标准模式:在模块全局或图的组装工厂函数外部完成模型实例初始化,节点函数直接通过闭包引用该实例:

1
2
3
4
5
6
7
8
9
10
11
# 正确做法:单例外部初始化,节点内部纯粹调用
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
timeout=30.0,
max_retries=2,
)

def good_summary_node(state: dict) -> dict:
res = llm.invoke(...)
return {"summary": res.content}

商业 API 方案:接入 OpenAI

1. 基础对话与 Prompt 模板管理

使用 ChatPromptTemplate 组织结构化的 System/Human 消息,保持业务逻辑与提示词解耦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一名资深的技术架构师,擅长用简练的语言做技术提炼。"),
("human", "请根据以下背景信息提炼 3 个关键设计考量:\n\n{context}"),
])

# 利用 LCEL 管道组合
summary_chain = prompt_template | llm

def analyze_architecture_node(state: dict) -> dict:
context = state.get("raw_text", "")
response = summary_chain.invoke({"context": context})
return {"key_considerations": response.content}

2. 结构化输出(Structured Output)驱动状态更新

如果节点需要返回结构化字段(如评分、状态码、布尔决策),直接要求模型返回 JSON 并用正则解析很容易遇到格式错误。推荐使用 LangChain 的 with_structured_output 绑定 Pydantic 模型:

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
from typing import List
from pydantic import BaseModel, Field


class CodeReviewResult(BaseModel):
score: int = Field(description="代码质量评分,范围 1 到 10")
passed: bool = Field(description="是否达到合并标准")
identified_issues: List[str] = Field(description="发现的潜在缺陷或规范问题列表")
optimization_suggestions: List[str] = Field(description="重构优化建议")


structured_llm = llm.with_structured_output(CodeReviewResult)

def code_review_node(state: dict) -> dict:
code_snippet = state.get("source_code", "")
prompt = f"请严格审查以下 Python 代码并输出审查结果:\n\n```python\n{code_snippet}\n```"

# 直接返回强类型的 Pydantic 对象
review: CodeReviewResult = structured_llm.invoke(prompt)

return {
"review_score": review.score,
"is_review_passed": review.passed,
"review_issues": review.identified_issues,
"review_suggestions": review.optimization_suggestions,
}

返回的字典与 StateGraph 的 TypedDict 完美契合,省去了编写防御性解析代码的成本。


开源与本地方案:接入 HuggingFace / 本地推理引擎

对于需要私有化部署、离线运行或降低成本的场景,开源模型是重要选择。

1. 使用 HuggingFace Serverless Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_huggingface import HuggingFaceEndpoint

hf_llm = HuggingFaceEndpoint(
repo_id="mistralai/Mistral-7B-Instruct-v0.2",
task="text-generation",
max_new_tokens=512,
temperature=0.1,
do_sample=False,
)

def hf_generate_node(state: dict) -> dict:
query = state["query"]
# Mistral 特定的提示词指令格式
prompt = f"[INST] 请根据用户问题给出技术解答:{query} [/INST]"

response = hf_llm.invoke(prompt)
return {"answer": response}

2. 多模型 Prompt 格式的适配考量

不同开源模型在微调时使用了不同的特殊标记(Special Tokens)。如果手动拼接字符串,极容易出现格式错位:

  • Mistral / Mixtral:[INST] 用户指令 [/INST]
  • Llama 3:<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n指令<|eot_id|><|start_header_id|>assistant<|end_header_id|>
  • ChatML 规范(Qwen、Yi 等):<|im_start|>user\n指令<|im_end|>\n<|im_start|>assistant\n

为了不把各模型的底层标记硬编码进节点,建议使用 ChatHuggingFace。它会自动读取模型的分词器(Tokenizer)配置并统一消息协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from langchain_core.messages import HumanMessage, SystemMessage

endpoint = HuggingFaceEndpoint(
repo_id="mistralai/Mistral-7B-Instruct-v0.2",
task="text-generation",
max_new_tokens=512,
)

# 自动处理模板格式映射
chat_hf_model = ChatHuggingFace(llm=endpoint)

def uniform_chat_node(state: dict) -> dict:
messages = [
SystemMessage(content="你是一个专业的运维排错专家。"),
HumanMessage(content=state["error_log"]),
]
response = chat_hf_model.invoke(messages)
return {"diagnostic_report": response.content}

如果是本地利用 vLLM 或 Ollama 搭建的推理服务,它们均兼容 OpenAI API 规范,只需在 ChatOpenAI 中指定 base_url 即可:

1
2
3
4
5
6
7
# 接入本地部署的 Ollama / vLLM
local_llm = ChatOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # 本地无需鉴权但不可为空
model="qwen2.5:7b",
temperature=0.1,
)

完整实战:基于真实 LLM 的多步内容生成图

我们将关键词提取、大纲生成和文章写作三个步骤,串联成一个具备完整调用的 StateGraph。

完整运行代码

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
import os
from typing import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, END


# 1. 状态定义
class ContentPipeline(TypedDict):
topic: str
keywords: str
outline: str
article: str


# 2. 全局初始化模型
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.3,
api_key=os.getenv("OPENAI_API_KEY"),
)


# 3. 节点实现
def extract_keywords_node(state: ContentPipeline) -> dict:
topic = state["topic"]
resp = model.invoke([
SystemMessage(content="你是一个 SEO 与关键词提取助手。请仅输出关键词,用逗号分隔。"),
HumanMessage(content=f"提取主题【{topic}】的 5 个技术核心关键词。"),
])
return {"keywords": resp.content.strip()}


def create_outline_node(state: ContentPipeline) -> dict:
topic = state["topic"]
keywords = state["keywords"]
resp = model.invoke([
SystemMessage(content="你是一名资深技术主编,擅长设计逻辑递进的技术文章大纲。"),
HumanMessage(
content=f"基于主题【{topic}】和关键词【{keywords}】,规划 3 个核心章节大纲,包含二级小节标题。"
),
])
return {"outline": resp.content.strip()}


def write_article_node(state: ContentPipeline) -> dict:
topic = state["topic"]
outline = state["outline"]
resp = model.invoke([
SystemMessage(content="你是一名工程博主,行文偏重实际应用和工程落地,语言精炼。"),
HumanMessage(
content=f"主题:{topic}\n\n大纲如下:\n{outline}\n\n请根据大纲撰写一篇 800 字左右的技术短文。"
),
])
return {"article": resp.content.strip()}


# 4. 组装与编译工作流图
workflow = StateGraph(ContentPipeline)
workflow.add_node("extract_keywords", extract_keywords_node)
workflow.add_node("create_outline", create_outline_node)
workflow.add_node("write_article", write_article_node)

workflow.set_entry_point("extract_keywords")
workflow.add_edge("extract_keywords", "create_outline")
workflow.add_edge("create_outline", "write_article")
workflow.add_edge("write_article", END)

app = workflow.compile()

# 5. 执行调用
if __name__ == "__main__":
inputs = {
"topic": "LangGraph 状态机的错误恢复机制",
"keywords": "",
"outline": "",
"article": "",
}

final_output = app.invoke(inputs)
print("========== 提取的关键词 ==========")
print(final_output["keywords"])
print("\n========== 生成的结构大纲 ==========")
print(final_output["outline"])
print("\n========== 正文产出 ==========")
print(final_output["article"])

生产级防线:节点级重试与错误熔断

大模型在实际生产中可能遭遇多种偶发错误:网络超时、API 限流(HTTP 429 Rate Limit)或偶尔的输出解析失败。如果在节点内部没有捕获,整个图的执行过程就会中断崩溃。

虽然 LangGraph 提供了 Checkpoint 机制支持断点恢复,但在单节点内部增加指数退避重试(Exponential Backoff)与故障降级标记,是保证系统健壮性的第一道防线:

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
import time
from typing import Optional


def resilient_llm_call_node(state: dict) -> dict:
"""具备重试容错与优雅降级的节点"""
prompt = state.get("query_prompt", "")
max_retries = 3
base_delay = 1.0 # 秒

last_error: Optional[Exception] = None
for attempt in range(max_retries):
try:
response = model.invoke([HumanMessage(content=prompt)])
return {
"generated_result": response.content,
"is_error": False,
"error_message": "",
}
except Exception as e:
last_error = e
# 指数退避:1s, 2s, 4s...
wait_time = base_delay * (2 ** attempt)
print(f"[警告] 模型调用异常 (尝试 {attempt + 1}/{max_retries}): {e},{wait_time} 秒后重试...")
time.sleep(wait_time)

# 重试全部耗尽后的优雅降级,不抛出致命异常中断整张图
return {
"generated_result": "生成失败:服务暂不可用",
"is_error": True,
"error_message": str(last_error),
}

这样下游的条件边路由器就可以检查 is_error 字段,决定是将任务切入备用兜底节点(Fallback Node),还是向管理端发出报警。


方案选型与演进路线

在工程选型中,可以根据业务阶段选择合适的模型调用方案:

评估维度 商业 API(OpenAI / Claude / DeepSeek) 开源托管 API(HF Endpoint / 硅基流动等) 本地私有化部署(vLLM / Ollama)
推理能力与遵循度 极高,复杂逻辑与结构化输出极其稳定 取决于底层开源模型规模 视显存算力而定,7B/14B 适合垂类聚焦任务
部署与运维成本 零基础设施负担,按 Token 计量 零运维,按需开销或调用计费 需维护 GPU 服务器与推理加速引擎
数据安全性与合规 数据需通过公网出境/经过服务商 数据经过托管平台 数据 100% 留存内网,满足严苛保密要求
首字时延与并发吞吐 受云端配额与公网延迟影响 免费层较慢,付费实例较快 局域网低延迟,吞吐完全由硬件决定
适用开发阶段 快速 PoC 验证、生产高要求业务 快速验证开源模型效果、中等成本场景 本地调试开发、内网安全敏感型企业落地

LangGraph 基础系列总结

通过这 7 篇的系统拆解,我们完成了对 LangGraph 基础核心模式的全面覆盖:

  1. 架构范式转变:跳出线性链式的僵硬结构,拥抱有向图与状态机范式(01 为什么不用链式调用)。
  2. 三件套基础:深入 TypedDict 共享状态、纯函数节点以及 Reducer 合并机制(02 State-Node-Graph三件套)。
  3. 顺序管线设计:防御性校验、状态更新契约与 stream 流式调试(03 顺序图与第一个Workflow)。
  4. 条件分支与循环:基于 Pydantic 结构化决策的动态路由与自旋重试拓扑(04 条件分支与动态路由)。
  5. 并发执行模式:Fan-out 分发、并发安全与 Fan-in 聚合规约(05 并行执行Fan-out与Fan-in)。
  6. 分步质检流水线:Prompt Chaining 模式、阶段性质量门禁与增量反馈(06 Prompt Chaining分步生成)。
  7. 真实模型集成:连接池管理、结构化数据绑定与节点容错兜底(本篇)。

掌握了这套状态图的编排思维,后续无论是在多 Agent 协同体系中划分决策边界(如我们在后续案例中深入讨论的“收紧 LLM 的决策权”),还是实现生产级持久化断点恢复,都会拥有清晰的架构控制力。


系列导航与参考