
书接上回,我们已经实现了一个 langchain.js 接入火山引擎的 ChatModel。
本文我们实现将这个大模型接入到聊天 CLI 实现和大模型进行交互式问答
需求
我们希望这个简易的聊天 CLI 能够拥有以下功能
- 启动时由用户输入 prompt
- 支持回答流式输出
- 支持连续聊天和清空上下文
聊天 CLI 基础能力实现
由于实现基本的 CLI 输入输出不是本文重点。这里我们直接通过以下代码实现一个简单的 node.js 交互式程序,实现了
- 启动后接收用户输入的 prompt
- 接收
/clear 指令打印清空
- 其它输入后原样打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| import readline from 'node:readline' import process, { stdin, stdout } from 'node:process' import { EventEmitter } from 'node:events'
class ChatCli extends EventEmitter { constructor() { super()
this.input = stdin this.output = stdout this.input.setEncoding('utf-8') this.output.setEncoding('utf-8') }
async runInputLoop() { const prompt = await this.prompt('请输入 prompt\n > ')
console.log('prompt', prompt)
return new Promise((resolve) => { const rl = readline.createInterface(this.input, this.output)
rl.setPrompt('> ') rl.prompt()
rl.on('line', async (line) => { if (line === '\\clear') { this.write('清空上下文\n') } else { this.write(`xxx ${line}`) this.write('\n') }
rl.prompt() })
rl.on('close', resolve)
rl.on('SIGINT', () => { rl.close() process.emit('SIGINT', 'SIGINT') }) }) }
write(data) { this.output.write(data) }
prompt(query = '> ') { return new Promise((resolve) => { const rl = readline.createInterface(this.input, this.output) rl.question(query, (answer) => { resolve(answer) rl.close() }) }) } }
const cli = new ChatCli()
cli.runInputLoop()
|

大模型接入
由于聊天 CLI 已经实现,我们只需要在对应的代码点进行模型的交互。相关接入火山引擎细节见LLM 应用开发入门 - 实现 langchain.js ChatModel 接入火山引擎大模型和实现一个 CLI 聊天机器人(上)
初始化 langchain 火山大模型
构造函数中初始化火山大模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { ChatVolcengine } from 'langchain-bytedance-volcengine'
class ChatCli extends EventEmitter { constructor() { super() this.chatModel = new ChatVolcengine({ volcengineApiHost: process.env.VOLCENGINE_HOST, volcengineApiKey: process.env.VOLCENGINE_API_KEY, model: process.env.VOLCENGINE_MODEL, })
} }
|
prompt 接收和大模型聊天交互
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { HumanMessage, SystemMessage } from '@langchain/core/messages'
async runInputLoop() { const prompt = await this.prompt('请输入 prompt\n > ') return new Promise((resolve) => {
rl.on('line', async (line) => { if (line === '\\clear') { this.write('清空上下文\n') } else { const stream = await this.chatModel.stream([new SystemMessage(prompt), new HumanMessage(line)])
for await (const chunk of stream) { this.write(chunk.content) } this.write('\n') } }) }) }
|
运行效果

连续聊天能力实现
虽然我们已经和 CLI 打通了和大模型的交互聊天,但是此时聊天 CLI 是没有聊天上下文功能的。

