LangChain 从入门到实战(03):别再用字符串拼提示词——多消息、few-shot 与结构化输出
上一篇我们用三个最小件跑通了第一次真实调用,也埋了根线:from_template生成的是单条用户消息。这一篇把这根线拆开——ChatPromptTemplate真正能上生产的三种写法:多角色消息(system/human)、few-shot 少样本示例、绑定结构化输出(JSON)。让提示词从「一段写死的字符串」变成「一个可被管理、复用、进生产的组件」。
老规矩,本篇代码都能直接跑,并附在每段后的「跑起来你会看到」。
一、单条消息的局限:你需要「系统」这个角色
上一篇的from_template只能生成一条用户消息。但真实生产里,一段合格的提示词往往是「先立规矩 + 再提问」:
- system 消息:设定模型的身份、语气、边界(「你是资深 Java 后端架构师,只回答与代码相关的问题」);
- human 消息:用户真正的问题。
from_messages就是干这个的,它能按顺序放多个角色:
fromlangchain_core.promptsimportChatPromptTemplate ckpt=ChatPromptTemplate.from_messages([("system","你是资深{role}工程师。回答要精炼,不超过3条。"),("human","{question}"),])chain=(ckpt|llm|parser)# llm、parser 沿用第02篇print(chain.invoke({"role":"Java 后端","question":"@Transactional 失效的三种场景?"}))跑起来你会看到:先有身份设定,再答,方向不会跑偏。把「立规矩的 system」和「用户输入 human」分开管,是生产提示词的第一条分水岭。
二、few-shot:给模型「照着写」的范本
有时光靠 system 描述一件事不够,模型还是不知道你要的格式。最有效的办法是给几个「输入 → 正确输出」的样例,让模型照着写——few-shot,少样本示例。
fromlangchain_core.promptsimportFewShotChatMessagePromptTemplate,ChatPromptTemplate examples=[{"question":"1+1 等于几?","answer":"2"},{"question":"2*3 等于几?","answer":"6"},]few=FewShotChatMessagePromptTemplate(example_prompt=ChatPromptTemplate.from_messages([("human","{question}"),("ai","{answer}"),]),examples=examples,)final=ChatPromptTemplate.from_messages([("system","你是一个只回数字的算术助手。"),few,("human","{question}"),])result=final.invoke({"question":"4*5 等于几?"})print(result)跑起来你会看到最后一条 human 是用户提问,前面带着两组示例做「锚定」。few-shot 的价值是「用节奏带动模型对齐格式」,它比千言万语更管用——模型直接抄格式,几乎没有发挥空间。
三、结构化输出:让链直接吐出 JSON(不进生产的关键一步)
前面两步都是「输入侧」。要到生产,输出侧必须结构化。用JsonOutputParser,模型就直接返回一个合规 JSON:
fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportJsonOutputParser parser_json=JsonOutputParser()prompt=ChatPromptTemplate.from_messages([("system","你是货币换算助手。"),("human","把金额 {amount} usd 转成人民币,四舍五入到元。\n{format_instructions}"),])# 把 JSON 格式约束注入成模板变量 {format_instructions}prompt=prompt.partial(format_instructions=parser_json.get_format_instructions())chain=prompt|llm|parser_json result=chain.invoke({"amount":12.5})print(result)# 直接就是 dict,例:{'amount_cny': 90}这里的两个关键点:①partial必须在模板里留{format_instructions}这个占位符,get_format_instructions()生成的「请按下面 JSON 结构返回」约束才注得进去;②invoke传入的键要和模板里的变量名严格对齐(这里是amount),多传、少传或拼错都会报 KeyError。
JsonOutputParser内部已经帮你做了json.loads,所以你拿到的直接是 Pythondict,不再是需要re去抠的一堆字符串——这在 Java 侧相当于你在服务里直接拿到Map<String, Object>而不是自己去 parse 文本。
对照 Java 视角:这个输出「强类型化」的过程,就是String→Map再 → 「Pydantic Model」的演进。想再进一步,用PydanticOutputParser能直接锚定到一个class上去——而v0.3+ 更主流也更少踩坑的写法是llm.with_structured_output(PydanticModel),一行就把「让模型按 schema 输出 + 反序列化成对象」两步合并了,我们第 05 篇会专门展开。
四、工程化对照(Java 视角)
- 模板即配置:
from_messages把提示词和业务代码分离,像 Java 把 SQL / 文案外置到资源文件,改文案不用动代码。 - few-shot 即「样例驱动开发」:调试模型行为,本质是「加样例」而不是「改逻辑」,跟对着测试用例改实现一个思路。
- 结构化输出即「对账」:
JsonOutputParser保证下游是强类型,少一层「文本→对象」的反序列化==少一类 parser 崩溃。
小结
| 能力 | 解决的问题 | 白话 |
|---|---|---|
from_messages(多角色) | 提示词只能「一条」 | 把「规矩」和「提问」分开 |
few-shot少样本 | 模型不知道你要什么格式 | 给 2~3 个例子,让它「照抄格式」 |
JsonOutputParser | 输出是一坨没法处理的文本 | 直接反射成 JSON / dict |
下一篇预告:这一篇我们把提示词打磨成组件,下一关是「模型」——第04篇《模型适配:一套代码,接所有 Provider》——把 API Key、base_url、模型名这些「会变的东西」做成可配置,让 LangChain 链能在一个项目里同时换 OpenAI、Zhipu、DeepSeek 多个供应商而业务代码零改动。