跳到主要内容

所有积木共用一个接口 — 组件、Runnable 与 LCEL

这一章讲三件事: 为什么自己直接调各家 API(程序之间对话的约定通道) 会越调越乱;LangChain 用什么思路统一它们; 以及「拼积木」这件事的两种写法——手写的和声明的——差在哪。 读完你会明白:后面所有花哨的 agent 和 RAG,都是本章这几个零件拼出来的。

1. 先看现象:同一句话,两家模型各说各话

你想在一个应用里同时用 OpenAI 和 Anthropic(两家卖 LLM 服务的公司)。听起来只需要 「都发一条聊天请求」。但作者指出一个具体的坑:

两家的聊天消息格式是「微妙不兼容」的。 形式上都差不多——一条消息带个类型名和内容文本 和内容字符串——真混用在同一个对话里就立刻出错1

这不是谁写得烂,而是没有强制统一标准时必然发生的漂移。LangChain 的第一个价值主张就在这里: 它在中间垫了一层自己的规范,让「换供应商不改代码」「两家同场演出」成为可能2

2. 顶层全景

你的问题


┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ prompt 模板 │ → │ chat model │ → │ output parser │ → 结果
│ 填空用的模具 │ │ 干活的模型 │ │ 整理输出的翻译官 │
└─────────────┘ └─────────────┘ └──────────────────┘

图说:LLM 应用 = 一条链。三个部件互相不认识对方,
靠同一个统一接口对话——这是全章唯一需要记住的结构。

3. 核心原理:每个零件怎么转

3.1 两个入口:补全接口与聊天接口

LangChain 给了两套调用入口。老的一套叫 LLM 接口:一段文字进,一段文字出, 比如 model.invoke("The sky is") 返回 "Blue!"3。它简单,但只适合「一问一答不复联」。

新的一套叫 chat model(聊天模型)接口,因为主流供应商都按角色区分消息, 这套接口照着做了。三种基本角色4:

角色装什么
system给模型的指令:你是谁、该怎么答
user用户的问题和输入
assistant模型自己产出的话

消息类型的名字也一一对应:SystemMessageHumanMessageAIMessage, 外加一个任意角色的 ChatMessage5

走查①:一句系统指令如何改写行为

原书示例问「法国的首都是什么?」,但先塞了一条系统指令: 「You are a helpful assistant that responds to questions with three exclamation marks.」

输出:AIMessage('Paris!!!')6

注意指令不在用户的问题里,却在输出里兑现了。 这就是 system 角色的用途:把「行为规则」和「本次提问」分开存放—— 规则写一次,之后每个用户进来都自动生效。

3.2 旋钮:model 名、温度、max_tokens

选模型是最常配的参数——同一个供应商通常摆着一排型号,越能干的越贵越慢7, 换型号只改一个字符串。

第二个是温度(temperature):控制抽样(挑下一个词的方式)的保守程度。低温(如 0.1)输出更可预测, 高温(如 0.9)更有创造性;结构化任务用低温,写作任务用高温8。 第三个 max_tokens 限制输出长度和成本——设太低会被硬生生截断在半截话上9。 其余参数每家不同,查你所用供应商的文档即可10

3.3 提示词模板:把提示词变成可复用的模具

上一章的原书实验里出现过一份经典模板:「根据 context 回答,答不了就说 I don't know」。 问题是 context 和 question 每次都不一样,总不能复制粘贴整段文字。

LangChain 的答案就是模板:静态部分写死,动态部分留 {大括号} 空位, 调用时传入当次的值,得到一份填好的提示词。PromptTemplate 管纯文本版; ChatPromptTemplate 管带角色的版本,用 (角色, 文本) 对来声明11: 先放一条系统的规则,再留两个人类位置装 context 和 question。

原书用它跑通了一个完整问答:问题「Which model providers offer LLMs?」配上那段 NLP(自然语言处理:让机器读懂并处理人类语言的这门技术)背景文字,模型的回答点名了三家:Hugging Face(transformers 库)、 OpenAI(openai 库)、Cohere(cohere 库)12。 同一份代码里模板和模型都注明「可以反复重用」——这正是模板存在的意义。

3.4 结构化输出:逼模型交出机器可读的结果

有时你要的不是一段话,而是一个能被程序直接接手的对象——存进数据库、发给前端。 这叫结构化输出(structured output)13

做法分三步,书的例子是那个经典脑筋急转弯「一磅砖头和一磅羽毛哪个重」:

class AnswerWithJustification(BaseModel): # ← 第一步:定义 schema(数据形状说明书)
answer: str
justification: str

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
structured_llm = llm.with_structured_output(AnswerWithJustification) # ← 第二步:挂上
structured_llm.invoke("What weighs more, ...") # ← 第三步:正常调用

返回的是:{answer: "They weigh the same", justification: "…"}14

