本文旨在从实际开发场景出发,系统介绍 LangChain 中提示词模板(PromptTemplate)、多角色对话管理(ChatPromptTemplate / MessagesPlaceholder)、少样本学习(Few-Shot)及结构化输出控制的核心用法,从而帮助开发者快速构建高质量、可维护的 AI 应用。

一、环境准备与模型配置

在实际生产环境中,我们通常需要对接多个大模型(如本地私有化模型、智谱 GLM、ModelScope 等),同时必须确保 API Key 等敏感信息的安全性。本章将搭建一套模块化、可扩展的环境配置体系。

1.1 项目依赖配置

首先创建 pyproject.toml 文件,定义项目及核心依赖。我们使用 uv 作为包管理工具,以提升依赖安装速度。

[project]
name = "langchain-structured-output-guide"
version = "0.1.0"
description = "LangChain 提示词工程与结构化输出实战指南"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "langchain>=1.2.10",
    "langchain-community>=0.4.1",
    "langchain-openai>=1.1.10",
    "pydantic>=2.0", # 配合结构化输出使用
    "python-dotenv>=1.0.0", # 用于加载 .env 文件
]

执行以下命令完成环境同步与依赖安装:

uv sync

1.2 环境变量配置

在项目根目录下创建 .env 文件,集中管理不同模型的 Key 和 Base URL。

安全提示:请务必将 .env 文件加入 .gitignore,防止密钥泄露。

# .env

# --- 本地私有化部署模型 (如 Ollama) ---
# LOCAL_API_KEY 通常为空,Ollama 无认证;如使用带鉴权的本地服务,请填写
LOCAL_API_KEY=
LOCAL_BASE_URL=http://127.0.0.1:11434/v1
LOCAL_MODEL=qwen3

# --- 智谱 AI (GLM) ---
GLM_API_KEY=sk-xxx
GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
GLM_MODEL=glm-4

# --- ModelScope (魔搭社区/多模态) ---
MT_API_KEY=ms-xxx
MT_BASE_URL=https://api-inference.modelscope.cn/v1
MT_MODEL=qwen-omni-3b

# --- DeepSeek ---
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat

1.3 工具类与模型工厂

为了在代码中消除硬编码,避免全局变量满天飞,我们采用工厂模式来统一管理 LLM 实例。

文件 1:env_utils.py
负责安全加载环境变量。

import os
from dotenv import load_dotenv

# 加载 .env 文件,override=True 使 .env 中的值覆盖系统已有的同名环境变量
load_dotenv(override=True)

_UNSET = object()

def get_env(key: str, default: str = _UNSET) -> str:
    """安全获取环境变量,未配置且无默认值时抛出异常"""
    value = os.getenv(key)
    if value is None:
        if default is not _UNSET:
            return default
        raise ValueError(f"缺少环境变量配置: {key}")
    return value

文件 2:models.py
封装不同模型的初始化逻辑,实现按需调用和统一管理。

from langchain_openai import ChatOpenAI
from env_utils import get_env

def get_local_llm(temperature: float = 0.8) -> ChatOpenAI:
    """获取本地部署的 LLM (支持自定义 Thinking 参数)"""
    return ChatOpenAI(
        model=get_env("LOCAL_MODEL"),
        temperature=temperature,
        api_key=get_env("LOCAL_API_KEY"), 
        base_url=get_env("LOCAL_BASE_URL"),
        extra_body={'chat_template_kwargs': {'enable_thinking': False}},
    )

def get_glm_llm(temperature: float = 0.5) -> ChatOpenAI:
    """获取智谱 GLM 模型"""
    return ChatOpenAI(
        model=get_env("GLM_MODEL"),
        temperature=temperature,
        api_key=get_env("GLM_API_KEY"),
        base_url=get_env("GLM_BASE_URL"),
    )

def get_multimodal_llm() -> ChatOpenAI:
    """获取多模态 LLM (如 ModelScope)"""
    return ChatOpenAI(
        model=get_env("MT_MODEL"),
        api_key=get_env("MT_API_KEY"),
        base_url=get_env("MT_BASE_URL"),
    )

# 默认导出一个常用的 LLM 实例(简化后续调用代码)
llm = get_local_llm()

1.4 快速开始

配置完成后,我们就可以在业务代码中优雅地导入并使用这些模型了:

from models import llm 
from langchain_core.prompts import ChatPromptTemplate

# 使用默认的本地模型
prompt = ChatPromptTemplate.from_template("讲一个关于{topic}的冷笑话。")

