阅读完本文,你可以轻松实现一个在本地可以运行的小工具: 一个可以随时在命令行唤醒的 AI 聊天机器人。
- 它足够简单
- 命令行直接唤起
- 支持记忆功能
- 支持流式输出
前置知识要求:
- 知道如何使用命令行
- 本文基于 JavaScript 实现,如果您具备基本的 JavaScript 知识更好
先来看一下最终的实现效果:
1. 实现基础对话
本文基于阿里云提供的通义千问模型能力来实现,其他模型同理。
首先,你需要去官网申请一个 api-key,跟着链接文档操作即可,不麻烦,前期都是免费的。
接下来,我选中一个模型,示例一下如何调接口。
API 示例,我们看到示例里有 http 调用,比较直观。
我们来测试一把 ai 接口调用,这是一个最基础的版本:
js
const readline = require('readline');
const axios = require('axios');
const headers = {
'Content-Type': 'application/json',
// 这里要替换成自己的 api-key
'Authorization': 'Bearer $DASHSCOPE_API_KEY'
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('You: ', async (input) => {
const messages = [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": input }
];
const response = await axios.post('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {
"model": "qwen-turbo",
"messages": messages
}, { headers });
console.log('AI: ', response.data.choices[0].message.content);
rl.close();
});
如图所示,我们第一个版本实现了基础版的一问一答。
这里我们知道了最基础的 ai 调用方式,就是调 http 接口。
2. 可持续对话
如果每次都一问一答就结束对话,有点麻烦,我们改造一下让他可持续对话:
js
const readline = require('readline');
const axios = require('axios');
const headers = {
'Content-Type': 'application/json',
// 这里要替换成自己的 api-key
'Authorization': 'Bearer $DASHSCOPE_API_KEY'
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function startChat() {
rl.question('You: ', async (input) => {
if (input === 'exit') {
console.log('Goodbye!');
rl.close();
return;
}
const messages = [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": input }
];
const response = await axios.post('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {
"model": "qwen-turbo",
"messages": messages
}, { headers });
console.log('\n\nAI: ', response.data.choices[0].message.content, '\n\n');
startChat();
});
}
startChat();
这里我们使用了递归,每次 ai 回答完,我们需要再次进入对话过程;
我们实现了持续对话的能力,并且可以通过 exit 来退出当前对话。
但是有一个明显的问题,ai 并不记得我们之前的对话。
3. 保持记忆
我想实现让 ai 保持记忆,该怎么做?
我们可以观察一下之前调用接口的入参:
js
const messages = [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": input }
];
我们只需要把对话历史都添加进来,ai 就知道之前聊了些什么。
这里我们维护一个 history 数组即可。
js
// ...省略
// 添加一个 history 数组,每轮回答完把记录添加进来
const history = [];
function startChat() {
rl.question('You: ', async (input) => {
if (input === 'exit') {
console.log('Goodbye!');
rl.close();
return;
}
const messages = [
{ "role": "system", "content": "You are a helpful assistant." },
...history,
{ "role": "user", "content": input }
];
const response = await axios.post('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {
"model": "qwen-turbo",
"messages": messages
}, { headers });
const aiResponse = response.data.choices[0].message.content;
console.log('\n\nAI: ', aiResponse, '\n\n');
history.push({ "role": "user", "content": input });
history.push({ "role": "assistant", "content": aiResponse });
startChat();
});
}
startChat();
对话测试结果如下:
4. 流式输出
如果你已经到了这一步,你会发现输出好像有点慢,这是因为每次要等待 ai 回答完才能一次性输出。
如何能想在线服务那样一个字一个字敲出答案(流式输出)?
这个优化点我大概清楚接口支持 "stream" 返回,但是具体怎么改造我也不知道,于是我问了 ai
惊喜的是,ai 一把帮我改造好了,测试运行效果符合预期。(截图为 Trae 编辑器)
最终代码如下:
js
const readline = require('readline');
const axios = require('axios');
const headers = {
'Content-Type': 'application/json',
// 这里要替换成自己的 api-key
'Authorization': 'Bearer $DASHSCOPE_API_KEY'
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const history = [];
function startChat() {
rl.question('You: ', async (input) => {
if (input === 'exit') {
console.log('Goodbye!');
rl.close();
return;
}
const messages = [
{ "role": "system", "content": "You are a helpful assistant." },
...history,
{ "role": "user", "content": input }
];
// 修改为流式请求
try {
const response = await axios.post('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {
"model": "qwen-turbo",
"messages": messages,
"stream": true // 启用流式输出
}, {
headers,
responseType: 'stream' // 设置响应类型为流
});
let aiResponse = '';
process.stdout.write('\n\nAI: ');
// 处理流式响应
response.data.on('data', (chunk) => {
const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content || '';
if (content) {
process.stdout.write(content);
aiResponse += content;
}
} catch (e) {
console.error('解析响应出错:', e);
}
}
}
});
response.data.on('end', () => {
console.log('\n\n');
history.push({ "role": "user", "content": input });
history.push({ "role": "assistant", "content": aiResponse });
startChat();
});
response.data.on('error', (err) => {
console.error('流处理错误:', err);
startChat();
});
} catch (error) {
console.error('请求错误:', error.message);
startChat();
}
});
}
startChat();
5. 总结
本文我们实现了一个运行在本地的命令行 AI chatbot
- 如何调用 ai?其实就是通过调用 http 接口实现
- 如何保持记忆?就是把历史数据都传给他,同时我们也知道了入参格式
- 如何流式输出?接口支持 stream 返回,每次把返回答案片段拼接输出即可;
基于上述,我们已经实打实地做了一款 AI 实用工具,尽管不完美,但是 AI 离我们更近了一步不是么?