with_structured_output 幕后干两件事15:把你定义的模式转换成 JSON(一种机器易读的数据写法)Schema 形状说明书 (JSON 数据的形状描述语言)随提示词一起发给模型,由 LangChain 替你挑各家最擅长的实现路子; 等结果回来,再拿同一个模式校验——不合格当场报错, 不会把烂输出漏给你的下游代码。Python 侧用 Pydantic 写模式,JS 侧用 Zod,效果相同15

实现路子通常是函数调用(function calling),或把格式要求直接写进喂给它的话。

3.5 Output parser:轻量版的格式翻译官

如果只是想拆个逗号分隔列表,不必动用上面那套重型武器。Output parser 是一批现成的小类,做两件事:往提示词里注入格式说明;把文本输出拆解并校验成目标结构16。 例如 CommaSeparatedListOutputParser().invoke("apple, banana, cherry") 得到 ['apple', 'banana', 'cherry']17

两者的分工:要保证结构合规用结构化输出;只是拆解文本用 parser

走查②:同一个接口,三种用法

LangChain 所有组件——模型、模板、parser,乃至后面章节的检索器——都实现同一组方法18:

model.invoke('Hi there!') # 单进单出 → 'Hi!'
model.batch(['Hi there!', 'Bye!']) # 列表进列表出
for token in model.stream('Bye!'): # 流式:出一块打一块
print(token) # Good / bye / !

一次一批的 batch() 和流式的 stream() 是免费学来的技能包:学会一个组件就是学会了全部组件。 (流式示例的三段输出 Good/bye/! 是原书实拍19。)

3.6 组合的两条路线:命令式 vs 声明式

零件到手,怎么拼?

命令式(imperative):就像平时写普通代码,建一个函数把模板和模型串起来。 Python 里加 @chain 装饰器(或 JS 里用 RunnableLambda)能让任意普通函数 获得全套 Runnable 接口20。代价:想要流式就得自己改成 yield,想要不干等就得整个函数改成 async 版本21

声明式(declarative):用 LCEL(LangChain Expression Language),核心就是把 | 管道符当成「上一个的输出接到下一个的输入」:

chatbot = template | model # Python 用 |,JS 用 .pipe()
chatbot.invoke({"question": "..."})

关键差别在后头:LCEL 写的组合体开箱即得同时跑多份、边跑边出、不干等与追踪(tracing), 一行不用改——框架把它编译成调优过的执行计划,而不是老实逐句解释22。 对比表里的每一个「Automatic」,命令式那边都要你自己动手写23。 顺带说,tracing 这个词在这里第一次露面:每次运行留下可回看的执行轨迹, 这个伏笔在第 11 章(LangSmith 测试)兑现。

选择口径(作者给的):大量自定义逻辑用命令式;纯拼装现成零件用声明式24

4. 作者的判断与证据

  • 「预置常见模式」+「组件可互换」是作者给出的两条买 LangChain 理由,并且给出了判据: 先从现成模式起步,不够好再去扩展25。这是立场宣言,不是实验结论。
  • 作者用 PGVector(Postgres 装上它就能按「数与数远近」取货的插件)、Weaviate、OpenSearch 举例说明 「任何组件都能替换」并附了替换清单26——这些清单是文档性陈述,正确性以官方文档为准。
  • 埋点(callbacks 系统)、长时应用可中断/恢复/重试,这三样「接线」能力在本章只有一句预告, 分别指向观测面与长时运行能力——这两处许诺分别在第 11、09 两章兑现

