Appearance
把对话做对 · 流式输出与多轮消息
上一节我们让模型"说了一句话"。但真实产品里,对话要像聊天一样一段一段地出字(流式),而且要记得前面聊过什么(多轮)。这一节把这件事做扎实。
本节基于环境与依赖搭好的最小工程继续。
1. 从"一句话"到"对话上下文"
先理解 LangChain 里消息的模型:一切对话都是消息列表。
你: "你好"
模型: "你好呀"
你: "你会什么?"
模型: "我会……"模型本身没有记忆,它只是吃一个"消息数组"并续写下一个。所以"多轮"这件事,本质是把所有历史消息都塞给它。
我们手工模拟一下两轮对话:
js
// src/index.js —— 手动拼消息,感受"上下文"
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
})
const messages = [
{ role: 'system', content: '你是一个耐心的助手,回答要简洁。' }, // 系统设定(可选)
{ role: 'user', content: '我叫小明,我喜欢猫。' },
{ role: 'assistant', content: '好的小明,我记住你喜欢猫啦!' }, // 上一轮模型说的
{ role: 'user', content: '我喜欢什么动物?' }, // 新问题
]
const reply = await model.invoke(messages) // ← invoke 可以吃一整个消息列表
console.log(reply.content) // 应该会回答: 猫核心变化只有一个:model.invoke() 的参数从一个字符串,变成了消息数组。 这就是多轮的全部秘密。
注意:消息里的
role有system(系统)/user(用户) /assistant(模型,上一轮回复)。其中 assistant 消息一般由代码填回去,不是人写的。
2. 流式输出:让对话像打字机一样蹦字
用户点发送后,整段话慢慢浮现(而不是等 5 秒一把出来),是对话产品的基本体验。LangChain 做流式只改一个词:
js
// src/index.js —— 流式版本
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
})
const stream = await model.stream('讲一个 20 字的笑话') // ★ stream 而非 invoke
for await (const chunk of stream) {
process.stdout.write(chunk.content ?? '') // 一个词一个词往外吐
}
console.log('\n--- 流式结束 ---')model.invoke()→ 一次性返回完整内容;model.stream()→ 返回一个异步迭代器,逐块吐字。
跑一下就能看到打字机效果。后续做工具调用、RAG 时,大部分也可以流式,只是要注意"当有工具调用时,中间流出的不是文字而是工具指令",后面篇会讲正确姿势。
3. 最小"聊天会话"封装
把上面两件事(记忆 + 流式)合起来,做一个像样的"命令行聊天机器人":
js
// src/index.js —— 你第一个可交互 Agent
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
import readline from 'node:readline/promises'
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
temperature: 0.8,
})
const history = [] // 记录所有消息,这就是"记忆"
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
console.log('🤖 开始聊天吧(输入 bye 退出)')
while (true) {
const user = await rl.question('你: ')
if (user === 'bye') break
history.push({ role: 'user', content: user }) // 1. 记住用户的话
const stream = await model.stream(history) // 2. 带着全部历史去问
process.stdout.write('AI: ')
let answer = ''
for await (const chunk of stream) {
if (chunk.content) {
process.stdout.write(chunk.content)
answer += chunk.content
}
}
console.log()
history.push({ role: 'assistant', content: answer }) // 3. 记住模型的回复
}
rl.close()试一下对话:
bash
npm run dev
# 你: 我叫小王
# 我是你刚认识的朋友, 你好呀
# 你: 我刚才叫什么名字?
# 小王!…这就是"记忆"的第一个朴素实现——把所有消息存进一个数组,每次全量发给模型。够用吗? 小规模可以,但用久了会很费 token、且重启就丢。真正的记忆管理(裁剪/摘要/持久化)留给记忆与多轮那篇。
4. 用 Vercel AI SDK 怎么写(极简对照)
如果你更喜欢轻量路线,同样的事 AI SDK 只用了几个函数:
bash
npm install ai @ai-sdk/openaijs
// ai-sdk 版
import 'dotenv/config'
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
const result = streamText({
model: openai(process.env.MODEL_NAME ?? 'gpt-4o-mini'),
system: '你是耐心的助手',
messages: [{ role: 'user', content: '你是谁' }],
})
for await (const delta of result.textStream) {
process.stdout.write(delta) // 打字机
}streamText一步到位:流式 + 多轮(messages) + system 设定;- AI SDK 更偏"薄调用",不带你复杂编排 — 这正是它和 LangChain 的分工。
🌱 本篇要点
| 能力 | 方法 |
|---|---|
| 一次对话 | model.invoke(messages) |
| 多轮 | 把历史消息放进 messages 数组 |
| 流式 | model.stream(messages) / for await ... of |
| 简单记忆 | 数组保存历史,重复发给模型 |
这节之后你已具备一个基础对话 Agent。 但它还没碰 KAG 语料、也没法"动手"做事。别急,下一篇就让模型拥有"外挂"知识 — 知识库与 RAG。