# 这里的 | 是 LangChain 的管道操作符(LCEL),用于将组件串联成处理链
chain = prompt | llm

response = chain.invoke({"topic": "程序员"})
print(response.content)

二、提示词模板——从变量替换到灵活拼接

在实际开发中,直接在代码中硬编码字符串提示词会导致难以维护和复用。LangChain 的 PromptTemplate 提供了参数化管理能力,能够有效解决这一痛点。

2.1 PromptTemplate:变量占位符与复用

业务场景:假设你需要为一个内容平台生成不同主题的文案,核心句式保持不变,仅需替换关键词。

核心技术:使用 {variable} 语法定义占位符,通过 .invoke() 方法在运行时动态注入数据。

from langchain_core.prompts import PromptTemplate

# 定义模板:使用大括号 {} 标记变量位置
prompt_template = PromptTemplate.from_template(
    "请为以下主题生成一段简短的宣传文案:{topic}。\n要求风格:{style}。"
)

# 调用模板:传入字典格式的变量值
prompt_value = prompt_template.invoke({"topic": "人工智能", "style": "科技感"})
print(prompt_value)
# 输出: 请为以下主题生成一段简短的宣传文案:人工智能。
#       要求风格:科技感。

原理解析

  • 一次定义,多次复用:通过将提示词逻辑与具体数据分离,实现了模板的通用性。
  • 类型安全invoke 方法返回的是一个 StringPromptValue 对象,而非普通字符串,这使得它能无缝接入 LangChain 的 LCEL(LangChain Expression Language)处理管道。

2.2 模板拼接:构建组合式提示词

业务场景:基础模板往往需要配合特定的约束条件(如“输出语言”、“长度限制”)。拼接机制允许我们在不修改基础模板的前提下,动态追加指令。

核心技术:利用 + 运算符组合模板,LangChain 会自动处理类型转换,将其整合为新的 ChatPromptTemplate

from langchain_core.prompts import PromptTemplate
from models import llm # 已在上文 1.3 节配置

# 基础任务指令
base_prompt = PromptTemplate.from_template(
    "请生成一个关于 {topic} 的简短介绍。"
)

# 动态追加约束(支持多段拼接),类似于字符串拼接
constraint_prompt = (
    base_prompt 
    + "\n\n约束条件:"
    + "\n1. 语言必须使用 {language}。"
    + "\n2. 字数不超过 50 字。"
)

# 构建 LCEL 链路
chain = constraint_prompt | llm

# 执行并传入所有必要变量
response = chain.invoke({"topic": "量子计算", "language": "中文"})
print(response.content)

最佳实践
在复杂的业务系统中,建议将“系统指令”、“少样本示例”和“用户问题”拆分为独立的模板片段。通过在运行时动态组装这些片段,我们可以极大地提高代码的可维护性,并在不同场景下灵活复用通用的提示词组件,避免重复造轮子。

三、聊天模板——构建多角色对话体系

随着大模型从早期的文本补全(Text Completion)模式全面转向对话交互模式,提示词的设计逻辑也发生了变化:我们不再仅仅拼接一段长文本,而是需要通过明确的“消息角色”来构建结构化的上下文。这种角色区分让模型能更清晰地理解指令的层级(如系统设定 vs. 用户输入),从而生成更符合预期的回复。

3.1 ChatPromptTemplate:多角色消息定义

业务场景:你需要设定一个具有特定人设的 AI 助手,要求它严格扮演特定角色进行回答。

核心技术:使用 ChatPromptTemplate.from_messages 定义消息列表,精确控制每个角色的上下文。

from langchain_core.prompts import ChatPromptTemplate

# 定义结构化聊天模板
chat_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的 {role},请用专业且客观的语气回答问题。"),
    ("user", "用户提问:{question}")
])

# 调用时生成包含角色信息的消息列表
messages = chat_template.invoke({"role": "金融分析师", "question": "当前市场趋势如何?"})
print(messages)
# 输出对应的消息对象列表,而非纯字符串

3.2 MessagesPlaceholder:动态对话历史容器

业务场景:在多轮对话中,历史记录的长度是不确定的。我们无法在模板中写死所有的 userassistant 消息。

核心技术MessagesPlaceholder 充当通配符,允许在运行时动态插入整个消息历史列表。

from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from models import llm # 已在上文 1.3 节配置

prompt_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个智能助手。"),
    MessagesPlaceholder("history"),  # 占位符:用于注入历史对话列表
    ("user", "{input}")              # 占位符:当前用户输入
])

chain = prompt_template | llm