我们需要为这个聊天 CLI 增加上下文功能。对于直接调用大模型 OPEN API 来说,这通常需要我们将上下文手动处理传入大模型的 API。
但是上面提过,作为一个强大的 LLM 应用开发框架,langchain 提供了开箱即用的能力帮助我们实现。
langchain 只所以称为 chain,它是可以以自定义chain的形式将多个工具串联起来使用。每个串联起来的工具必须是一个实现了 Runnable 接口的实例,目前 langchain 中实现了Runnable 接口的组件有 Prompt ChatModel LLM OutputParser Retriever Tool
这里我们使用 langchain 提供的RunnableWithMessageHistory进行聊天上下文的记录和调用;使用InMemoryChatMessageHistory来实现内存的聊天上下文的存储
修改代码实现如下
通过 ChatPromptTemplate.fromMessages 来初始化传给模型的完整 prompt。其中第一项为我们输入的SystemMessage,第二项为占位传递的历史上下文,第三项是本次我们的输入
通过自定义链将这个prompt和我们的火山chatModel串联起来
将自定义链传递给RunnableWithMessageHistory构造出 withMessageHistory 对象,并实现聊天历史的上下文对象
通过 withMessageHistory.stream 进行模型的调用,并同时传递本次的上下文config对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import { ChatPromptTemplate, MessagesPlaceholder, } from '@langchain/core/prompts'
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history' import { RunnableWithMessageHistory } from '@langchain/core/runnables'
async runInputLoop() { const _prompt = await this.prompt('请输入 prompt\n > ')
const prompt = ChatPromptTemplate.fromMessages([ ['system', _prompt], new MessagesPlaceholder('chat_history'), ['human', '{input}'], ])
const chain = prompt.pipe(this.chatModel)
const messageHistories = {}
const withMessageHistory = new RunnableWithMessageHistory({ runnable: chain, getMessageHistory: async (sessionId) => { if (messageHistories[sessionId] === undefined) { messageHistories[sessionId] = new InMemoryChatMessageHistory() } return messageHistories[sessionId] }, inputMessagesKey: 'input', historyMessagesKey: 'chat_history', })
return new Promise((resolve) => { const config = { configurable: { sessionId: `${Date.now()}`, }, }
rl.on('line', async (line) => { if (line === '\\clear') { config.configurable.sessionId = `${Date.now()}` } else { const stream = await withMessageHistory.stream({ input: line, }, config)
for await (const chunk of stream) { this.write(chunk.content) } this.write('\n') }
rl.prompt() }) }) }
|
再次运行代码测试,表现符合预期

完整实现
代码详见
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| import readline from 'node:readline' import process, { stdin, stdout } from 'node:process' import { EventEmitter } from 'node:events'
import { ChatVolcengine } from 'langchain-bytedance-volcengine' import 'dotenv/config' import { HumanMessage, SystemMessage } from '@langchain/core/messages' import { ChatPromptTemplate, MessagesPlaceholder, } from '@langchain/core/prompts'
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history' import { RunnableWithMessageHistory } from '@langchain/core/runnables'
class ChatCli extends EventEmitter { constructor() { super()
this.input = stdin this.output = stdout this.input.setEncoding('utf-8') this.output.setEncoding('utf-8')
this.chatModel = new ChatVolcengine({ volcengineApiHost: process.env.VOLCENGINE_HOST, volcengineApiKey: process.env.VOLCENGINE_API_KEY, model: process.env.VOLCENGINE_MODEL, }) }
async runInputLoop() { const _prompt = await this.prompt('请输入 prompt\n > ')
const prompt = ChatPromptTemplate.fromMessages([ ['system', _prompt], new MessagesPlaceholder('chat_history'), ['human', '{input}'], ])
const chain = prompt.pipe(this.chatModel)
const messageHistories = {} const withMessageHistory = new RunnableWithMessageHistory({ runnable: chain, getMessageHistory: async (sessionId) => { if (messageHistories[sessionId] === undefined) { messageHistories[sessionId] = new InMemoryChatMessageHistory() } return messageHistories[sessionId] }, inputMessagesKey: 'input', historyMessagesKey: 'chat_history', })
return new Promise((resolve) => { const rl = readline.createInterface(this.input, this.output)
rl.setPrompt('> ') rl.prompt()
const config = { configurable: { sessionId: `${Date.now()}`, }, }
rl.on('line', async (line) => { if (line === '\\clear') { config.configurable.sessionId = `${Date.now()}` } else { const stream = await withMessageHistory.stream({ input: line, }, config)
for await (const chunk of stream) { this.write(chunk.content) } this.write('\n') }
rl.prompt() })
rl.on('close', resolve)
rl.on('SIGINT', () => { rl.close() process.emit('SIGINT', 'SIGINT') }) }) }
write(data) { this.output.write(data) }
prompt(query = '> ') { return new Promise((resolve) => { const rl = readline.createInterface(this.input, this.output) rl.question(query, (answer) => { resolve(answer) rl.close() }) }) } }
const cli = new ChatCli()
cli.runInputLoop()
|
总结
通过本文我们实现了一个简易的聊天 CLI,并成功接入了火山引擎大模型,实现了流式输出和上下文管理功能。通过 langchain.js 提供的工具类和自定义链,我们不仅简化了与大模型的交互,还实现了连续聊天的能力