LangChain 1.0+ 第二篇:Prompt、结构化输出与 LCEL 组件组合
摘要:光会调用模型还不够,应用还需要把任务规则说清楚、让程序稳定拿到结构化数据。本文讲透 Prompt 输入协议、Structured Output 输出协议,以及把组件串成链的 LCEL 语法。
引子:调通了模型,然后呢
你已经会用 model.invoke() 调通一个大模型。但把一句用户问题直接丢给模型,它只会大概知道"要回答",不知道你的业务规则:排查步骤是什么、能不能编造、缺少信息时怎么办、要不要转人工。
另一头是程序:模型回答的自然语言,人看得懂,程序却不好稳定处理------取个电影评分,得靠正则去猜数字。
这两个问题,正是本文要讲的两套"协议":
- Prompt 解决输入侧:如何把任务规则说清楚 → 输入协议
- Structured Output 解决输出侧:如何让模型返回程序能处理的数据 → 输出协议
- LCEL 解决组合侧:如何把 Prompt、Model、Parser 串成一条流水线
一句话记忆:Prompt 让模型知道该怎么做;Structured Output 让程序知道模型返回了什么;LCEL 让它们能串起来。
一、Prompt:把任务写成输入协议
1. 工程化 Prompt 的五类信息
工程化 Prompt 不是"写一段话",而是把任务规则结构化地讲清楚。通常包含五类信息:
| 要素 | 说明 |
|---|---|
| 角色 | 模型以什么身份回答 |
| 任务 | 这次要完成什么 |
| 上下文 | 可依据的资料、消息或工具结果 |
| 约束 | 哪些不能做,缺少信息时怎么办 |
| 输出 | 回答按什么形式给出 |
常见的五类错误,都对应这五要素的缺失:
- 只写"你是一个助手",没有业务边界(缺约束)
- 只要求"不要出错",没说缺信息时怎么办(缺约束)
- 需要实时信息时,没要求使用工具或说明来源(缺上下文)
- 只要求输出 JSON,但没定义字段(缺输出)
- 所有约束都放 Prompt 里,却没在工具/Schema/代码层做校验(过度依赖 Prompt)
Prompt 不是让模型变聪明,而是让模型更清楚当前任务规则。
2. PromptTemplate:让 Prompt 可复用、可组合
每次手写完整 Prompt,会带来重复代码多、规则不一致、变量易漏传、难以和其他组件组合四个问题。
f-string 也能做字符串替换,但 PromptTemplate 的价值不在替换,而在结构和组合:
- 保留 system / human 等消息角色结构
- 自动管理模板变量,缺变量时报错
- 可作为 Runnable,与 model、structured output 组合
- 适合长 Prompt 统一管理和复用
python
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "你是{device_type}设备的客服助手,回答要简洁、准确。"),
("human", "用户问题:{question}"),
])
chain = prompt | model
response = chain.invoke({
"device_type": "饮水机",
"question": "不出水怎么办?"
})
执行流程:输入变量 → PromptTemplate 生成 messages → Model 生成回答。
f-string 管字符串替换;PromptTemplate 管消息模板和组件组合。
二、Structured Output:让程序拿到可处理的数据
1. 为什么需要结构化输出
自然语言适合人看,程序却要"格式稳定"。同样一个电影信息,两种形态:
| 形式 | 示例 |
|---|---|
| 自然语言 | 《肖申克的救赎》上映于 1994 年,导演是弗兰克·德拉邦特,评分 9.7 |
| 结构化数据 | {"title": "肖申克的救赎", "year": 1994, "director": "弗兰克·德拉邦特", "rating": 9.7} |
Schema 就是"数据格式说明书",规定返回哪些字段、什么类型、什么含义。课程中主要用 Pydantic 定义:
python
from pydantic import BaseModel, Field
class Movie(BaseModel):
title: str = Field(..., description="电影名称")
year: int = Field(..., description="上映年份")
director: str = Field(..., description="导演")
rating: float = Field(..., description="评分,满分 10 分")
一个关键认知:结构化输出解决的是"格式稳定",不保证事实正确或最新。实时信息应来自搜索、数据库或 API,而不是让模型猜。
2. with_structured_output:按 Schema 返回数据
调用方式只需要在模型上加一层:
python
model_with_structure = model.with_structured_output(Movie)
response = model_with_structure.invoke("""
请从下面文本中抽取电影信息:
《肖申克的救赎》上映于 1994 年,导演是弗兰克·德拉邦特,评分 9.7 分。
""")
print(response.title) # 肖申克的救赎
print(response.rating) # 9.7
普通调用和结构化调用的差别:
vbscript
普通调用:输入 → 模型 → AIMessage(response.content)
结构化调用:输入 → 模型 → 按 Schema 解析 → Pydantic 对象(response.title)
三个实用细节:
Field(description=...)不只是给开发者看,也帮助模型理解字段含义。include_raw=True用于调试 :同时拿到原始消息和解析结果,返回{"raw": AIMessage, "parsed": Movie, "parsing_error": None}。- 返回结果可直接作为 Python 对象使用 ,程序拿着
.title、.rating就能写进数据库或往下传。
3. 与 Agent 的关系预告
with_structured_output 适合模型级单步任务 (信息抽取、分类、整理字段)。到了 Agent 阶段,结构化输出可以作为任务完成后的最终业务结果,例如:
json
{
"answer": "建议先检查电源和进水管...",
"need_human": true,
"reason": "用户描述可能涉及设备故障",
"suggested_action": "转人工客服"
}
程序拿到这份结果,就能判断是否转人工、创建工单、记录故障------这正是"让程序知道模型返回了什么"在真实业务里的落点。
三、LCEL:Runnable 与组件组合
1. Runnable:统一调用接口
LangChain 里 Prompt、Model、Parser、Retriever、Agent 都有 invoke / stream / batch 方法,这不是巧合------它们都实现了同一套执行接口 Runnable。
| 方法 | 说明 |
|---|---|
invoke |
单次调用 |
stream |
流式调用,边生成边返回 |
batch |
批量调用 |
ainvoke |
异步调用 |
python
# 模型
response = model.invoke("你好,介绍一下 LangChain。")
# 流式:聊天 UI 打字机效果
for chunk in model.stream("写一段客服欢迎语。"):
print(chunk.content, end="")
# Agent 也遵循同一接口
result = agent.invoke({"messages": [{"role": "user", "content": "帮我查订单状态"}]})
Runnable 解决的是"组件怎么被统一调用"的问题。
2. prompt | model 为什么能这样写
LCEL (LangChain Expression Language)是组合 Runnable 的表达式语法。prompt | model 里的 | 不是"或",而是把左边 Runnable 的输出,传给右边 Runnable 作为输入:
python
chain = prompt | model
# 等价于:
messages = prompt.invoke(input_data)
response = model.invoke(messages)
为什么 | 左右通常必须是 Runnable?因为 | 的本质是组件组合,LangChain 需要知道怎么执行 A 和 B------普通字符串没有 invoke 方法,不能参与链式组合。
普通函数想参与也可以,用 RunnableLambda 包装:
python
from langchain_core.runnables import RunnableLambda
def add_prefix(text):
return "用户问题:" + text
chain = RunnableLambda(add_prefix) | model
Runnable 解决"怎么执行";
|解决"怎么连接"。
3. Chain:多条 Runnable 串成流水线
Chain 就是多个 Runnable 按顺序组合形成的执行流程:
python
chain = prompt | model # Prompt → Model
chain = prompt | model | parser # Prompt → Model → Parser
执行时内部流程:输入 → prompt.invoke(...) → model.invoke(...) → parser.invoke(...) → 输出。
Chain 不是新组件,而是把多个 Runnable 组成一条流水线。
4. LCEL 需要学到什么程度
对 LangChain 1.0+ 的 Agent 应用,新手不需要一开始系统学复杂 LCEL。分清楚重点:
| 内容 | 是否重点 |
|---|---|
| invoke / stream / batch | 需要掌握 |
| prompt | model | 需要掌握 |
| RunnableLambda | 了解即可 |
| 复杂 LCEL 分支、并行、路由 | 后面需要时再学 |
| 用 LCEL 替代 Agent 主线 | 不建议 |
原因:LangChain 1.0+ 的主线更偏向 Agent、Middleware、State 和 LangGraph。这一章只要能看懂 chain = prompt | model | parser,就达到目标。
四、小结
回到开头的两个问题:
- 输入侧:Prompt 是输入协议,PromptTemplate 让它可以复用、可组合。
- 输出侧 :Schema 是输出协议,
with_structured_output让程序拿到可处理的 Python 对象。 - 组合侧 :Runnable 是统一执行接口,
|把组件串成 Chain。
一句话收束:Prompt 管输入;Schema 管输出;Runnable 管执行;| 管连接。
下一步,Agent 会把 Prompt、工具、结构化输出组织成完整任务,把这三块知识真正用起来。相关背景可延伸阅读 \[MCP模型上下文协议]、\[主流LLM落地框架]。