# 模拟多轮对话历史
history = [
    HumanMessage(content="我叫小明"),
    AIMessage(content="你好小明,很高兴认识你。")
]

# 执行链路,传入历史和当前问题
response = chain.invoke({
    "history": history,
    "input": "我叫什么名字?"
})
print(response.content)

核心价值
这是构建具备“记忆能力” AI 应用的基础,它能完美适配 LangChain 的 RunnableWithMessageHistory 组件,让应用轻松处理多轮对话上下文,记住之前的交互内容。

四、少样本学习——通过示例引导模型推理

对于复杂的逻辑任务,单纯的语言指令往往效果有限。通过提供“示例”,我们可以像教学一样引导模型模仿特定的推理过程和输出风格,这就是少样本学习(Few-Shot Learning)。

4.1 FewShotPromptTemplate:文本模式的思维链

业务场景:构建一个推理问答系统,要求模型展示逐步推理过程,而不是直接给出答案。

核心技术:将示例打包为 PromptTemplate,并通过 FewShotPromptTemplate 进行格式化拼接,形成思维链。

from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from models import llm 

# 定义示例集(包含思维链 CoT)
examples = [
    {
        "question": "史蒂夫·乔布斯和艾伦·图灵谁活得更久?",
        "answer": """
1. 查询史蒂夫·乔布斯:生于1955年,卒于2011年,享年56岁。
2. 查询艾伦·图灵:生于1912年,卒于1954年,享年41岁。
3. 比较结果:56 > 41。
最终答案:史蒂夫·乔布斯。
"""
    },
    {
        "question": "乔治·华盛顿的外祖父是谁?",
        "answer": """
是否需要后续问题:是。
后续问题:乔治·华盛顿的母亲是谁?
中间答案:乔治·华盛顿的母亲是玛丽·鲍尔·华盛顿。
后续问题:玛丽·鲍尔·华盛顿的父亲是谁?
中间答案:玛丽·鲍尔·华盛顿的父亲是约瑟夫·鲍尔。
所以最终答案是:约瑟夫·鲍尔
""",
    }
]

# 定义单个示例的展示格式
example_prompt = PromptTemplate(
    input_variables=["question", "answer"],
    template="问题: {question}\n{answer}"
)

# 构建少样本提示词模板
few_shot_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    suffix="问题: {input}",  # 真正的用户问题接在示例后面
    input_variables=["input"]
)

chain = few_shot_prompt | llm
response = chain.invoke({"input": "对比一下爱因斯坦和牛顿的寿命。"})
print(response.content)

4.2 FewShotChatMessagePromptTemplate:对话模式的示例注入

业务场景:在聊天场景中,教模型执行一个特殊的自定义指令(例如自定义算术运算符)。

核心技术:使用聊天消息格式构建示例,并将其作为一部分嵌入到 ChatPromptTemplate 中。

from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser
from models import llm 

# 定义示例:教模型识别 '🦜' 为乘法
examples = [
    {"input": "3 🦜 3 等于多少?", "output": "9"},
    {"input": "5 🦜 6 等于多少?", "output": "30"},
]

# 示例格式化模板
example_prompt = ChatPromptTemplate.from_messages([
    ("human", "{input}"),
    ("ai", "{output}"),
])

# 构建少样本提示词对象
few_shot_prompt = FewShotChatMessagePromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
)

# 组装最终提示词:System + FewShot Examples + User Input
final_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个数学助手,请根据用户示例回答问题。"),
    few_shot_prompt,
    ("human", "{input}")
])

chain = final_prompt | llm | StrOutputParser()
response = chain.invoke({"input": "2 🦜 9 等于多少?"})
# 输出: 18
print(response)

Few-shot 示例仅用于引导模型理解特定指令的含义(如上面的自定义运算符),不会改变模型的通用能力,但回答风格可能会受到示例格式的影响。

五、结构化输出——确保数据格式的规范性

将大模型生成的非结构化文本转化为可被程序直接读取的结构化数据(如 JSON/Pydantic 对象),是 AI 应用落地的关键一步。

5.1 方案一:with_structured_output(推荐)

业务场景:提取用户反馈中的情感、主题和关键实体,并直接映射为代码对象以便后续处理。

核心技术:利用 Pydantic 定义强类型数据模型,LangChain 会自动处理提示词增强和输出解析,确保模型返回的数据符合定义。

from typing import List
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from models import llm 

