Terminal里的ChatGPT:用80行代码实现带记忆的智能对话流

阅读完本文,你可以轻松实现一个在本地可以运行的小工具: 一个可以随时在命令行唤醒的 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 离我们更近了一步不是么?

相关推荐
Yolo@~1 小时前
个人网站:基于html、css、js网页开发界面
javascript·css·html
斯~内克1 小时前
Electron 菜单系统深度解析:从基础到高级实践
前端·javascript·electron
dr李四维2 小时前
vue生命周期、钩子以及跨域问题简介
前端·javascript·vue.js·websocket·跨域问题·vue生命周期·钩子函数
旭久2 小时前
react+antd中做一个外部按钮新增 表格内部本地新增一条数据并且支持编辑删除(无难度上手)
前端·javascript·react.js
朴拙数科3 小时前
技术长期主义:用本分思维重构JavaScript逆向知识体系(一)Babel、AST、ES6+、ES5、浏览器环境、Node.js环境的关系和处理流程
javascript·重构·es6
拉不动的猪4 小时前
vue与react的简单问答
前端·javascript·面试
旭久5 小时前
react+antd封装一个可回车自定义option的select并且与某些内容相互禁用
前端·javascript·react.js
阿丽塔~5 小时前
React 函数组件间怎么进行通信?
前端·javascript·react.js
冴羽5 小时前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte