LLM(Large Language Model,大语言模型)本身可以完成文本生成、问答、翻译、总结等任务,但真正把它接入业务系统时,开发者通常会遇到一组更工程化的问题:
- 不同模型供应商的调用方式不一样,切换模型要改适配代码。
- 提示词需要模板化,不能每次手写一大段字符串。
- 模型返回的是自然语言,业务系统更希望拿到 JSON(JavaScript Object Notation)或对象。
- 多轮对话需要保存历史上下文,否则每一轮问答都是孤立的。
- 模型需要查询数据库、调用接口、搜索网页、执行计算,单纯聊天不够用。
- 企业知识库不在模型训练数据里,需要把外部文档接进来做问答。
- 应用上线后需要观测调用链路、调试提示词、评估输出质量。
LangChain 就是围绕这些问题设计的大模型应用开发框架。它把提示词、模型、解析器、链、记忆、工具、智能体、检索等能力拆成标准组件,让开发者像组装流水线一样构建复杂的 AI(Artificial Intelligence,人工智能)应用。
一个最小的 LangChain 应用通常长这样:
flowchart LR
A[用户输入] --> B[Prompt 模板]
B --> C[大语言模型]
C --> D[输出解析器]
D --> E[业务系统可用的结果]
当应用变复杂后,LangChain 可以继续接入记忆、工具、检索和智能体:
flowchart TD
U[用户] --> APP[LangChain 应用]
APP --> P[Prompt 模板]
APP --> M[Memory 记忆]
APP --> R[Retrieval 检索]
APP --> T[Tools 工具]
APP --> A[Agent 智能体]
R --> V[(向量数据库)]
T --> API[外部 API / 数据库 / 搜索引擎]
A --> T
P --> LLM[LLM / Chat Model]
M --> P
R --> P
LLM --> O[Output Parser]
O --> U
LangChain 的价值不在于“替你调用一次模型”,而在于把大模型应用里重复出现的工程环节标准化。直接调用模型 API(Application Programming Interface,应用程序编程接口)当然也可以,但当业务需要 RAG(Retrieval-Augmented Generation,检索增强生成)、多轮对话、工具调用、多模型切换和链路调试时,框架化的收益会明显一些。
| 对比维度 | 直接调用模型 API | 使用 LangChain |
|---|---|---|
| 模型切换 | 每家模型都要写适配代码 | 使用统一接口封装不同模型 |
| 提示词管理 | 手写字符串,容易混乱 | PromptTemplate / ChatPromptTemplate 模板化 |
| 输出处理 | 手动解析文本 | Output Parser 转成字符串、JSON、列表、对象 |
| 多步骤任务 | 自己串联函数调用 | LCEL 管道式编排 |
| 多轮对话 | 自己保存上下文 | Memory / Chat History 组件处理 |
| 外部知识库 | 自己实现加载、切分、向量化、检索 | Retrieval 组件提供完整流程 |
| 工具调用 | 自己设计调用协议 | Tools + Agent 统一封装 |
| 调试观测 | 自己打日志 | LangSmith / Callbacks 支持链路追踪 |
LangChain 生态的几个核心模块
LangChain 早期只是一个 Python 软件包,现在已经拆成一套生态。理解这些模块的边界,后面写代码时会更清楚。
flowchart TB
subgraph Core[LangChain 基础层]
LC[langchain-core<br/>基础抽象、Runnable、LCEL]
COM[langchain-community<br/>第三方模型、工具、加载器集成]
LCP[langchain<br/>Chains、Agents、Retrieval 等高层能力]
end
subgraph Graph[复杂智能体编排]
LG[LangGraph<br/>状态图、条件分支、循环、多智能体]
end
subgraph Observe[开发与生产观测]
LS[LangSmith<br/>调试、评估、监控、链路追踪]
end
subgraph Deploy[服务化部署]
LSV[LangServe<br/>把 Chain / Agent 暴露为 REST API]
end
LC --> LCP
COM --> LCP
LCP --> LG
LCP --> LS
LCP --> LSV
几个常见包的职责如下:
| 模块 | 作用 |
|---|---|
langchain-core | 定义基础接口,例如 Runnable、Message、Prompt、Output Parser |
langchain-community | 放置社区集成,例如第三方模型、工具、文档加载器、向量库连接器 |
langchain | 提供 Chains、Agents、Retrieval 等上层能力 |
langchain-openai | OpenAI 兼容模型的封装 |
langchain-ollama | Ollama 本地模型和嵌入模型封装 |
langchain-chroma | Chroma 向量数据库集成 |
langgraph | 用图结构构建复杂 Agent 工作流 |
langsmith | 调试、测试、评估和监控大模型应用 |
langserve | 基于 FastAPI 将 LangChain 应用发布为 REST API(Representational State Transfer Application Programming Interface,表述性状态转移应用接口) |
环境准备与最小调用示例
LangChain 的 Python 版本使用较多,推荐 Python 3.10 及以上。
常用安装命令:
pip install -U langchain langchain-core langchain-community
pip install -U langchain-openai
如果需要本地 Ollama 嵌入模型、Chroma 向量数据库和文本切分器,可以继续安装:
pip install -U langchain-ollama langchain-chroma langchain-text-splitters
一个 OpenAI 兼容接口的聊天模型可以这样初始化。很多模型服务都兼容 OpenAI 风格接口,只要配置 base_url 和 api_key 即可。
from langchain_openai import ChatOpenAI
chat_model = ChatOpenAI(
model="deepseek-chat", # 模型名称,按实际服务填写
base_url="https://api.example.com/v1",
api_key="YOUR_API_KEY",
temperature=0.7 # 数值越高,输出越发散;越低,输出越稳定
)
response = chat_model.invoke("用一句话解释什么是 LangChain")
print(response.content)
invoke 是 LangChain Runnable 标准接口的一部分。模型、提示词、解析器、检索器、很多 Chain 都实现了 Runnable,所以它们可以用统一方式组合和调用。
Model I/O:把模型输入输出标准化
Model I/O 是 LangChain 中应用与模型交互的基础层。可以把它类比为 JDBC(Java Database Connectivity,Java 数据库连接)和数据库之间的关系:业务代码不应该关心底层数据库驱动的细节,而应该通过统一接口读写数据;大模型应用也不应该到处散落不同供应商的调用细节,而应该通过标准接口构造输入、调用模型、解析输出。
Model I/O 主要分三段:
flowchart LR
A[Format<br/>Prompt / Message] --> B[Predict<br/>LLM / Chat Model]
B --> C[Parse<br/>Output Parser]
| 阶段 | LangChain 组件 | 解决的问题 |
|---|---|---|
| Format | PromptTemplate、ChatPromptTemplate、Message | 把用户输入格式化为模型能理解的提示 |
| Predict | LLM、Chat Model、Embedding Model | 调用文本模型、对话模型或嵌入模型 |
| Parse | StrOutputParser、JsonOutputParser 等 | 把模型输出转成业务系统可处理的结构 |
模型类型:LLM、Chat Model、Embedding Model
LangChain 里常见模型分三类。
LLM:普通文本生成模型
LLM 输入通常是字符串,输出也是字符串或文本结果,适合一次性文本生成任务,例如改写、翻译、摘要。
from langchain_openai import OpenAI
llm = OpenAI(
model="gpt-3.5-turbo-instruct",
api_key="YOUR_API_KEY"
)
result = llm.invoke("什么是 LangChain?")
print(result)
Chat Model:对话模型
Chat Model 面向多轮对话,输入和输出不是简单字符串,而是带角色的消息对象。现在大多数主流大模型应用都使用对话模型接口。
from langchain_openai import ChatOpenAI
chat_model = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.example.com/v1",
api_key="YOUR_API_KEY"
)
response = chat_model.invoke("用一句话解释反洗钱")
print(response.content)
Embedding Model:嵌入模型
Embedding Model 不负责生成答案,而是把文本转换为向量。语义相近的文本,在向量空间里的距离也更近。RAG、语义搜索、推荐系统都会用到嵌入模型。
from langchain_ollama import OllamaEmbeddings
embeddings_model = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://127.0.0.1:11434"
)
vector = embeddings_model.embed_query("hello world")
print(f"向量维度: {len(vector)}")
print(f"前 10 个值: {vector[:10]}")
嵌入模型输出的是浮点数组,例如:
[0.0123, -0.0456, 0.0871, ...]
这些数值本身没有人工可读含义,但可以用余弦相似度、欧氏距离、点积等算法比较语义相关性。
Message:对话模型里的消息格式
对话模型通常不是只看一段字符串,而是看一组带角色的消息。LangChain 提供了统一消息类型,方便在不同模型间切换。
| 消息类型 | 角色 | 用途 |
|---|---|---|
SystemMessage | 系统 | 设定模型角色、规则、输出约束 |
HumanMessage | 用户 | 用户问题或指令 |
AIMessage | 模型 | 模型返回内容 |
ToolMessage / FunctionMessage | 工具 | 工具或函数执行结果,常用于 Agent |
示例:
from langchain_core.messages import SystemMessage, HumanMessage
messages = [
SystemMessage(content="你是反洗钱领域的专家,回答要准确、简洁。"),
HumanMessage(content="什么是反洗钱?")
]
response = chat_model.invoke(messages)
print(type(response))
print(response.content)
对话模型收到的不是“裸问题”,而是完整上下文:
sequenceDiagram
participant App as 应用
participant Model as Chat Model
App->>Model: SystemMessage:你是反洗钱专家
App->>Model: HumanMessage:什么是反洗钱?
Model-->>App: AIMessage:反洗钱是……
Prompt Template:让提示词可复用
提示词不是越长越好,关键是要稳定表达任务目标、角色、约束、输入变量和输出格式。如果把提示词直接写死在代码里,后续维护会很痛苦。Prompt Template 的作用就是把固定部分和变量部分分开。
PromptTemplate:生成普通字符串提示
PromptTemplate 适合普通 LLM,也可以传给 Chat Model。
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template(
"请用一句话解释什么是 {concept},要求适合初学者理解。"
)
formatted = prompt.invoke({"concept": "LangChain"})
response = chat_model.invoke(formatted)
print(response.content)
ChatPromptTemplate:生成带角色的消息列表
对话模型更推荐使用 ChatPromptTemplate,因为它可以明确系统消息、用户消息和模型消息。
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个 {domain} 领域专家,回答要准确、结构清晰。"),
("human", "请解释:{question}")
])
messages = prompt.invoke({
"domain": "反洗钱",
"question": "什么是 EDD?"
})
response = chat_model.invoke(messages)
print(response.content)
FewShotPromptTemplate:用少量样例约束输出风格
Few-shot prompting 的做法是给模型几个输入输出样例,让模型模仿格式和风格。
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
examples = [
{
"question": "什么是 Spring Boot?",
"answer": "Spring Boot 是一个基于 Spring 的 Java 框架,用于简化应用创建和部署。"
},
{
"question": "什么是依赖注入?",
"answer": "依赖注入是一种由外部容器提供对象依赖的设计方式。"
}
]
example_prompt = PromptTemplate(
input_variables=["question", "answer"],
template="问题:{question}\n答案:{answer}"
)
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
suffix="问题:{question}\n答案:",
input_variables=["question"]
)
prompt_text = few_shot_prompt.format(question="什么是 LangChain?")
response = chat_model.invoke(prompt_text)
print(response.content)
Output Parser:把模型输出变成结构化数据
模型默认返回自然语言字符串,而业务系统常常需要结构化数据。例如风控系统需要拿到字段:
{
"risk_level": "high",
"reason": "命中高风险地区"
}
Output Parser 可以把模型输出解析成字符串、JSON、列表、日期、XML(Extensible Markup Language,可扩展标记语言)等格式。
一个 JSON 解析示例:
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
json_parser = JsonOutputParser()
prompt = PromptTemplate(
template=(
"请用一句话解释什么是 {name}。"
"必须按以下格式输出:\n{format_instructions}"
),
input_variables=["name"],
partial_variables={
"format_instructions": json_parser.get_format_instructions()
}
)
messages = prompt.invoke({"name": "反洗钱"})
response = chat_model.invoke(messages)
data = json_parser.parse(response.content)
print(data)
print(type(data))
更常用的组合方式是 LCEL 管道:
chain = prompt | chat_model | json_parser
data = chain.invoke({"name": "反洗钱"})
print(data)
Runnable 调用方式:invoke、stream、batch
Runnable 是 LangChain 组件统一调用协议。常见方法有三类:
| 方法 | 作用 | 适合场景 |
|---|---|---|
invoke | 单次输入,等待完整结果返回 | 普通问答、后台任务 |
stream | 流式返回 token 或消息片段 | 聊天界面、实时输出 |
batch | 批量处理多个输入 | 批量摘要、批量分类 |
单次调用:
response = chat_model.invoke("什么是风控?")
print(response.content)
流式调用:
streaming_model = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.example.com/v1",
api_key="YOUR_API_KEY",
streaming=True
)
for chunk in streaming_model.stream("详细解释什么是反洗钱"):
print(chunk.content, end="", flush=True)
批量调用:
from langchain_core.messages import HumanMessage
inputs = [
[HumanMessage(content="什么是风控?")],
[HumanMessage(content="什么是反洗钱?")],
[HumanMessage(content="什么是 EDD?")]
]
responses = chat_model.batch(inputs)
for item in responses:
print(item.content)
Runnable 也有异步方法,例如 ainvoke、astream、abatch,适合并发调用或异步 Web 服务。
Chains:把多个组件串成工作流
Chain 的核心思想很简单:上一个组件的输出作为下一个组件的输入,多个步骤组成一个可执行流程。
flowchart LR
A[输入变量] --> B[Prompt Template]
B --> C[Chat Model]
C --> D[Output Parser]
D --> E[最终结果]
以前 LangChain 常用 LLMChain、SequentialChain 这类封装。它们能用,但在较新的版本里已经不再是推荐方式。更推荐使用 LCEL(LangChain Expression Language,LangChain 表达式语言)。
LCEL:用管道符组合组件
LCEL 的写法类似 Linux 管道:
chain = prompt | chat_model | parser
完整示例:
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
parser = JsonOutputParser()
prompt = PromptTemplate(
template=(
"请用一句话解释 {concept},"
"并按格式输出:{format_instructions}"
),
input_variables=["concept"],
partial_variables={
"format_instructions": parser.get_format_instructions()
}
)
chain = prompt | chat_model | parser
result = chain.invoke({"concept": "LangChain"})
print(result)
LCEL 的优势是组合方式统一,组件可以替换。例如把 JSON 解析器换成字符串解析器:
from langchain_core.output_parsers import StrOutputParser
chain = prompt | chat_model | StrOutputParser()
多步骤 Chain:先生成,再压缩,再翻译
可以把一个 Chain 的输出继续送到另一个 Chain。
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
explain_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个技术讲师。"),
("human", "请详细解释:{topic}")
])
summary_prompt = ChatPromptTemplate.from_messages([
("system", "你擅长压缩长文本。"),
("human", "请把以下内容压缩成 3 条要点:\n{text}")
])
parser = StrOutputParser()
explain_chain = explain_prompt | chat_model | parser
summary_chain = summary_prompt | chat_model | parser
full_chain = {
"text": explain_chain
} | summary_chain
result = full_chain.invoke({"topic": "RAG 的工作原理"})
print(result)
这类写法适合固定流程,例如:
- 生成初稿 → 改写 → 结构化输出
- 查询知识库 → 组织上下文 → 回答问题
- 提取实体 → 查询数据库 → 生成解释
如果流程里有条件分支、循环、失败重试、多智能体协作,LangGraph 会比普通 Chain 更合适。
Memory:让应用拥有对话上下文
大模型本身不会“记住”用户上一次说了什么。多轮对话的本质是:应用把历史消息保存起来,并在下一轮请求时连同当前问题一起发给模型。
Memory 的工作流程如下:
sequenceDiagram
participant User as 用户
participant App as LangChain 应用
participant Memory as Memory
participant Model as 大模型
User->>App: 当前问题
App->>Memory: 读取历史对话
Memory-->>App: 返回历史消息
App->>Model: 历史消息 + 当前问题
Model-->>App: 生成回答
App->>Memory: 保存当前问题和回答
App-->>User: 返回回答
使用 ChatMessageHistory 保存消息
最基础的做法是直接管理消息列表。
from langchain_core.chat_history import InMemoryChatMessageHistory
history = InMemoryChatMessageHistory()
history.add_user_message("你好,我叫 RiskHelper")
history.add_ai_message("你好,我是一个 AI 助手")
history.add_user_message("我叫什么?")
response = chat_model.invoke(history.messages)
print(response.content)
这种方式轻量,但只存在内存中,程序重启就会丢失。生产环境通常会把历史消息存到 Redis、数据库或对象存储中。
RunnableWithMessageHistory:更适合新项目的对话记忆
在 LCEL 体系里,可以用 RunnableWithMessageHistory 给 Chain 加上会话历史。
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个回答简洁的技术助手。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
chain = prompt | chat_model
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
response1 = chain_with_history.invoke(
{"input": "用一句话解释 LangChain"},
config={"configurable": {"session_id": "user-001"}}
)
response2 = chain_with_history.invoke(
{"input": "它主要解决什么问题?"},
config={"configurable": {"session_id": "user-001"}}
)
print(response1.content)
print(response2.content)
同一个 session_id 对应同一段会话历史。用户第二次追问时,模型能看到前一轮上下文。
常见 Memory 类型对比
LangChain 早期提供过多种 Memory 组件,理解它们的策略有助于设计自己的上下文管理方案。
| Memory 类型 | 保存方式 | 优点 | 代价 | 适合场景 |
|---|---|---|---|---|
ConversationBufferMemory | 保存完整历史 | 上下文最完整 | Token 消耗持续增长 | 短对话、需要完整上下文 |
ConversationBufferWindowMemory | 只保留最近 K 轮 | 控制上下文长度 | 早期信息会丢失 | 客服、轻量多轮问答 |
ConversationSummaryMemory | 用模型总结历史 | 适合长对话 | 摘要可能丢细节,还会额外调用模型 | 长期对话、只需保留主线 |
ConversationSummaryBufferMemory | 摘要 + 最近原文 | 平衡细节和长度 | 实现更复杂,需要 token 统计 | 长对话助手 |
ConversationEntityMemory | 抽取实体和属性 | 能结构化记住人名、地点、产品等 | 依赖抽取质量 | 用户画像、CRM 场景 |
VectorStoreRetrieverMemory | 历史消息向量化检索 | 可从大量历史里召回相关片段 | 需要向量库和嵌入模型 | 长期个性化助手 |
一个关键限制是上下文窗口。无论使用哪种记忆策略,最终都要把内容塞进模型输入里。历史消息越多,可用于当前问题推理的空间就越少。长对话系统通常会组合使用:
- 最近几轮原始消息,用来保留细节;
- 历史摘要,用来保留主线;
- 向量检索,用来召回相关旧信息;
- 结构化状态,用来保存用户偏好、任务进度等稳定信息。
部分模型封装没有实现 token 统计方法,使用依赖 token 统计的 Memory 时可能报错。解决思路是自己实现 get_num_tokens_from_messages,或者换用支持 token 统计的模型封装。
from typing import List
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage
class CustomChatOpenAI(ChatOpenAI):
def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int:
# 生产环境应接入真实 tokenizer;这里只是示例
return sum(len(m.content) for m in messages if isinstance(m.content, str))
chat_model = CustomChatOpenAI(
model="deepseek-chat",
base_url="https://api.example.com/v1",
api_key="YOUR_API_KEY"
)
Tools:让模型调用外部能力
LLM 擅长语言理解和推理,但它不能直接访问数据库、获取实时股价、读取内部系统,也不应该靠“猜”来完成计算。Tool 的作用是把外部能力封装成模型可调用的函数。
flowchart LR
U[用户问题] --> A[Agent / 模型]
A -->|判断需要工具| T[Tool]
T --> S[搜索引擎 / 数据库 / API / 本地函数]
S --> T
T --> A
A --> U
一个 Tool 通常包含:
| 要素 | 说明 |
|---|---|
| 名称 | 模型识别工具时使用的标识 |
| 描述 | 告诉模型工具能做什么、什么时候该用 |
| 参数 | 参数名、类型、描述、是否必填 |
| 返回值 | 工具执行后的结果 |
| 执行逻辑 | 真正运行的 Python 函数或外部接口调用 |
工具描述非常重要。模型是否会调用某个工具,往往取决于描述是否清晰。如果工具描述只写“查询”,模型很难判断用途;如果写成“用于查询指定公司的当日股票价格,输入应为公司中文名或股票代码”,调用概率会高很多。
用 @tool 定义工具
from langchain_core.tools import tool
@tool("multiply", return_direct=True)
def multiply(a: int, b: int) -> int:
"""计算两个整数的乘积。输入 a 和 b,返回 a * b。"""
return a * b
result = multiply.invoke({"a": 2, "b": 3})
print(result)
return_direct=True 表示工具结果可以直接作为最终输出返回。多数 Agent 场景会设置为 False,让模型拿到工具结果后继续组织自然语言回答。
用 StructuredTool 包装函数
from langchain_core.tools import StructuredTool
def get_stock_price(company: str) -> str:
"""查询公司股票价格。"""
# 示例逻辑,真实场景应调用行情接口
fake_data = {
"腾讯": "HKD 320.40",
"阿里": "HKD 82.15"
}
return fake_data.get(company, "未找到该公司的股票价格")
stock_tool = StructuredTool.from_function(
func=get_stock_price,
name="get_stock_price",
description="用于查询指定公司的股票价格,输入应为公司名称,例如:腾讯、阿里。"
)
print(stock_tool.invoke({"company": "腾讯"}))
MCP Server:把通用工具做成独立服务
MCP(Model Context Protocol,模型上下文协议)可以理解为 Agent 与外部工具服务之间的通信规范。Tool 通常写在 Agent 应用内部,而 MCP Server 把通用工具抽成独立服务,例如网页浏览、文件读写、日历、消息发送、数据库查询等能力,可以被多个 Agent 复用。
flowchart LR
A1[Agent A] --> MCP[MCP Server]
A2[Agent B] --> MCP
A3[Agent C] --> MCP
MCP --> W[网页浏览工具]
MCP --> F[文件工具]
MCP --> DB[(数据库)]
MCP --> MSG[消息系统]
内置 Tool 和 MCP Server 的差异:
| 对比维度 | 内置 Tool | MCP Server |
|---|---|---|
| 部署位置 | Agent 同一进程内 | 独立进程或远程服务 |
| 耦合程度 | 代码直接引用 | 按协议通信 |
| 复用性 | 通常服务于当前应用 | 多个 Agent 可共用 |
| 更新方式 | 改工具常常要重新部署 Agent | 工具服务可独立更新 |
| 性能 | 本地函数调用,延迟低 | 多一次进程间或网络通信 |
| 适合场景 | 专用、简单、高频工具 | 通用、复杂、跨应用工具 |
MCP 本身不关心 Agent 使用哪个模型,也不负责推理。它只负责描述和暴露工具、资源、提示词等上下文能力。
Agent:让模型自己决定下一步动作
Agent 是由模型驱动的任务执行系统。它不只是“回答一句话”,而是会根据用户目标决定是否需要调用工具、调用哪个工具、用什么参数、是否继续下一步。
一个典型 Agent 包含以下能力:
| 能力 | 作用 |
|---|---|
| LLM / Chat Model | 负责理解任务、推理和决策 |
| Memory | 保存会话上下文和任务状态 |
| Tools | 连接搜索、数据库、计算器、业务系统 |
| Planning | 拆解任务,决定步骤顺序 |
| Action | 实际执行工具调用或外部操作 |
| Observation | 读取工具结果,判断是否继续 |
Agent 的工作流可以画成循环:
flowchart TD
U[用户目标] --> T[Thought<br/>分析当前任务]
T --> A{是否需要工具}
A -- 否 --> F[Final Answer<br/>生成最终回答]
A -- 是 --> C[Action<br/>选择工具并构造参数]
C --> O[Observation<br/>读取工具结果]
O --> T
Function Calling / Tool Calling
Function Calling,也常叫 Tool Calling,是让模型返回结构化工具调用请求。模型不会执行函数,它只决定:
- 要不要调用工具;
- 调用哪个工具;
- 参数是什么。
真正执行工具的是应用或 Agent Executor。
适合 Function Calling 的任务:
- 查询天气、股价、物流状态等实时数据;
- 查询数据库;
- 执行计算;
- 调用公司内部业务接口;
- 多个工具组合处理任务。
示例:
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
@tool
def get_stock_price(company: str) -> str:
"""查询指定公司的股票价格,输入公司名称,例如:腾讯、阿里。"""
fake_data = {
"腾讯": "腾讯当前价格示例:HKD 320.40",
"阿里": "阿里当前价格示例:HKD 82.15"
}
return fake_data.get(company, "没有查询到价格")
tools = [get_stock_price]
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个可以使用工具的金融数据助手。"),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad")
])
agent = create_tool_calling_agent(
llm=chat_model,
tools=tools,
prompt=prompt
)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True
)
result = agent_executor.invoke({
"input": "帮我查一下腾讯的股票价格"
})
print(result["output"])
模型是否调用工具,和几个因素有关:
| 问题 | 处理方式 |
|---|---|
| 模型认为任务太简单,不调用工具 | 提示词里明确要求特定问题必须使用工具 |
| 工具描述模糊 | 写清工具用途、输入格式、返回含义 |
| 参数结构复杂,模型构造失败 | 使用 Pydantic 模型约束参数 |
| 模型工具调用能力弱 | 换用更擅长 Tool Calling 的模型 |
| 工具返回内容太长 | 做摘要或字段过滤后再交给模型 |
ReAct:思考、行动、观察循环
ReAct 是 Reasoning + Acting 的组合。它通过提示词约束模型按固定格式工作:
Thought: 我需要先判断当前问题是否需要外部信息
Action: 调用搜索工具
Action Input: 查询关键词
Observation: 工具返回结果
Thought: 已经拿到信息,可以回答
Final Answer: 最终回答
ReAct 的优势是过程清晰,便于调试;代价是 token 消耗更高,且对提示词格式更敏感。
一个搜索 Agent 示例:
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.tools.tavily_search import TavilySearchResults
search_tool = TavilySearchResults(
max_results=3,
description="用于搜索最新网页信息、新闻和历史数据。输入应该是明确的搜索查询。"
)
tools = [search_tool]
prompt = hub.pull("hwchase17/react")
agent = create_react_agent(
llm=chat_model,
tools=tools,
prompt=prompt
)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True
)
response = agent_executor.invoke({
"input": "2021 年 2 月 5 日腾讯收盘价是多少?"
})
print(response["output"])
ReAct 不是模型天然自带的流程,而是系统提示词约束出来的行为模式。模型遵循得好不好,和模型能力、提示词、工具描述都有关系。
Agent 类型怎么选
| 类型 | 特点 | 适合场景 |
|---|---|---|
| Tool Calling Agent | 使用模型原生工具调用能力,参数结构清晰 | 生产系统、API 调用、数据库查询 |
| ReAct Agent | 思考过程可见,便于排查问题 | 教学、调试、复杂推理探索 |
| Structured Chat Agent | 支持更复杂参数结构 | 多参数工具、多轮对话工具调用 |
| LangGraph Agent | 用状态图管理复杂流程 | 多智能体、循环、条件分支、人工审核 |
轻量工具调用优先考虑 Tool Calling Agent;复杂流程、有状态任务和多智能体协作更适合 LangGraph。
Retrieval 与 RAG:把外部知识接给模型
大模型有两个常见限制:
| 限制 | 说明 |
|---|---|
| 知识冻结 | 模型训练结束后,内部知识不会自动更新 |
| 幻觉 | 模型可能生成看起来合理但实际错误的信息 |
在金融、医疗、法律、企业知识库等领域,不能让模型凭记忆回答专业问题。RAG 的思路是:回答前先从外部知识库检索相关资料,把资料和用户问题一起交给模型,让模型基于上下文生成答案。
RAG 分两条链路:知识入库链路和用户查询链路。
flowchart TD
subgraph Ingest[知识入库]
A[原始文件<br/>PDF / TXT / CSV / HTML] --> B[Document Loader<br/>文档加载]
B --> C[Text Splitter<br/>文本切分]
C --> D[Embedding Model<br/>向量化]
D --> E[(Vector Store<br/>向量数据库)]
end
subgraph Query[用户查询]
U[用户问题] --> QE[问题向量化]
QE --> E
E --> R[Retriever<br/>召回相关片段]
R --> RR[可选:Rerank<br/>重排序]
RR --> P[Prompt<br/>问题 + 资料]
P --> L[LLM 生成答案]
L --> O[返回答案和引用]
end
RAG 的核心不是“把整个知识库塞给模型”,而是只挑出和问题最相关的片段。这样既控制上下文长度,也减少无关信息干扰。
RAG 的优缺点
| 方面 | 具体说明 |
|---|---|
| 知识可更新 | 更新知识库即可让系统使用新资料,不必重新训练模型 |
| 成本较低 | 相比微调,构建向量库和检索链路通常成本更低 |
| 可溯源 | 可以返回引用片段,让答案有依据 |
| 权限可控 | 可以按用户权限过滤可检索文档 |
| 检索质量是瓶颈 | 召回不到正确片段,模型很难答对 |
| 系统复杂度更高 | 需要维护加载、切分、嵌入、向量库、检索、重排序 |
| 受上下文窗口限制 | 检索片段太多会挤占模型推理空间 |
| 依赖知识库质量 | 文档错误、过期、格式混乱会直接影响回答 |
Retrieval 组件实践
LangChain 的 Retrieval 流程通常包含五步:
flowchart LR
S[Source 数据源] --> L[Load 加载]
L --> T[Transform 转换 / 切分]
T --> E[Embed 向量化]
E --> V[(Store 存储)]
V --> R[Retrieve 检索]
Document Loader:加载文档
Document Loader 把外部文件转换成 LangChain 的 Document 对象。Document 主要包含两个字段:
| 字段 | 说明 |
|---|---|
page_content | 文档正文 |
metadata | 文件名、页码、来源等元数据 |
示例:
from langchain_community.document_loaders import TextLoader, PyPDFLoader, CSVLoader, JSONLoader
# TXT(Plain Text,纯文本)
txt_loader = TextLoader("./risk.txt", encoding="utf-8")
txt_docs = txt_loader.load()
# PDF(Portable Document Format,便携式文档格式)
pdf_loader = PyPDFLoader("./risk.pdf")
pdf_docs = pdf_loader.load()
# CSV(Comma-Separated Values,逗号分隔值)
csv_loader = CSVLoader("./risk.csv")
csv_docs = csv_loader.load()
# JSON 加载通常需要指定 jq_schema
json_loader = JSONLoader(
file_path="./risk.json",
jq_schema=".",
text_content=False
)
json_docs = json_loader.load()
print(txt_docs[0].page_content)
print(txt_docs[0].metadata)
Text Splitter:切分长文本
长文档不能直接塞进向量库。过大的文本块会带来两个问题:
- 检索结果包含太多无关内容,模型容易被干扰;
- 检索片段占用上下文窗口,留给模型推理的空间变少。
切分策略常见几类:
| 策略 | 说明 | 适合场景 |
|---|---|---|
| 固定长度切分 | 按字符数或 token 数切分 | 简单文本、快速验证 |
| 递归切分 | 按段落、句子、标点逐级切分 | 通用文本,最常用 |
| 结构化切分 | 按 Markdown 标题、HTML 标签、代码函数切分 | 文档站点、代码库 |
| 语义切分 | 根据向量相似度寻找语义边界 | 对回答质量要求高的知识库 |
推荐先用 RecursiveCharacterTextSplitter:
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
loader = TextLoader("./risk.txt", encoding="utf-8")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=120,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
chunks = splitter.split_documents(docs)
print(len(chunks))
print(chunks[0].page_content)
chunk_overlap 用来让相邻文本块保留一部分重叠内容,避免关键上下文被切断。常见取值是 chunk_size 的 10% 到 20%。
Embedding:把文本转成向量
向量化后的文本可以做相似度搜索。
from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://127.0.0.1:11434"
)
query_vector = embeddings.embed_query("什么是反洗钱?")
doc_vectors = embeddings.embed_documents([
"反洗钱是预防和打击洗钱犯罪的措施。",
"今天天气很好。",
"客户尽职调查是金融机构识别客户风险的重要手段。"
])
print(len(query_vector))
print(len(doc_vectors))
嵌入向量可以支持:
| 能力 | 说明 |
|---|---|
| 语义匹配 | 判断两个文本语义是否相近 |
| 语义搜索 | 从知识库中找出最相关片段 |
| 推荐 | 根据用户兴趣向量匹配内容 |
| 聚类分析 | 发现文档集合里的主题分布 |
| RAG 检索 | 为模型生成答案提供上下文 |
Vector Store:存储和查询向量
向量数据库负责保存向量和元数据,并提供相似度查询。LangChain 支持多种向量库,例如 Chroma、FAISS、Milvus、Weaviate、Pinecone、Elasticsearch 等。
用 Chroma 建一个本地向量库:
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
loader = TextLoader("./risk.txt", encoding="utf-8")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=120
)
chunks = splitter.split_documents(docs)
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://127.0.0.1:11434"
)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
results = vectorstore.similarity_search("什么是反洗钱?", k=3)
for doc in results:
print(doc.page_content)
print(doc.metadata)
常见检索方式:
| 方法 | 说明 |
|---|---|
similarity_search | 返回最相似的文档 |
similarity_search_with_score | 返回文档和距离分数 |
similarity_search_by_vector | 用已经计算好的问题向量检索 |
max_marginal_relevance_search | MMR(Maximal Marginal Relevance,最大边际相关性)检索,兼顾相关性和多样性 |
Retriever:把向量库变成检索组件
Retriever 不一定自己存数据,它更像统一检索接口,可以封装向量库、关键词搜索、多路召回、重排序等策略。
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
docs = retriever.invoke("什么是反洗钱?")
for doc in docs:
print(doc.page_content)
带相似度阈值的检索:
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 5,
"score_threshold": 0.5
}
)
一个完整 RAG Chain
把检索器、提示词、模型和解析器串起来,就能得到一个最小 RAG 问答系统。
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_prompt = ChatPromptTemplate.from_template(
"""
你是一个严谨的知识库问答助手。
只能根据给定资料回答问题;如果资料中没有答案,就说“资料中没有相关信息”。
资料:
{context}
问题:
{question}
"""
)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| rag_prompt
| chat_model
| StrOutputParser()
)
answer = rag_chain.invoke("什么是反洗钱?")
print(answer)
这个流程里,用户问题会先进入 retriever,检索出的文档片段会填入 {context},原始问题会填入 {question},模型只能基于这些上下文组织回答。
Callbacks 与 LangSmith:让调用链路可观测
大模型应用调试困难,常见问题包括:
- 不知道最终发给模型的提示词是什么;
- 不知道 Agent 为什么选择某个工具;
- 不知道 RAG 检索到了哪些片段;
- 不知道哪个步骤耗时最长;
- 不知道线上错误来自解析器、模型还是工具。
Callbacks 是 LangChain 的回调机制,可以监听模型开始、模型结束、工具调用、Chain 执行等事件。LangSmith 则提供可视化链路追踪、数据集评估、Prompt 管理和线上监控。
开启 LangSmith 追踪通常需要配置环境变量:
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="YOUR_LANGSMITH_API_KEY"
export LANGCHAIN_PROJECT="langchain-demo"
当 Chain 或 Agent 执行后,可以在 LangSmith 中看到完整调用树,包括输入、输出、中间步骤、耗时和错误信息。
LangChain 适合什么场景,不适合什么场景
LangChain 不是所有大模型需求的默认答案。它适合复杂应用,但简单场景可能会显得偏重。
| 场景 | 是否适合 LangChain | 原因 |
|---|---|---|
| 单次调用模型生成文本 | 不一定需要 | 直接调用模型 SDK 更简单 |
| Prompt 模板很多、需要结构化输出 | 适合 | Prompt 和 Parser 能减少重复代码 |
| 多轮对话 | 适合 | Chat History / Memory 能统一管理上下文 |
| 企业知识库问答 | 适合 | Loader、Splitter、Embedding、Vector Store、Retriever 组件完整 |
| 工具调用 Agent | 适合 | Tools 和 Agent Executor 能减少胶水代码 |
| 多智能体、复杂状态机 | 更适合 LangGraph | 图结构比普通 Chain 更清晰 |
| 极致性能、链路很短 | 谨慎使用 | 框架抽象会带来额外复杂度 |
| 强业务定制平台 | 可作为原型工具 | 核心链路稳定后可保留框架或逐步自研 |
一种实用判断方式是:如果需求只是“把用户输入发给模型,再把输出展示出来”,直接调用模型 API 更清爽;如果需求涉及上下文、检索、工具、多步骤编排、调试评估,LangChain 的模块化结构能省掉大量重复工程。
用一个系统视角理解各组件
可以把一个大模型应用看成一个具备感知、记忆、知识、行动和执行流程的系统:
| LangChain 组件 | 类比 | 作用 |
|---|---|---|
| LLM / Chat Model | 大脑 | 理解、推理、生成语言 |
| PromptTemplate | 沟通模板 | 稳定表达任务、角色、约束和输出格式 |
| Output Parser | 翻译器 | 把自然语言输出转成业务数据结构 |
| Chains / LCEL | 工作流 | 把多个步骤串成可复用流程 |
| Memory | 短期记忆 | 保存当前会话上下文 |
| Vector Store / RAG | 知识库 | 提供外部专业知识和可更新资料 |
| Tools | 手、脚、感官 | 查询外部系统、执行计算、读写数据 |
| Agent | 调度者 | 决定下一步动作和工具调用顺序 |
| Callbacks / LangSmith | 监控系统 | 追踪调用链路、定位问题、评估效果 |
| LangGraph | 状态机 | 管理复杂分支、循环、多智能体协作 |
掌握 LangChain 的关键不是背 API,而是理解大模型应用的工程结构:输入如何构造,模型如何调用,输出如何解析,历史如何保存,外部知识如何检索,工具如何执行,复杂任务如何编排。只要这些环节清楚,后续无论使用 LangChain、LlamaIndex、Spring AI、Semantic Kernel,还是自研轻量框架,都能快速迁移思路。