# 1. 定义强类型数据模型
class UserFeedback(BaseModel):
    """用户反馈的结构化数据"""
    sentiment: str = Field(description="情感倾向:positive/negative/neutral")
    topics: List[str] = Field(description="反馈涉及的主题列表,如['价格', '服务']")
    summary: str = Field(description="一句话总结反馈内容")

# 2. 配置 LLM 使用结构化输出
# 注意:并非所有模型都原生支持此功能,需模型具备 Tool Calling 能力
structured_llm = llm.with_structured_output(UserFeedback)

# 3. 构建调用链
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的客服数据分析助手。"),
    ("human", "请分析以下用户反馈:\n{feedback_text}")
])

chain = prompt | structured_llm

# 4. 执行
response = chain.invoke({"feedback_text": "这就离谱!价格太贵了,而且客服响应还很慢。"})

# 5. 结果已是 Pydantic 对象,支持 IDE 自动补全和类型校验
print(response.sentiment) # negative
print(response.topics)    # ['价格', '客服']

5.2 方案二:SimpleJsonOutputParser(轻量级)

业务场景:仅需简单的键值对输出,且使用的模型可能不支持原生 Function Calling。

核心技术:在提示词中显式要求 JSON 格式,并使用解析器清洗模型输出的 Markdown 代码块等杂质。

from langchain_core.output_parsers import SimpleJsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from models import llm 

prompt = ChatPromptTemplate.from_template(
    "回答用户问题。输出必须为严格的 JSON 格式,包含 'answer' 和 'confidence' (0-1) 两个键。\n"
    "问题:{question}"
)

# 使用解析器包装 LLM,自动处理输出格式
chain = prompt | llm | SimpleJsonOutputParser()

response = chain.invoke({"question": "地球是圆的吗?"})
# 输出示例: {'answer': '是的,地球是圆的。', 'confidence': 0.99}
print(response)

SimpleJsonOutputParser() 会从模型输出的文本中提取 JSON 部分(自动去除 Markdown 代码块包裹等杂质),并解析为 Python dict 返回。

5.3 方案三:bind_tools(底层机制)

业务场景:需要更精细地控制工具调用参数,或模拟特定的 API 响应格式。

核心技术:将 Pydantic 模型绑定为工具,强制模型通过工具调用的方式返回结果,适合 Agent 开发。

from pydantic import BaseModel, Field
from models import llm 

# 定义工具对应的 Schema
class ResponseFormat(BaseModel):
    """用于格式化回答的工具"""
    answer: str = Field(description="最终答案")
    source: str = Field(description="信息来源")

# 绑定工具到 LLM
llm_with_tools = llm.bind_tools([ResponseFormat])

resp = llm_with_tools.invoke("中国的首都在哪里?")

# 提取工具调用参数
if resp.tool_calls:
    print(resp.tool_calls[0]['args'])
    # 输出: {'answer': '北京', 'source': '地理常识数据库'}

三种方案如何选择?

  • 优先使用 with_structured_output:开发体验最好,自动完成提示词增强、输出解析和类型校验,适合绝大多数场景。
  • 模型不支持 Tool Calling 时退而求其次用 SimpleJsonOutputParser:只需在提示词中要求 JSON 格式即可,兼容性最广。
  • 需要精细控制工具调用行为时使用 bind_tools:适合 Agent 开发或需要自定义工具调用逻辑的高级场景。

总结:技术选型决策矩阵

技术组件 核心能力 典型应用场景 优势
PromptTemplate 字符串变量替换 单次文本生成、简单指令 轻量、直观、易于上手
ChatPromptTemplate 多角色消息管理 聊天机器人、人设设定 结构清晰,符合对话模型逻辑
MessagesPlaceholder 动态历史注入 带记忆的多轮对话 灵活性高,支持可变长度历史
FewShotPromptTemplate 上下文学习 复杂推理、特定风格模仿 显著提升复杂任务准确率
with_structured_output 强类型结构化输出 数据提取、RAG 检索重构 开发体验最佳,自动解析与校验
SimpleJsonOutputParser 文本转 JSON 轻量级信息获取 兼容性好,适用于非 Tool Calling 模型
bind_tools 原生工具调用 Function Calling、Agent 开发 底层控制力强,与生态深度集成

写在最后

良好的提示词工程是确保大模型稳定输出的基石。通过将提示词模块化、结构化并规范输出格式,我们可以更精准地控制模型行为,为后续构建复杂、可靠的 AI 应用打下坚实的基础。

本文是 LangChain 系列的第一篇,后续将继续探讨多模态交互RAG 检索增强生成等进阶主题,欢迎关注。

如有疑问或建议,欢迎留言讨论!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