LangChain 入门学习第二篇-PromptTemplate 和结构化输出

上一篇把 LangChain 的基础环境准备好了,也跑通了最简单的一次模型调用。

这一篇继续往前走一点,主要看两个内容:PromptTemplate 和结构化输出。

本文对应代码位于 code/langchain-demo/chapter02。为了让每篇教程都能对上自己的代码,第二篇没有直接覆盖第一篇的 chapter01,而是新建了 chapter02 目录。

本篇目标

第一篇里面,我们是手动创建 SystemMessageHumanMessage,然后直接调用模型:

python 复制代码
response = model.invoke(
    [
        SystemMessage(content="你是一个简洁、准确的 LangChain 学习助手。"),
        HumanMessage(content=question),
    ]
)

这种写法适合第一个 demo,但是只要提示词稍微复杂一点,就会遇到几个问题:

  1. 提示词内容和变量混在一起,不方便维护。
  2. 多个 demo 里重复创建模型对象,代码比较散。
  3. 模型返回的是普通文本,如果后续程序要继续处理,还要自己解析。

所以第二篇主要做三件事:

  1. 把创建模型的逻辑抽到 llm.py
  2. 使用 ChatPromptTemplate 管理提示词模板。
  3. 使用 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_keymodelbase_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

具体来说:

  1. prompt 根据变量生成消息。
  2. model 调用大语言模型。
  3. 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"
}

这种结构化结果有几个好处:

  1. 程序可以继续读取字段,不需要再从自然语言里猜。
  2. 可以用 Pydantic 校验字段类型。
  3. 后续存数据库、写接口、做前端展示都更方便。

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 对象。

常见问题

结构化输出解析失败

如果模型没有按格式输出,就可能解析失败。

这种情况可以从几个方向排查:

  1. 把提示词写得更明确一点,比如强调"只输出 JSON"。
  2. temperature 调低,减少模型自由发挥。
  3. 检查字段定义是否过于复杂。
  4. 如果模型服务支持原生 JSON Schema,可以后面再尝试 with_structured_output

这一篇先使用 PydanticOutputParser,就是为了减少对模型服务能力的依赖。

PromptTemplate 和普通字符串有什么区别

普通字符串也可以写提示词,但是一旦变量多起来,就不太好维护。

PromptTemplate 的作用就是把提示词结构固定下来,把变化的内容留给变量传入。

后面做 RAG 时,这一点会更明显。比如用户问题、检索出来的文档片段、回答格式要求,都可以通过模板组织起来。

小结

这一篇在第一篇基础调用的基础上,继续完成了两个关键能力:

  1. 使用 ChatPromptTemplate 管理提示词模板。
  2. 使用 LCEL 的 prompt | model | parser 串起处理流程。
  3. 使用 StrOutputParser 获取文本输出。
  4. 使用 PydanticOutputParser 把模型输出解析成结构化对象。
  5. 通过 chapter02 目录保留第二篇独立代码,避免影响第一篇示例。

到这里,模型已经不只是能回答问题了,还可以按照我们指定的提示词结构和数据结构返回结果。

下一篇就可以继续往工具调用方向走,看看 LangChain 里怎么让模型选择和调用工具。

参考

  1. https://docs.langchain.com/oss/python/langchain/overview
  2. https://docs.langchain.com/oss/python/langchain/quickstart
  3. https://python.langchain.com/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html
  4. https://python.langchain.com/api_reference/core/output_parsers/langchain_core.output_parsers.pydantic.PydanticOutputParser.html
相关推荐
qyyyyy5701 小时前
PDF 表格翻译后总是错位?参数表、测试数据和跨页表格处理方法
ai·语音识别·机器翻译·数据库管理员·石墨文档
上玄code1 小时前
【Agent精讲】调一个LLMAPI背后的工程问题
java·开发语言·python·chatgpt
Uncommon.1 小时前
使用Pytorch操作张量(多维数组)
人工智能·pytorch·python
码云骑士1 小时前
105-Ollama本地部署-Llama3-Qwen2-Modelfile-REST-API
python
晚安code1 小时前
MCP Server 开发入门:手把手写一个能跑的 Server,三种协议怎么选
python·ai编程
CTA量化套保1 小时前
量化脚本准备实盘了吗?TqSdk 上线前工程检查
人工智能·python
暂时先用这个名字1 小时前
安装deepseek harness及插件
人工智能·ai·npm·pnpm·deepseek·深度求索·harness
青 春 记 忆2 小时前
零基础入门python07:让程序记住数据——JSON文件和异常处理
开发语言·windows·python·json·python3.11
嘟哩DuliDuli2 小时前
AI 账单变高的技术原因:重复上下文和用量归属
android·人工智能·安全·ai·软件工程
清水白石0082 小时前
Python 如何设计一个线程安全的缓存?从锁策略、LRU 到工程化实战
python·安全·缓存