从单次大语言模型调用发展成LangChain应用
逐步将一个LLM调用发展成LangChain应用层:提示词、链式结构、解析器、结构化输出、RAG、工具、智能体、MCP、可观测性以及安全性。
来源: From One LLM Call to a Real AI Application with LangChain
初次集成大型语言模型时,人们往往觉得最困难的部分是让模型给出任何回复。但实际上并非如此。只要拥有提示词、API密钥和SDK,几行代码就能生成合理的答案,这已经足够用于构建原型。
应用程序不会一直停留在原型阶段。接下来便是无需重新部署业务代码即可编辑提示词、使用不同模型、获得结构化回复以及访问应用程序自身的数据等功能。最终,模型还需要能够调用API和工具。正是在这个时候,简单的SDK调用便能演变成真正的AI应用程序——这也是LangChain备受关注的原因。
这并非因为调用模型本身很困难,实际上调用模型已经相当简单。真正的价值在于调用模型之外的所有环节:提示词设计、内容组合、解析处理、服务集成、数据检索、工具使用以及生产环境控制等。
从最简单的示例开始
LangChain 的核心函数是 invoke()。以 OpenAI 对话模型为例:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0
)response = model.invoke(
"Explain microservices to a software developer who is new to distributed systems."
)print(response.content)
创建模型,传入输入后调用 invoke(),再从 response.content 中读取文本。其实并没有什么复杂的技巧。
如果产品仅发送一个提示语并输出一个答案,使用提供商的 SDK 通常就足够了。而当应用规模扩大时,就会出现问题。
硬编码的提示语难以长期使用
将问题直接写入调用代码中无法实现扩展:
response = model.invoke(
"Explain event-driven architecture to a beginner."
)
主题和目标受众应当是运行时确定的值。ChatPromptTemplate 能让这一过程更加简单:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are a helpful software engineering assistant."
),
(
"human",
"Explain {topic} to a {audience}."
)
])
调用时提供变量:
messages = prompt.invoke({
"topic": "event-driven architecture",
"audience": "beginner"
})
response = model.invoke(messages)print(response.content)
提示语便能成为独立的组件。提示语决定了程序的行为,因此应与配置和业务规则一样受到重视。
将提示词与模型相连
在架构设计方面,LangChain有别于普通的SDK。无需手动连接每个步骤:
chain = prompt | model
而是这样:
response = chain.invoke({
"topic": "event-driven architecture",
"audience": "software engineer"
})
print(response.content)
通过管道符号可以构建一个链条:提示词的输出会传递给模型。随着阶段数量的增加,这种架构的扩展性也会提升。
从模型获取有用的结果
模型对象返回的是消息封装结构。而应用程序通常需要纯字符串形式的数据。StrOutputParser可以解决这个问题:
chain = prompt | model | parser
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()chain = prompt | model | parser
使用变量调用该工具:
response = chain.invoke({
"topic": "Docker",
"audience": "beginner"
})
print(response)
最终得到的字符串可以被后续处理流程直接使用。随着链条的复杂度增加,这种设计的便利性也会进一步提升。
此时架构设计就变得至关重要
直接在API处理函数中嵌入LangChain调用仅能暂时解决问题:
def endpoint():
# business logic
# prompt
# model configuration
# database call
# response formatting
数月之后,该接口就变得无法再被修改了。此时应采用服务边界设计:
class LLMService:
def __init__(self, chain):
self.chain = chain def generate(self, topic, audience):
return self.chain.invoke({
"topic": topic,
"audience": audience
})
API会请求服务端生成内容,而无需知晓其背后使用的是哪种模型。这样就能保持结构清晰。
那些跳过服务边界设计的团队往往会出现温度设置重复、API密钥分散以及各接口日志不一致的问题。将模型构建流程集中管理,还能及时发现同一域名下有两个功能意外使用了不同的提示语格式的情况。通常一个LLMService(或一小组针对特定域名的服务)就足够了,除非流量需求或所有权划分要求进行拆分。
如果我想更换模型该怎么办?
抽象层链式结构使得更换服务提供商更为便捷。开发工作可以先从 OpenAI 开始,之后再评估其他提供商、Azure 托管的模型或 Bedrock。无需为每次更换都重新编写产品代码。
例如,Anthropic 的聊天模型:
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-3-5-sonnet-latest"
)
这种链式结构可以保持不变:
chain = prompt | model | parser
抽象并不意味着所有模型都完全相同——它们的功能、上下文窗口大小、价格、延迟及行为表现都有所差异——但这样一来,评估和切换就变得可行了。
仅靠文本已不够用的时刻
活动策划类产品便体现了这一变化。用户会说明需要在周六使用 40x60 大小的帐篷、150 把椅子以及桌子。聊天机器人可能会用文字回复,但实际应用需要的是数据:
{
"tent_size": "40x60",
"chairs": 150,
"tables": true,
"event_date": "next Saturday"
}
使用 Pydantic 定义数据结构:
from pydantic import BaseModel
class EventRequest(BaseModel):
tent_size: str
chairs: int
tables: bool
event_date: str
需配置模型以返回符合该架构的数据(具体方式取决于集成方案)。需求从“给我文本”转变为“将此自然语言请求解析为已知结构”。这种模式对软件而言更为高效。
接下来是上下文问题
用户会提出模型仅凭提示信息无法回答的问题——例如预订是否可以更改。预订状态属于应用数据,因此必须提供上下文。
检索增强生成(RAG)是一种常见的解决方案。借助庞大的政策文档库,无需在每次调用时都传输所有文档,应先检索相关内容。具体而言:
question = "What is your cancellation policy?"
该应用会查找匹配的段落并将其作为上下文提供。模型则基于这些内容进行回答。它并非在“学习”公司政策,而是会在运行时检索相关证据并将其传递给模型。这种思维模式有助于理解RAG技术。
大语言模型无需包办一切
大语言模型不能替代普通软件。诸如算术运算之类的功能:
total = quantity * price
应该由代码来实现。订单查询功能:
SELECT *
FROM orders
WHERE order_id = ?
则应在数据库中完成。认证功能不应交由模型处理。大语言模型在语言理解、推理、分类、总结、生成以及有限决策方面表现优异。应将确定性任务保持为确定性处理,这样的分工能让人工智能系统更易于理解。
让模型能够使用工具
当用户询问订单所在位置时,模型可能并不知情——但应用程序可以提供相关查询功能:
def get_order_status(order_id: str):
...
将该函数注册为工具。模型可以判断自己需要信息,调用该工具,获取结果,再将该结果整合到最终答案中。文本生成 thus 变成了工作流中的一环。在此背景下,关于智能体的讨论才有了意义。
但智能体并非总是必需的
将每个工作流都称为“智能体”并无帮助。那些路径明确的流程——理解问题、获取客户数据、生成回复——可能只需要简单的串联操作即可完成。只有当模型必须动态选择工具来完成任务时,智能体才能发挥作用。例如,检查订单、报告物流状态并在必要时更新地址的请求,可能需要依次执行获取数据→检查→验证→更新→解释这些步骤。这样的场景才更适合作为智能体的应用案例。
不要仅仅因为存在某种智能体就引入它,应优先选择能够解决问题最简单的架构。
MCP在整体架构中的定位
随着系统规模的扩大,模型上下文协议(MCP)便显得十分有用:它为人工智能应用提供了与外部工具及数据交互的标准方式。无需为每项集成单独开发定制方案,MCP能够统一地暴露客户信息、订单数据、日历内容、数据库、文档以及业务API。LangChain与MCP处于不同的层级——LangChain负责协调提示词、模型、检索功能、工具及工作流;而MCP则负责标准化能力暴露的方式。随着聊天功能逐渐融入业务工作流,这种组合显得尤为重要。
可观测性的重要性出乎意料地快
原型设计通常会忽略可观测性问题。传统API的流程往往表现为请求→查询→响应。而大型语言模型的一次请求可能涉及提示词构建、数据检索、多次模型调用、工具使用、数据解析、重试操作以及更多次模型调用。仅进行日志记录是远远不够的:
LLM request failed
这毫无用处。操作人员需要了解提示词、模型、延迟时间、调用的工具、故障位置,以及各步骤是如何共同形成答案的。LangSmith和OpenTelemetry风格的监控工具能提供帮助。概率性行为使得可观测性变得更加重要,而非更不重要。
当模型拥有工具时,安全问题则有所不同
绝不能将模型选择的工具调用视为自动授权的行为。诸如以下的路径:
LLM → get_customer(12345) → Database
仍必须经过身份验证、授权检查、有效性验证、访问控制、速率限制以及审计日志记录。模型可以提出建议,但由应用程序来决定什么是被允许的。大型语言模型绝不能成为安全边界。
我目前的典型开发流程是怎样的
新的大型语言模型功能总是从简单之处开始。首先,能否调用该模型?
model.invoke(...)
接着,是否需要可重复使用的提示词?
prompt | model
当可预测的数据很重要时,需添加结构化输出功能;当模型需要使用自有数据时,需添加检索功能;当必须与其他系统交互时,需添加工具功能。只有在需要动态的多工具决策时才考虑使用智能体。如果在实际应用中该功能至关重要,还需添加可观测性、评估机制、安全保障以及成本控制措施。这样的设计能避免过度设计。
更重要的启示
调用模型是最简单的部分:
model.invoke(...)
随后便是工程实现的环节:提示词管理、响应结构设计、可靠上下文构建、API连接处理、将业务逻辑与模型分离、监控机制、权限控制,以及无需重写即可更换模型的功能。这些要素共同将一个实验转化为真正的软件。LangChain不应被视为“调用大语言模型的方式”,而应是连接概率模型与确定性系统的构建模块。模型只是其中一个组成部分,真正的核心工作在于其周边的一切。
最后思考
不要一开始就设计最复杂的智能体。先从一次模型调用开始,接着是可复用的提示词,再是结构化的输出结果,然后是受控的数据处理,之后才是工具的使用;只有在问题确实需要时才引入智能体。这样的发展路径更易于理解、测试、调试和部署。软件工程依然是核心学科,只不过现在工具箱里多了一种新的组件而已。
开发团队还意识到,提示词版本控制应与代码版本控制并列。若将提示词文本视为处理函数中的不可见字符串,一旦措辞变化悄然导致回答质量下降,就很难进行回滚。应将模板存储在可审查的文件中,在日志中于invoke()调用记录旁标注版本信息,并保留一组在每次提示词修改后仍需通过测试的黄金评估输入。这种规范其实就是将常规的软件维护措施应用到了概率性组件上。
成本与延迟同样需要尽早得到重视。在本地看来很流畅的流程,一旦出现数据检索、工具循环和重试叠加的情况,就会导致代币消耗激增。应统计每次成功处理用户请求所消耗的代币数,而不仅仅是每次模型调用的代币消耗。应对稳定的数据检索片段进行缓存,限制工具循环的次数,当工具返回空结果时立即处理错误,而非让模型去编造缺失的信息。这些控制措施虽不在LangChain的标准流程之内,却决定了系统在流量激增时的稳定性。
评估机制能够形成闭环。单元测试可用于验证解析器和数据结构是否正确。离线评估集可衡量系统对检索到内容的忠实度。在线监控则能追踪工具错误率以及人工干预的频率。没有评估机制,模型更换和提示词调整就只是无据可依的猜测;而有了评估机制,LangChain便不再只是一堆相互连接的组件,而会变成一个具备明确质量标准、便于维护的子系统。
当生产环境中出现问题时,应优先使用结构化的日志,而非聊天界面的截图。需记录提示词版本、模型编号、检索到的文档编号、调用的工具名称以及最终解析结果。这样的记录方式能将“模型出现幻觉”这一问题转化为可复现的故障,通过调整提示词、优化检索过滤器或完善数据结构即可解决,而非仅仅成为对人工智能不可预测性的模糊抱怨。
应逐步优化改进措施,聚焦关键指标,并将模型视为更大确定性系统中的众多协作组件之一。