LLM 应用开发入门 - 实现 langchain.js ChatModel 接入火山引擎大模型和实现一个 CLI 聊天机器人(下)
flytam Lv4

书接上回,我们已经实现了一个 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 接收和大模型聊天交互

  • 将接受的 prompt 作为SystemMessage和将用户输入作为 HumanMessage传入stream方法。这里的SystemMessageHumanMessage也是 langchain 提供的工具类用于构造消息

  • 解析大模型返回的流式数据输出到终端

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 > ')

// 通过 `ChatPromptTemplate.fromMessages` 来初始化传给模型的完整 prompt。其中第一项为我们输入的`SystemMessage`,第二项为占位传递的历史上下文,第三项是本次我们的输入
const prompt = ChatPromptTemplate.fromMessages([
['system', _prompt],
new MessagesPlaceholder('chat_history'),
['human', '{input}'],
])

// 通过自定义链将这个`prompt`和我们的火山`chatModel`串联起来
const chain = prompt.pipe(this.chatModel)

const messageHistories = {}

// 将自定义链传递给`RunnableWithMessageHistory`构造出 `withMessageHistory` 对象,并实现聊天历史的上下文对象
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
config.configurable.sessionId = `${Date.now()}`
}
else {
// 通过 `withMessageHistory.stream` 进行模型的调用,并同时传递本次的上下文`config`对象
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 提供的工具类和自定义链,我们不仅简化了与大模型的交互,还实现了连续聊天的能力

  • Post title:LLM 应用开发入门 - 实现 langchain.js ChatModel 接入火山引擎大模型和实现一个 CLI 聊天机器人(下)
  • Post author:flytam
  • Create time:2024-08-29 21:51:50
  • Post link:https://blog.flytam.vip/LLM 应用开发入门 - 实现 langchain.js ChatModel 接入火山引擎大模型和实现一个 CLI 聊天机器人(下).html
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.