LangChain.js 第一步:模型 IO 与流式
实战锚点 · cooking-app:对照裸 OpenAI SDK 代码,理解 ChatModel、流式输出与结构化输出。
在碰任何框架之前,先看懂裸 API 长什么样——因为 LangChain 全部是对这一层的包装。
从 30 行裸代码开始
下面是一个真实开源项目 easy-agent 的 Step 1——一个从零重造 Claude Code 的项目,这 30 行就是 LLM API 的全部本质:
import Anthropic from "@anthropic-ai/sdk";
export async function* streamMessage({ messages, system, tools }) {
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_AUTH_TOKEN });
const stream = client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages, // 消息数组:[{ role, content }]
stream: true,
...(system ? { system } : {}),
...(tools?.length ? { tools } : {}),
});
for await (const event of stream) {
// 每个事件 yield 给 UI 层渲染
yield event;
}
}
如果你之前用过 DeepSeek 或 OpenAI 的 API,你会发现格式几乎一样——因为整个行业都兼容 OpenAI 协议。换一个模型供应商,改的只是 URL 和 Key。
消息数组:唯一的输入格式
模型只认一种输入:消息数组。每条消息有一个角色和一段内容:
messages: [
{ role: "system", content: "你是烹饪助手…" }, // 人设/规则(权重最高)
{ role: "user", content: "鸡胸肉怎么做嫩?" }, // 用户问的
{ role: "assistant", content: "低温慢煮…" }, // 模型上次答的
{ role: "user", content: "温度设多少?" }, // 用户追问的
]
四个知识点碎开来说:
| 角色 | 谁在说话 | 类比 |
|---|---|---|
system | 你写给模型的设定 | 函数的隐式约定——不对用户展示,但影响所有输出 |
user | 用户 | 函数的入参 |
assistant | 模型 | 函数的返回值(也会成为下一轮的入参) |
tool | 工具执行结果 | 外部数据回填——后面工具调用章会讲 |
模型没有记忆。每次调用都是独立的——所谓「多轮对话」,只是把之前所有消息重新发一遍。这就是为什么长对话越来越贵(Dify 篇讲过的 token 账单)。
流式输出:不是一个字一个字「下载」
你看到的打字机效果,不是前端在做动画,是模型本来就是一个 token 一个 token 生成的(认知地图的 LLM 原理 3 分钟讲过这个)。
流式 API 返回的不是完整回答,而是一串事件流:
message_start → 开始了,这是 message ID
content_block_start → 第一个内容块开始了(text 类型)
content_block_delta → 增量文字:"今"… "天"… "北"… "京"…
content_block_stop → 这个块结束了
message_delta → 快结束了,stop_reason 来了
message_stop → 完整回答结束
easy-agent 的 Step 1 用一个 async generator 处理这些事件,每收到一个 text_delta 就 yield 给终端 UI 渲染——这就是所有流式 UI 的底层原理。
content block:回答不止是文字
模型的回答不是一个字符串,是一个内容块数组:
content: [
{ type: "text", text: "让我查一下天气…" }, // 文字块
{ type: "tool_use", id: "toolu_01", name: "getWeather", input: { city: "北京" } }, // 工具调用块
]
当模型想调用工具时,它不会说「请帮我调用 getWeather」,而是输出一个结构化的 tool_use 块。你的代码执行工具后,把结果作为 tool_result 塞回消息数组,再次调用模型。
这个循环就是 Agent 的核心——下一章会拆开讲。
那 LangChain 到底包装了什么?
现在你能自己回答这个问题了:
| 裸 API(你刚看的) | LangChain 的包装 |
|---|---|
| 手动拼 messages 数组 | ChatPromptTemplate 帮你拼 |
| 手动处理 stream 事件 | .stream() 返回一个好用的 async iterator |
| 手动解析 content blocks | 自动提取文字 / 工具调用 |
| 换模型供应商要改代码 | ChatOpenAI / ChatAnthropic 统一接口 |
LangChain 的价值不是「更强」,是「更省事」——但如果你不知道底下在发生什么,框架出问题时就抓瞎。
本章要点
- 模型 API = 发一个 POST,传消息数组,拿回复写
- 消息数组是唯一输入格式,四种角色各司其职
- 模型没有记忆,多轮对话 = 全量历史重发
- 流式 = 逐 token 事件流,不是前端动画
- content block 不止文字,还有 tool_use——这是 Agent 的起点
→ 下一章:工具调用——Agent 的「手」