芥末
发布于 2025-11-19 / 0 阅读
0
0

LangChain 大模型应用开发框架核心组件详解

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-openaiOpenAI 兼容模型的封装
langchain-ollamaOllama 本地模型和嵌入模型封装
langchain-chromaChroma 向量数据库集成
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_urlapi_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 组件解决的问题
FormatPromptTemplate、ChatPromptTemplate、Message把用户输入格式化为模型能理解的提示
PredictLLM、Chat Model、Embedding Model调用文本模型、对话模型或嵌入模型
ParseStrOutputParser、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 也有异步方法,例如 ainvokeastreamabatch,适合并发调用或异步 Web 服务。

Chains:把多个组件串成工作流

Chain 的核心思想很简单:上一个组件的输出作为下一个组件的输入,多个步骤组成一个可执行流程。

flowchart LR
    A[输入变量] --> B[Prompt Template]
    B --> C[Chat Model]
    C --> D[Output Parser]
    D --> E[最终结果]

以前 LangChain 常用 LLMChainSequentialChain 这类封装。它们能用,但在较新的版本里已经不再是推荐方式。更推荐使用 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 的差异:

对比维度内置 ToolMCP 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_searchMMR(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,还是自研轻量框架,都能快速迁移思路。


评论