LLM 对话框组件(一) | 豆包MarsCode AI刷题

254 阅读3分钟

本文是一个简单的LLM 对话框组件的开发过程介绍

要求

模仿市面上常见的LLM 对话框布局,进行开发一个对话框组件

技术栈

框架:Vite + Vue3

语言:使用 TypeScript 编写代码

测试:vitest/jest

规范:eslint

布局

页面布局:

QQ20241124-214309.png

组件结构:

QQ20241124-214345.png

初始化

创建项目:使用Vite创建Vue3项目,并配置TypeScript语言

# 创建项目
npm create vite@latest llm-dialog

# 安装依赖
npm i

布局过程:页面垂直分配,上面部分为发送给LLM的消息和返回的消息,下面部分为用户的输入框

QQ20241124-215122.png

布局较为简单,使用 flex 弹性盒布局,设主轴为垂直,再给子元素配置flex属性即可

核心逻辑

向 LLM 大模型发送请求,并接收流式传输的数据,逐字打印显示在消息框处

(之前调用过 Kimi 的接口,就先用这个举例了)

发送请求

// 发送请求
const stream = await this.client.chat.completions.create({
    model: "moonshot-v1-8k",
    messages: [
        { "role": "system", "content": "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。" },
        { "role": "user", content: message }
    ],
    temperature: 0.3,
    stream: true, // 开启流式传输
});

接收流式传输的数据

// 逐字打印
for await (const chunk of stream) {
    // 如果传输结束
    if (chunk.choices[0].finish_reason === 'stop') {
        messagesStore.finishPrint();
    }

    // 提取每个块返回的内容
    const delta = chunk.choices[0].delta

    // 如果有内容,则放入缓存里
    if (delta.content) {
        messagesStore.cacheContent(delta.content);
    }
}

内容不直接打印出来,而是放入缓存的原因:每次返回的字符数不固定,放入缓存后可以控制打印的速率(个人爱好处理,可自行调整)

逐字打印的逻辑:当内容放入缓存时,开启定时器,定时器会以固定的频率尝试从缓存中取出字符并依次打印到屏幕上,在接收结束时设置标志,让定时器在下一次缓存内没有内容时自动关闭

 // 逐字打印
function printContent() {
    if (cacheMessages.value.length > 0) {
        // 逐字打印
        const char = cacheMessages.value.charAt(0);
        cacheMessages.value = cacheMessages.value.slice(1);
        messages[messages.length - 1].content += char;

        // 尝试停止打印定时器
        stopPrintTimer()
    }
}

// 缓存内容
function cacheContent(content: string) {
    isTransmiting.value = true;
    cacheMessages.value += content;
    if (timer === null) {
        timer = setInterval(printContent, printSpeed);
    }
}

// 完成传输
function finishPrint() {
    isTransmiting.value = false;
}

// 停止打印定时器
function stopPrintTimer() {
    if (!isTransmiting.value && cacheMessages.value.length === 0 && timer !== null) {
        // 停止定时器
        clearInterval(timer);
        timer = null;

        // 发送完成事件
        emitter.emit('CompletePrint');
    }
}

适配多个 LLM

使用适配器模式适配多个 LLM 大模型的调用

// 定义一个统一的 LLM 接口
interface LLMAdapter {
    sendMessage(message: string): void;
}

单个 LLM 大模型调用的适配器

// KIMI LLM 适配器
class KimiAdapter implements LLMAdapter {
    private apiKey: string;
    private client: OpenAI;

    constructor(apiKey: string) {
        ......
    }

    async sendMessage(message: string) {
        ......
    }
}

export default KimiAdapter;

编写适配器工厂

class LLMAdapterFactory {
    static createAdapter(type: string, apiKey: string): LLMAdapter {
        switch (type) {
            case 'kimi':
                return new KimiAdapter(apiKey);
            case '???':
                return new ???(key);
            default:
                throw new Error(`Unsupported LLM type: ${type}`);
        }
    }
}

export default LLMAdapterFactory;

处理发送事件

// 处理发送消息事件
const handleSendMessage = (content: string) => {
    const kimi = new KimiAdapter(import.meta.env.VITE_KIMI_API_KEY as string)
    kimi.sendMessage(content);
};

语法解析

使用 marked.parse() 解析 LLM 返回的 Markdown 文档

let toMDcontent = computed(() => {
    return marked(props.message.content);
});

引入样式

import "github-markdown-css";

代码语法高亮:使用自定义指令实现

import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';

export const highlight = {
    mounted(el: HTMLElement) {
        const blocks = el.querySelectorAll('pre code');
        blocks.forEach((block) => {
            hljs.highlightElement(block as HTMLElement);
        });
    },
    updated(el: HTMLElement) {
        const blocks = el.querySelectorAll('pre code');
        blocks.forEach((block) => {
            hljs.highlightElement(block as HTMLElement);
        });
    },
};

注册自定义指令

// 注册代码块高亮指令
app.directive('highlight', highlight);

配置高亮 v-highlight

<div 
    v-highlight
    v-if="message.role === 'bot'"
    v-html="toMDcontent"
    :class="['chat-message__content', 'markdown-body', message.role]"
>
</div>

开发暂时到此,后面还需要配置记忆化对话,上传并解析图片与文档等