上一篇把 LangChain 的基础环境准备好了,也跑通了最简单的一次模型调用。
这一篇继续往前走一点,主要看两个内容:PromptTemplate 和结构化输出。
本文对应代码位于 code/langchain-demo/chapter02。为了让每篇教程都能对上自己的代码,第二篇没有直接覆盖第一篇的 chapter01,而是新建了 chapter02 目录。
本篇目标
第一篇里面,我们是手动创建 SystemMessage 和 HumanMessage,然后直接调用模型:
python
response = model.invoke(
[
SystemMessage(content="你是一个简洁、准确的 LangChain 学习助手。"),
HumanMessage(content=question),
]
)
这种写法适合第一个 demo,但是只要提示词稍微复杂一点,就会遇到几个问题:
- 提示词内容和变量混在一起,不方便维护。
- 多个 demo 里重复创建模型对象,代码比较散。
- 模型返回的是普通文本,如果后续程序要继续处理,还要自己解析。
所以第二篇主要做三件事:
- 把创建模型的逻辑抽到
llm.py。 - 使用
ChatPromptTemplate管理提示词模板。 - 使用
PydanticOutputParser把模型输出解析成 Python 对象。
代码结构
第二篇的目录结构如下:
text
code/langchain-demo/
chapter02/
__init__.py
config.py
llm.py
chat_basic.py
prompt_template_demo.py
structured_output_demo.py
其中 config.py 和第一篇类似,依然负责读取 .env 里的 OpenAI 兼容接口配置。
这一篇新加了一个 llm.py,用来统一创建模型对象:
python
from langchain_openai import ChatOpenAI
from chapter02.config import get_openai_settings
def create_chat_model(temperature: float = 0.2) -> ChatOpenAI:
"""创建一个 LangChain ChatOpenAI 模型对象。
第二章会写多个 demo,把模型创建逻辑放到这里,可以避免每个文件重复
写 api_key、model、base_url 这些配置代码。
"""
settings = get_openai_settings()
return ChatOpenAI(
model=settings.model,
api_key=settings.api_key,
base_url=settings.base_url,
temperature=temperature,
)
这样后面每个 demo 只需要调用 create_chat_model(),不用重复写 api_key、model、base_url。
这里不算什么高级封装,只是为了让后续示例更聚焦在 LangChain 本身。
PromptTemplate 是什么
在开始之前,先简单了解一下 PromptTemplate。
可以把它理解成提示词模板。也就是说,提示词的固定结构先写好,真正变化的内容用变量传进去。
比如第二篇里面的示例:
python
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个适合新手的 LangChain 教程作者,回答要简洁。"),
("human", "请用三点说明 {topic},每点不超过 30 个字。"),
]
)
这里的 {topic} 就是变量。
后面运行时,只需要传入:
python
{"topic": "LangChain 的 Runnable"}
LangChain 就会根据模板生成完整的消息列表,再交给模型处理。
使用 ChatPromptTemplate
接下来看完整代码,文件是 chapter02/prompt_template_demo.py:
python
import sys
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from chapter02.config import load_environment
from chapter02.llm import create_chat_model
DEFAULT_TOPIC = "LangChain 的 Runnable"
def main() -> None:
"""演示用 ChatPromptTemplate 组织提示词。"""
load_environment()
topic = " ".join(sys.argv[1:]) or DEFAULT_TOPIC
# PromptTemplate 的价值是把"提示词结构"和"变量内容"分开。
# 以后 topic 可以来自命令行、接口参数、数据库或检索结果。
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个适合新手的 LangChain 教程作者,回答要简洁。"),
("human", "请用三点说明 {topic},每点不超过 30 个字。"),
]
)
model = create_chat_model()
# 这是 LangChain Expression Language 的写法:
# prompt 负责生成消息,model 负责调用大模型,StrOutputParser 负责取出文本。
chain = prompt | model | StrOutputParser()
answer = chain.invoke({"topic": topic})
print("Topic:")
print(topic)
print()
print("Answer:")
print(answer)
if __name__ == "__main__":
main()
这里需要注意的是这一行:
python
chain = prompt | model | StrOutputParser()
这是 LangChain Expression Language,也就是经常看到的 LCEL 写法。
它表示一条处理链路:
text
PromptTemplate -> ChatModel -> OutputParser
具体来说:
prompt根据变量生成消息。model调用大语言模型。StrOutputParser从模型返回结果中取出文本内容。
这样写的好处是流程非常清晰,以后要替换模型、替换解析器、或者在中间加别的步骤,也比较自然。
运行 PromptTemplate 示例
在 code/langchain-demo 目录下运行:
bash
uv run python -m chapter02.prompt_template_demo
默认主题是:
text
LangChain 的 Runnable
也可以自己传入主题:
bash
uv run python -m chapter02.prompt_template_demo "LangChain 的 PromptTemplate"
正常输出类似这样:
text
Topic:
LangChain 的 Runnable
Answer:
1. Runnable 是可执行组件接口。
2. 支持 invoke、stream、batch。
3. 可用管道符组合成链。
可以看到,这里已经不是手写完整提示词了,而是把主题作为变量传给模板。
为什么需要结构化输出
上面的 PromptTemplate 示例,最终返回的还是一段普通文本。
但是在真实开发中,经常会希望模型返回固定结构,比如:
json
{
"title": "LangChain 框架简介",
"summary": "LangChain 是一个用于构建大语言模型应用的框架。",
"keywords": ["LangChain", "大语言模型应用", "工具"],
"difficulty": "beginner"
}
这种结构化结果有几个好处:
- 程序可以继续读取字段,不需要再从自然语言里猜。
- 可以用 Pydantic 校验字段类型。
- 后续存数据库、写接口、做前端展示都更方便。
LangChain 支持多种结构化输出方式。这里先不用模型原生的 json_schema 能力,而是使用 PydanticOutputParser。
这样对本地 OpenAI 兼容代理会更友好,只要模型能按照提示词输出 JSON,就可以解析成 Pydantic 对象。
定义输出结构
先定义一个 Pydantic 类型:
python
from typing import Literal
from pydantic import BaseModel, Field
class ArticleSummary(BaseModel):
"""模型最终要返回的数据结构。"""
title: str = Field(description="给内容生成一个短标题")
summary: str = Field(description="用一句话总结内容")
keywords: list[str] = Field(description="提取 3 个关键词")
difficulty: Literal["beginner", "intermediate", "advanced"] = Field(
description="判断这段内容适合的学习难度"
)
这里定义了四个字段:
title:标题。summary:一句话总结。keywords:关键词列表。difficulty:学习难度,只允许三个固定值。
这个结构就是后面希望模型返回的格式。
使用 PydanticOutputParser
完整代码在 chapter02/structured_output_demo.py:
python
import json
import sys
from typing import Literal
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
from chapter02.config import load_environment
from chapter02.llm import create_chat_model
DEFAULT_TEXT = (
"LangChain 是一个用于构建大语言模型应用的框架,"
"可以把模型、提示词、工具、记忆和外部数据源组合起来。"
)
class ArticleSummary(BaseModel):
"""模型最终要返回的数据结构。"""
title: str = Field(description="给内容生成一个短标题")
summary: str = Field(description="用一句话总结内容")
keywords: list[str] = Field(description="提取 3 个关键词")
difficulty: Literal["beginner", "intermediate", "advanced"] = Field(
description="判断这段内容适合的学习难度"
)
def main() -> None:
"""演示用 PydanticOutputParser 把模型输出解析成 Python 对象。"""
load_environment()
text = " ".join(sys.argv[1:]) or DEFAULT_TEXT
parser = PydanticOutputParser(pydantic_object=ArticleSummary)
# 这里没有依赖模型原生 json_schema 能力,而是把格式要求写进提示词。
# 这种方式对本地 OpenAI 兼容代理更友好,适合作为入门学习。
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个信息整理助手,只输出符合要求的数据。"),
(
"human",
"请把下面内容整理成结构化结果。\n\n"
"内容:{text}\n\n"
"{format_instructions}",
),
]
).partial(format_instructions=parser.get_format_instructions())
model = create_chat_model(temperature=0)
chain = prompt | model | parser
result = chain.invoke({"text": text})
print("Input:")
print(text)
print()
print("Structured output:")
print(json.dumps(result.model_dump(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
这段代码的关键是:
python
parser = PydanticOutputParser(pydantic_object=ArticleSummary)
parser 会根据 ArticleSummary 生成格式说明。
然后通过:
python
parser.get_format_instructions()
把格式要求塞进提示词里。
最后这一行:
python
chain = prompt | model | parser
表示模型返回结果后,不再直接拿字符串,而是交给 parser 解析成 ArticleSummary 对象。
这里把 temperature 设置成 0,是为了让模型输出更稳定一点。结构化输出场景里,一般不希望模型太发散。
运行结构化输出示例
在 code/langchain-demo 目录下运行:
bash
uv run python -m chapter02.structured_output_demo
正常情况下,会看到类似输出:
text
Input:
LangChain 是一个用于构建大语言模型应用的框架,可以把模型、提示词、工具、记忆和外部数据源组合起来。
Structured output:
{
"title": "LangChain 框架简介",
"summary": "LangChain 是一个用于构建大语言模型应用的框架,可组合模型、提示词、工具、记忆和外部数据源。",
"keywords": [
"LangChain",
"大语言模型应用",
"工具与数据源"
],
"difficulty": "beginner"
}
如果能正常输出这个 JSON,就说明模型输出已经被成功解析成了 Pydantic 对象。
常见问题
结构化输出解析失败
如果模型没有按格式输出,就可能解析失败。
这种情况可以从几个方向排查:
- 把提示词写得更明确一点,比如强调"只输出 JSON"。
- 把
temperature调低,减少模型自由发挥。 - 检查字段定义是否过于复杂。
- 如果模型服务支持原生 JSON Schema,可以后面再尝试
with_structured_output。
这一篇先使用 PydanticOutputParser,就是为了减少对模型服务能力的依赖。
PromptTemplate 和普通字符串有什么区别
普通字符串也可以写提示词,但是一旦变量多起来,就不太好维护。
PromptTemplate 的作用就是把提示词结构固定下来,把变化的内容留给变量传入。
后面做 RAG 时,这一点会更明显。比如用户问题、检索出来的文档片段、回答格式要求,都可以通过模板组织起来。
小结
这一篇在第一篇基础调用的基础上,继续完成了两个关键能力:
- 使用
ChatPromptTemplate管理提示词模板。 - 使用 LCEL 的
prompt | model | parser串起处理流程。 - 使用
StrOutputParser获取文本输出。 - 使用
PydanticOutputParser把模型输出解析成结构化对象。 - 通过
chapter02目录保留第二篇独立代码,避免影响第一篇示例。
到这里,模型已经不只是能回答问题了,还可以按照我们指定的提示词结构和数据结构返回结果。
下一篇就可以继续往工具调用方向走,看看 LangChain 里怎么让模型选择和调用工具。
参考
- https://docs.langchain.com/oss/python/langchain/overview
- https://docs.langchain.com/oss/python/langchain/quickstart
- https://python.langchain.com/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html
- https://python.langchain.com/api_reference/core/output_parsers/langchain_core.output_parsers.pydantic.PydanticOutputParser.html