LangChain 实战:从模型调用到可组合的 LLM 应用
大语言模型很擅长生成文本,但把它真正嵌入应用时,问题会迅速变得复杂:提示词如何统一管理?模型供应商如何灵活切换?非结构化回答怎样交给程序继续处理?如何获取实时信息、调用数据库或执行计算?当一次请求经过多个环节后,又该怎样定位性能和结果问题?
LangChain 的价值正在于此。它并不只是替开发者调用一次模型 API,而是把模型、消息、解析器和工具等能力抽象成可组合的标准组件,使一条 LLM 工作流能够像数据管道一样被搭建、运行和观察。
从一次最小调用开始
使用聊天模型前,应先把 API Key 配置在环境变量中,避免将密钥直接写进代码或提交到版本库。安装对应集成包后,可以创建一个聊天模型:
bash
pip install -U langchain-openai
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
model = ChatOpenAI(
model="gpt-5-mini",
temperature=0,
max_retries=2,
)
messages = [
SystemMessage(content="将用户输入的英文翻译成中文"),
HumanMessage(content="Hi!"),
]
response = model.invoke(messages)
print(response.content)
聊天模型接收的不是一段无法区分来源的纯文本,而是一组带角色的消息。SystemMessage 用于设定模型的行为边界,HumanMessage 表示用户输入,模型则返回 AIMessage。这种结构天然适合多轮对话,也为工具调用和上下文管理留下了空间。
模型响应中除了 content,通常还包含两类值得关注的信息:response_metadata 保存模型名称、请求标识和结束原因等响应细节,适合调试与日志记录;usage_metadata 记录输入、输出和总 Token 数,可用于成本统计与预算控制。
如果应用只需要最终文本,可以加入字符串输出解析器:
python
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
text = parser.invoke(response)
print(text)
Runnable:统一不同组件的调用方式
模型和输出解析器看起来职责完全不同,却都可以调用 invoke()。原因是它们都遵循 Runnable 接口。Runnable 为组件提供了一套统一能力:
invoke():处理单个输入;batch():批量处理多个输入;stream():边生成边返回结果;- 组合与检查:把多个组件接成工作流,并查看其输入、输出和配置。
统一接口带来的直接收益是低耦合。上游只负责产生输出,下游只需接收符合约定的输入,开发者不必为每种组件重新设计一套调用流程。
LangChain Expression Language(LCEL)进一步提供了声明式编排方式。使用 | 运算符即可把多个 Runnable 连接为一个新的 Runnable:
python
chain = model | parser
result = chain.invoke(messages)
print(result)
这里的 chain 本质上是 RunnableSequence。前一个组件的输出会成为后一个组件的输入,而且组合后的整体依然遵循 Runnable 接口。因此,一条小链可以继续作为组件接入更大的工作流。这种"组合后仍可组合"的特性,是 LangChain 构建复杂应用的基础。
用统一入口管理不同聊天模型
如果已经确定使用 OpenAI 兼容接口,可以通过 ChatOpenAI 明确创建模型,并配置 model、temperature、max_tokens、timeout、max_retries 和 base_url 等参数。对于兼容 OpenAI 协议的服务,只需按供应商要求调整地址、密钥和模型名。
当应用需要在多个供应商之间切换时,init_chat_model() 更适合作为统一入口:
python
from langchain.chat_models import init_chat_model
gpt_model = init_chat_model(
"gpt-5-mini",
model_provider="openai",
temperature=0,
)
deepseek_model = init_chat_model(
"deepseek-chat",
model_provider="deepseek",
temperature=0,
)
各模型集成实现相同的聊天模型接口,因此业务代码可以继续使用 invoke(),而不必感知底层供应商。还可以把模型名称、供应商、温度等字段声明为运行时可配置项,让同一条工作流根据场景动态选择模型。
除了云端 API,LangChain 也能连接通过 Ollama 部署的本地模型:
python
from langchain_ollama import ChatOllama
local_model = ChatOllama(
model="deepseek-r1:70b",
base_url="http://127.0.0.1:11434",
)
本地部署更有利于数据控制和定制,云端服务通常更便于快速接入。由于二者具有一致的调用形式,切换方案时上层业务受到的影响更小。
工具调用:让模型连接真实世界
模型本身不能保证掌握实时信息,也不会真正执行计算、查询数据库或操作外部系统。工具调用的作用,是让模型根据工具名称、说明和参数结构,判断何时需要借助外部能力,并生成一次结构化的调用请求。
最简单的自定义工具可以用 @tool 创建:
python
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""计算两个整数的乘积。
Args:
a: 第一个整数
b: 第二个整数
"""
return a * b
函数名、类型标注和文档字符串共同组成工具的 Schema。模型不会阅读函数实现来猜测用途,因此清晰的名称、准确的参数类型和无歧义的描述会直接影响调用质量。复杂输入还可以借助 Pydantic 模型和 Field 描述,以获得更严格的数据验证。
将工具绑定到模型后,完整流程通常包含两次模型交互:
- 模型根据用户问题决定是否调用工具,并在
AIMessage.tool_calls中给出工具名和参数; - 应用执行对应函数,把结果封装为
ToolMessage并连同历史消息再次交给模型,由模型组织最终答案。
python
from langchain_core.messages import HumanMessage, ToolMessage
tools = [multiply]
model_with_tools = model.bind_tools(tools)
tool_map = {tool.name: tool for tool in tools}
messages = [HumanMessage(content="9 乘 6 等于多少?")]
ai_message = model_with_tools.invoke(messages)
messages.append(ai_message)
for call in ai_message.tool_calls:
output = tool_map[call["name"]].invoke(call["args"])
messages.append(
ToolMessage(content=str(output), tool_call_id=call["id"])
)
final_message = model_with_tools.invoke(messages)
print(final_message.content)
关键点在于,模型只是提出调用意图,真正执行工具的是应用程序。这使开发者能够在执行前加入权限检查、参数校验、超时控制和审计日志。除了自定义函数,还可以接入搜索服务、企业 API 和数据库查询等现成工具,把自然语言变成现有系统的统一交互入口。
结构化输出:让回答直接进入业务流程
一段自然语言对人很友好,却不适合程序稳定读取。如果后续逻辑需要固定字段,与其依赖正则表达式,不如提前声明输出结构,并让模型按结构生成结果。
python
from pydantic import BaseModel, Field
class Joke(BaseModel):
setup: str = Field(description="笑话的铺垫")
punchline: str = Field(description="笑话的包袱")
rating: int = Field(description="1 到 10 分的趣味评分")
structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("讲一个关于程序员的笑话")
print(result.setup)
print(result.rating)
结构可以使用 Pydantic、TypedDict 或 JSON Schema 描述。Pydantic 适合需要解析和校验的 Python 应用;TypedDict 较轻量;JSON Schema 则便于跨语言和接口协作。调试阶段还可以启用 include_raw=True,同时保留模型原始响应、解析结果和解析错误。
结构化输出非常适合信息抽取、分类、表单填充和下游 API 入参生成。不过,"结构正确"不等于"事实正确",业务仍应对日期范围、金额关系和权限规则等语义约束进行二次校验。
流式响应:降低用户感知等待
invoke() 会等待完整答案生成后一次性返回。对于较长内容,用户会经历明显空白期。stream() 则返回一个迭代器,让应用在消息块生成时立即处理:
python
for chunk in model.stream("写一段五句话的短诗"):
print(chunk.content, end="", flush=True)
因为模型和解析器都实现 Runnable,组合后的链同样可以流式执行:
python
chain = model | StrOutputParser()
for text_chunk in chain.stream("介绍一下 Runnable"):
print(text_chunk, end="", flush=True)
在 Web 应用中,服务端可以使用 Server-Sent Events(SSE)持续向浏览器推送数据。SSE 基于 HTTP,适合模型文本这种服务器到客户端的单向增量传输。高并发 I/O 场景还可以使用 astream() 配合 Python 的 async、await 与事件循环,避免等待网络响应时阻塞其他请求。
需要注意的是,流式处理的粒度不一定等于完整词语或句子。如果界面要求逐句展示,可以在下游增加缓冲区,累计到标点后再输出;同时还应处理断线、取消请求和最终 Token 统计等问题。
可观测性:看清复杂链路内部发生了什么
当应用只有一次模型调用时,打印响应基本够用。随着链路加入模型、工具、解析器和多次调用,仅看最终结果很难解释错误来自哪里。LangSmith 可以记录每一步的输入、输出、耗时、调用关系和异常,形成完整的运行轨迹。
开启跟踪后,无需把大量日志代码侵入业务组件,就能观察 RunnableSequence 内各环节的执行情况。这类信息可以回答几个关键问题:哪一次模型调用最慢?工具是否拿到了正确参数?结构化解析在哪一步失败?Token 成本主要消耗在哪里?
在开发阶段,可观测性帮助定位提示词和编排问题;在生产环境中,它也为性能优化、质量评估和故障审计提供依据。API Key、用户隐私数据和工具返回的敏感字段仍需按实际安全要求进行脱敏和访问控制。
结语
LangChain 的核心思路可以概括为三点:用标准接口屏蔽组件差异,用声明式组合搭建工作流,用工具和结构化数据连接外部系统。最小的模型调用只是起点;当 Runnable、LCEL、工具调用、结构化输出、流式传输和链路跟踪结合起来后,模型才真正具备成为应用组件的条件。
实际项目中,可以先从一条短链开始:聊天模型负责理解与生成,解析器负责稳定输出,工具负责访问真实数据,再通过流式交互和可观测性改善体验与运维。随着需求增长,这套组合式结构可以自然扩展,而不必推翻最初的业务代码。