(二)消息、提示与一条可运行的链
1. 输入与输出带有角色
前面的回归模型接收数值数组,返回预测值。聊天模型通常接收消息序列,返回一条模型消息。HumanMessage 保存本轮用户输入,AIMessage 保存模型回答或工具调用请求,ToolMessage 保存工具执行结果;system 消息用于说明任务要求。
模型读到的是这次调用实际传入的上下文。上一次请求存在数据库里,并不意味着下一次调用会自动读取它;程序需要决定取哪些历史消息、检索材料和当前问题一起传入。
token 是模型处理文本时使用的切分单位,与 Python 字符、中文汉字、英文单词没有固定的一一关系。上下文窗口容纳输入消息和生成内容;工具说明与检索材料也占空间。记录模型返回的 token 用量,比用字符数直接当作 token 数更合适。
2. 逐步组合提示、模型与解析器
下面先整理一条阅读安排。Pydantic 的 BaseModel 已在 FastAPI 中使用过;这里用它说明模型输出应包含哪些字段。
ChatPromptTemplate 负责用变量填充带角色的提示;FakeListChatModel 按顺序返回准备好的文本;PydanticOutputParser 把模型消息中的 JSON 解析成对象。它们分别来自 langchain_core 的提示、回放模型和解析器模块,完整导入路径见代码开头。
"""把提示、消息模型和字段解析接成同一条可运行流程。"""
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel
class ReadingReply(BaseModel):
title: str
minutes: int
# 模型回放固定文本,用来观察程序传递;不在这里训练或调用生成模型。
parser = PydanticOutputParser(pydantic_object=ReadingReply)
prompt = ChatPromptTemplate.from_messages([
("system", "把阅读安排整理成指定格式。\n{format}"),
("human", "阅读《{title}》{minutes} 分钟。"),
]).partial(format=parser.get_format_instructions())
model = FakeListChatModel(responses=['{"title":"Python 笔记","minutes":35}'])
# | 的两边是 Runnable 对象,前一步输出会作为后一步输入。
chain = prompt | model | parser
inputs = {"title": "Python 笔记", "minutes": 35}
# 先展开三步;to_messages 将提示值转换成带角色的消息列表。
filled = prompt.invoke(inputs)
print([(message.type, message.content) for message in filled.to_messages()])
message = model.invoke(filled)
print(type(message).__name__, message.content)
parsed = parser.invoke(message)
assert parsed.minutes == 35
# 再执行组合后的链,核对它与展开执行得到同样的对象。
reply = chain.invoke(inputs)
assert reply == parsed
print(reply.model_dump())
# 流式模型逐段交付消息;这里的回放模型每次交付一个字符。
parts = [piece.content for piece in model.stream("展示回放输出")]
assert "".join(parts) == '{"title":"Python 笔记","minutes":35}'partial 先填入不会随本次问题变化的格式说明;invoke 再填书名和时长。提示对象的输出进入模型,模型消息再进入解析器,最后得到 ReadingReply。打印结果为 {'title': 'Python 笔记', 'minutes': 35}。
沿着展开的三步读:filled 是 ChatPromptValue,其中含 system 和 human 两条消息;filled.to_messages() 将它们取成列表。human 消息中的变量已经变成“阅读《Python 笔记》35 分钟。”。model.invoke(filled) 返回 AIMessage,其 content 仍是 JSON 文本;parser.invoke(message) 才把这份文本变成带字段的 Python 对象。
| 执行位置 | 此时拿到的数据 | 下一步读取的内容 |
|---|---|---|
| 调用提示前 | {"title": "Python 笔记", "minutes": 35} |
提示中的同名变量 |
| 提示填充后 | system:任务与输出格式;human:已填入书名和时长 | 模型读取消息内容 |
| 模型返回后 | AIMessage.content 中的 JSON 字符串 |
解析器读取字段与类型 |
| 解析完成后 | ReadingReply(title="Python 笔记", minutes=35) |
程序读取 reply.minutes |
这里的 | 是 Runnable 对象提供的组合操作:先执行左边,再把结果交给右边。它沿用了 Python 的运算符语法,处理的是这些对象定义的行为。直接用字符串、普通函数照写同一个表达式,并不会自动建立一条 LangChain 链。
Runnable 可以先理解为“有统一调用入口的一个处理步骤”。模型、提示和解析器各自的实现不同,但都能接收一次输入并给出下一步需要的输出。组合后的 chain 也有 invoke,因此能够继续与别的步骤组合。对照代码中的断言,分步调用与一条链得到的 ReadingReply 相同。
stream 返回逐段消息,invoke 返回完整结果。回放示例逐字符输出是为了观察过程;真实模型每次交付的片段长度由模型与服务决定,不必与一个 token 完全对应。模型调用与消息