Skip to content
On this page

把对话做对 · 流式输出与多轮消息 ​

上一节我们让模型"说了一句话"。但真实产品里,对话要像聊天一样一段一段地出字(流式),而且要记得前面聊过什么(多轮)。这一节把这件事做扎实。

本节基于环境与依赖搭好的最小工程继续。

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/openai
js
// 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。

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命