5. 边界与局限

  • 书中代码今天要打折看。 本章用的是 from langchain_openai.llms import OpenAI 这条补全式老接口。 补充(不在书里,依据我们的 frontier 书架):新版 LangChain(v1 包)里已经没有这条补全式入口, 实际项目应使用 chat model 接口。依据: shelf=ai-frontier-reference/langchain@src:libs/langchain_v1/langchain/init.py:1 事实=v1 顶层只有版本声明,包内目录(agents/chat_models/embeddings/messages/tools)里不存在 llms 模块。
  • 同理,示例里的 langchain_core.pydantic_v1 兼容层和部分 import 路径属于过渡期产物, 新项目的路径以当前文档为准(补充(不在书里,来自通用知识))。
  • LCEL 的「编译成精调执行计划」在书里只是一句能力陈述,编译器内部怎么做没讲; 想深挖机制,我们的书架有更底层的一份拆解(补充(不在书里,依据我们的 frontier 书架): Runnable/LCEL 的地基设计与「为何一切都可流式」有专章分析。依据: shelf=ai-frontier-reference/langchain#03-runnable-lcel.md 事实=该章论证了 LCEL 自动获得 stream/batch/async 来自「一切皆 Runnable」的统一抽象)。
  • 书里没有任何性能数字、跑分对照;本章所有结论都属于「设计承诺」,而非实测数据。

6. 可带走的

  1. 直接调两家以上的模型 API,消息格式的微妙不兼容是常态,统一层是刚需;
  2. system/user/assistant 三个角色,本质是把「规则」与「输入」分流到不同车道;
  3. 温度管随机度:结构化任务拧低,创作任务拧高;max_tokens 太低会腰斩输出;
  4. 模板 = 静态骨架 + {占位符},一处定义处处复用;
  5. 要机器可读的结构,用 with_structured_output:schema 发过去 + 校验拦回来,双保险;
  6. 学会一个组件的 invoke/batch/stream,等于学会了所有组件;
  7. template | model 一根管道换来免费的同时跑多份/边跑边出/不干等——纯拼装场景优先 LCEL;
  8. 自定义逻辑多的地方退回普通函数(@chain/RunnableLambda 包一下照样入列);
  9. 注意:书中的部分 API 已过时,动手前对照最新官方文档校准 import 路径。

7. 原文地图

主题原书章原文位置
为什么需要 LangChain、消息不兼容实例Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:45(搜「subtly incompatible」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:15(搜「Prebuilt common patterns」)
可互换组件与替换清单Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:19(搜「Interchangeable building blocks」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:43(搜「PGVector」)
编排三连(callbacks/中断恢复预告)Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:47(搜「orchestration capabilities」)
补全接口调用实例Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:125(搜「The sky is」)
三角色定义Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:155(搜「system role」)
四种消息类型Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:174(搜「HumanMessage」)
SystemMessage 行为改写走查Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:223(搜「exclamation marks」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:247(搜「Paris」)
参数三件套Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:141(搜「trade-offs」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:147(搜「temperature」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:151(搜「truncated」)
PromptTemplate 与 f-string 占位符Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:273(搜「prompt template interfaces」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:341(搜「curly braces」)
ChatPromptTemplate 角色化模板Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:413(搜「ChatPromptTemplate」)
三家 provider 的回答Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:410(搜「transformers」)
结构化输出定义与砖羽例Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:541(搜「structured output」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:596(搜「Pydantic」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:565(搜「pound of bricks」)
with_structured_output 双重职责Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:598(搜「JSONSchema」)
output parser 两大功能Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:606(搜「format instructions」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:620(搜「cherry」)
Runnable 接口三方法Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:644(搜「common interface」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:652(搜「retries, fallbacks」)
stream 分块输出实例Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:667(搜「batch」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:5(搜「Good」)
命令式组合与 @chainChapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:744(搜「Imperative composition」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:762(搜「@chain decorator」)
手工加流式/异步Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:809(搜「streaming or async support」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:846(搜「yielding the values」)
LCEL 与管道符Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:863(搜「Declarative Composition」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:883(搜「operator」)
LCEL 自动化的能力清单Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:865(搜「optimized execution plan」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:726(搜「Parallel execution」)
两种风格的选择口径Chapter 1text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:958(搜「chain consisting of」) · text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:962(搜「custom logic」)

Footnotes

  1. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 45 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:45,搜「subtly incompatible」)。原文判断:「if you try to use both models in the same conversation, you'll immediately run into issues」。

  2. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 21 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:21,搜「future-proof」)。「换供应商或升级能力时不需要整体重写应用」即此段的措辞。

  3. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 115 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:115,搜「string prompt」);调用与输出见第 125 与 137 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:137,搜「Blue」)。

  4. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 155–167 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:157,搜「System role」)。

  5. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 195–211 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:211,搜「arbitrary setting of role」)。

  6. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 213–249 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:247,搜「Paris」);行为解读引第 249 段(搜「preconfigure your AI application」)。

  7. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 141 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:141,搜「more expensive and slower」)。

  8. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 145–147 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:147,搜「sampling algorithm」)。

  9. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 149–151 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:151,搜「truncated」)。

  10. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 153 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:153,搜「different set of parameters」)。

  11. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 341 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:341,搜「f-string syntax」)与第 413 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:413,搜「role of the chat message」)。

  12. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 408–411 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:410,搜「transformers」)。

  13. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 541 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:541,搜「machine-readable format」)。

  14. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 554–594 段,输出在第 586 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:586,搜「pound of feathers」)。

  15. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 596–600 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:598,搜「best method to do this」)。schema 校验职责在第 600 段(搜「respects the schema」)。 2

  16. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 604–612 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:604,搜「two functions」)。

  17. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 618–632 段,输出在第 632 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:632,搜「banana」)。

  18. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 642–656 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:646,搜「invoke: transforms a single input」)。内置工具(retry/fallback/schema/运行时配置)在第 652 段。

  19. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 667–693 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:671,搜「print(token)」)。

  20. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 707–716 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:707,搜「Imperative」);装饰器注释在第 764 段(搜「adds the same Runnable interface」)。

  21. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 809–846 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:846,搜「yield」)。

  22. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 863–865 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:865,搜「automatic parallelization」)。

  23. 出处:「Chapter 1. LLM Fundamentals with LangChain」表 1-1 所在段:text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:717(搜「main differences between imperative and declarative」)。

  24. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 962 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:962,搜「custom logic」)。

  25. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 17 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:17,搜「out of the box are good enough」)。

  26. 出处:「Chapter 1. LLM Fundamentals with LangChain」第 29–43 段(text/04-ch01-chapter-1-llm-fundamentals-with-langchain.txt:35,搜「Anthropic as a commercial alternative」)。