返回笔记首页

AI 能力集成

主题配置

一、技术实现方案

1.1 AI集成架构

plain
AI能力集成架构
  ├── 模型接入层
  │   ├── OpenAI (GPT-4/3.5)
  │   ├── 文心一言 (百度)
  │   ├── 通义千问 (阿里)
  │   ├── Deepseek
  │   └── Claude (Anthropic)
  │
  ├── 适配器层
  │   ├── 统一接口封装
  │   ├── 参数格式转换
  │   ├── 错误处理
  │   └── 重试机制
  │
  ├── 流控层
  │   ├── Token计数
  │   ├── 速率限制
  │   ├── 并发控制
  │   └── 成本统计
  │
  └── 优化层
      ├── Prompt优化
      ├── 缓存策略
      ├── 负载均衡
      └── 降级方案

1.2 技术栈

  • HTTP客户端: Axios
  • Token计数: tiktoken-js / gpt-3-encoder
  • 流式处理: SSE / Fetch API
  • 状态管理: Pinia
  • 错误处理: 统一错误拦截器

二、OpenAI API集成

2.1 OpenAI客户端封装

openai-client.js

javascript
import axios from 'axios'

export class OpenAIClient {
    constructor(config = {}) {
        this.apiKey = config.apiKey || ''
        this.baseURL = config.baseURL || 'https://api.openai.com/v1'
        this.model = config.model || 'gpt-3.5-turbo'
        this.temperature = config.temperature || 0.7
        this.maxTokens = config.maxTokens || 2000

        this.client = axios.create({
            baseURL: this.baseURL,
            headers: {
                Authorization: `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
            },
            timeout: 60000,
        })
    }

    // 聊天补全(非流式)
    async chat(messages, options = {}) {
        try {
            const response = await this.client.post('/chat/completions', {
                model: options.model || this.model,
                messages: messages,
                temperature: options.temperature || this.temperature,
                max_tokens: options.maxTokens || this.maxTokens,
                top_p: options.topP || 1,
                frequency_penalty: options.frequencyPenalty || 0,
                presence_penalty: options.presencePenalty || 0,
                stream: false,
            })

            return {
                content: response.data.choices[0].message.content,
                usage: response.data.usage,
                model: response.data.model,
                finishReason: response.data.choices[0].finish_reason,
            }
        } catch (error) {
            throw this.handleError(error)
        }
    }

    // 聊天补全(流式)
    async chatStream(messages, options = {}) {
        try {
            const response = await fetch(`${this.baseURL}/chat/completions`, {
                method: 'POST',
                headers: {
                    Authorization: `Bearer ${this.apiKey}`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    model: options.model || this.model,
                    messages: messages,
                    temperature: options.temperature || this.temperature,
                    max_tokens: options.maxTokens || this.maxTokens,
                    stream: true,
                }),
            })

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`)
            }

            return this.handleStreamResponse(response, options)
        } catch (error) {
            throw this.handleError(error)
        }
    }

    // 处理流式响应
    async handleStreamResponse(response, options = {}) {
        const reader = response.body.getReader()
        const decoder = new TextDecoder()
        let buffer = ''

        options.onStart?.()

        try {
            while (true) {
                const { done, value } = await reader.read()

                if (done) {
                    options.onComplete?.()
                    break
                }

                const chunk = decoder.decode(value, { stream: true })
                buffer += chunk

                const lines = buffer.split('\n')
                buffer = lines.pop() || ''

                for (const line of lines) {
                    if (line.trim() === '') continue
                    if (line.trim() === 'data: [DONE]') continue

                    if (line.startsWith('data: ')) {
                        try {
                            const data = JSON.parse(line.slice(6))
                            const content =
                                data.choices[0]?.delta?.content || ''

                            if (content) {
                                options.onChunk?.(content)
                            }

                            // 检查是否完成
                            if (data.choices[0]?.finish_reason) {
                                options.onFinish?.(
                                    data.choices[0].finish_reason
                                )
                            }
                        } catch (e) {
                            console.error('Parse error:', e, line)
                        }
                    }
                }
            }
        } catch (error) {
            options.onError?.(error)
            throw error
        }
    }

    // 文本补全
    async complete(prompt, options = {}) {
        try {
            const response = await this.client.post('/completions', {
                model: options.model || 'text-davinci-003',
                prompt: prompt,
                max_tokens: options.maxTokens || this.maxTokens,
                temperature: options.temperature || this.temperature,
                top_p: options.topP || 1,
            })

            return {
                content: response.data.choices[0].text,
                usage: response.data.usage,
            }
        } catch (error) {
            throw this.handleError(error)
        }
    }

    // 图片生成
    async createImage(prompt, options = {}) {
        try {
            const response = await this.client.post('/images/generations', {
                prompt: prompt,
                n: options.n || 1,
                size: options.size || '1024x1024',
                response_format: options.responseFormat || 'url',
            })

            return response.data.data
        } catch (error) {
            throw this.handleError(error)
        }
    }

    // Embeddings生成
    async createEmbedding(input, options = {}) {
        try {
            const response = await this.client.post('/embeddings', {
                model: options.model || 'text-embedding-ada-002',
                input: input,
            })

            return response.data.data[0].embedding
        } catch (error) {
            throw this.handleError(error)
        }
    }

    // 错误处理
    handleError(error) {
        if (error.response) {
            const { status, data } = error.response

            switch (status) {
                case 401:
                    return new Error('API密钥无效或已过期')
                case 429:
                    return new Error('请求频率超限,请稍后重试')
                case 500:
                case 502:
                case 503:
                    return new Error('OpenAI服务暂时不可用')
                default:
                    return new Error(data.error?.message || '请求失败')
            }
        } else if (error.request) {
            return new Error('网络连接失败,请检查网络')
        } else {
            return error
        }
    }

    // 设置API密钥
    setApiKey(apiKey) {
        this.apiKey = apiKey
        this.client.defaults.headers['Authorization'] = `Bearer ${apiKey}`
    }

    // 设置模型
    setModel(model) {
        this.model = model
    }
}

export default new OpenAIClient()

2.2 OpenAI集成演示组件

OpenAIDemo.vue

vue
<script setup>
import { ref } from 'vue'
import { OpenAIClient } from './openai-client.js'

const apiKey = ref('')
const model = ref('gpt-3.5-turbo')
const temperature = ref(0.7)
const maxTokens = ref(2000)

const messages = ref([{ role: 'system', content: '你是一个有帮助的AI助手。' }])
const userInput = ref('')
const isLoading = ref(false)
const streamedResponse = ref('')
const isStreaming = ref(false)

const usage = ref(null)

let client = null

// 初始化客户端
const initClient = () => {
    if (!apiKey.value) {
        alert('请输入API Key')
        return false
    }

    client = new OpenAIClient({
        apiKey: apiKey.value,
        model: model.value,
        temperature: temperature.value,
        maxTokens: maxTokens.value,
    })

    return true
}

// 发送消息(非流式)
const sendMessage = async () => {
    if (!userInput.value.trim()) return
    if (!initClient()) return

    messages.value.push({
        role: 'user',
        content: userInput.value,
    })

    const input = userInput.value
    userInput.value = ''
    isLoading.value = true

    try {
        const response = await client.chat(messages.value)

        messages.value.push({
            role: 'assistant',
            content: response.content,
        })

        usage.value = response.usage
    } catch (error) {
        alert('错误: ' + error.message)
        messages.value.pop() // 移除用户消息
    } finally {
        isLoading.value = false
    }
}

// 发送消息(流式)
const sendMessageStream = async () => {
    if (!userInput.value.trim()) return
    if (!initClient()) return

    messages.value.push({
        role: 'user',
        content: userInput.value,
    })

    userInput.value = ''
    streamedResponse.value = ''
    isStreaming.value = true

    try {
        await client.chatStream(messages.value, {
            onStart: () => {
                console.log('Stream started')
            },
            onChunk: (content) => {
                streamedResponse.value += content
            },
            onFinish: (reason) => {
                console.log('Finish reason:', reason)
                messages.value.push({
                    role: 'assistant',
                    content: streamedResponse.value,
                })
                isStreaming.value = false
            },
            onComplete: () => {
                console.log('Stream complete')
            },
            onError: (error) => {
                console.error('Stream error:', error)
                alert('错误: ' + error.message)
                messages.value.pop()
                isStreaming.value = false
            },
        })
    } catch (error) {
        alert('错误: ' + error.message)
        messages.value.pop()
        isStreaming.value = false
    }
}

// 清空对话
const clearMessages = () => {
    messages.value = [{ role: 'system', content: '你是一个有帮助的AI助手。' }]
    streamedResponse.value = ''
    usage.value = null
}

// 预设问题
const presetQuestions = [
    '介绍一下Vue 3的新特性',
    '写一个JavaScript防抖函数',
    '解释什么是闭包',
    '比较React和Vue的区别',
]

const askPresetQuestion = (question) => {
    userInput.value = question
}

// 模型选项
const modelOptions = [
    { value: 'gpt-4', label: 'GPT-4', description: '最强大的模型' },
    {
        value: 'gpt-3.5-turbo',
        label: 'GPT-3.5 Turbo',
        description: '快速且经济',
    },
    {
        value: 'gpt-3.5-turbo-16k',
        label: 'GPT-3.5 Turbo 16K',
        description: '更长上下文',
    },
]
</script>

<template>
    <div class="openai-demo">
        <div class="demo-header">
            <h2>OpenAI API集成演示</h2>
            <p class="subtitle">测试GPT模型对话功能</p>
        </div>

        <!-- 配置面板 -->
        <div class="config-panel">
            <h3>API配置</h3>

            <div class="config-grid">
                <div class="config-item">
                    <label>API Key:</label>
                    <input
                        v-model="apiKey"
                        type="password"
                        placeholder="sk-..."
                        class="api-key-input"
                    />
                </div>

                <div class="config-item">
                    <label>模型:</label>
                    <select v-model="model" class="model-select">
                        <option
                            v-for="option in modelOptions"
                            :key="option.value"
                            :value="option.value"
                        >
                            {{ option.label }} - {{ option.description }}
                        </option>
                    </select>
                </div>

                <div class="config-item">
                    <label>Temperature: {{ temperature }}</label>
                    <input
                        v-model.number="temperature"
                        type="range"
                        min="0"
                        max="2"
                        step="0.1"
                    />
                </div>

                <div class="config-item">
                    <label>Max Tokens: {{ maxTokens }}</label>
                    <input
                        v-model.number="maxTokens"
                        type="range"
                        min="100"
                        max="4000"
                        step="100"
                    />
                </div>
            </div>
        </div>

        <!-- 预设问题 -->
        <div class="preset-section">
            <h3>快速开始</h3>
            <div class="preset-grid">
                <button
                    v-for="question in presetQuestions"
                    :key="question"
                    @click="askPresetQuestion(question)"
                    class="preset-btn"
                >
                    {{ question }}
                </button>
            </div>
        </div>

        <!-- 对话区域 -->
        <div class="chat-section">
            <h3>对话</h3>

            <div class="messages-area">
                <div
                    v-for="(msg, index) in messages"
                    :key="index"
                    class="message-item"
                    :class="msg.role"
                >
                    <div class="message-role">
                        {{
                            msg.role === 'user'
                                ? '👤 用户'
                                : msg.role === 'assistant'
                                  ? '🤖 AI'
                                  : '⚙️ 系统'
                        }}
                    </div>
                    <div class="message-content">{{ msg.content }}</div>
                </div>

                <!-- 流式输出 -->
                <div
                    v-if="isStreaming"
                    class="message-item assistant streaming"
                >
                    <div class="message-role">🤖 AI</div>
                    <div class="message-content">
                        {{ streamedResponse }}
                        <span class="cursor">|</span>
                    </div>
                </div>

                <div v-if="isLoading" class="loading-indicator">
                    <div class="loading-dots">
                        <span></span>
                        <span></span>
                        <span></span>
                    </div>
                    <span>AI正在思考...</span>
                </div>
            </div>

            <!-- 输入区域 -->
            <div class="input-area">
                <textarea
                    v-model="userInput"
                    @keydown.enter.ctrl="sendMessageStream"
                    placeholder="输入消息... (Ctrl+Enter发送流式,Enter发送普通)"
                    class="message-input"
                ></textarea>

                <div class="button-group">
                    <button
                        @click="sendMessage"
                        :disabled="isLoading || isStreaming"
                        class="send-btn"
                    >
                        普通发送
                    </button>
                    <button
                        @click="sendMessageStream"
                        :disabled="isLoading || isStreaming"
                        class="stream-btn"
                    >
                        流式发送
                    </button>
                    <button
                        @click="clearMessages"
                        :disabled="isLoading || isStreaming"
                        class="clear-btn"
                    >
                        清空
                    </button>
                </div>
            </div>
        </div>

        <!-- 使用统计 -->
        <div v-if="usage" class="usage-panel">
            <h3>Token使用情况</h3>
            <div class="usage-grid">
                <div class="usage-item">
                    <label>提示Token:</label>
                    <span>{{ usage.prompt_tokens }}</span>
                </div>
                <div class="usage-item">
                    <label>补全Token:</label>
                    <span>{{ usage.completion_tokens }}</span>
                </div>
                <div class="usage-item">
                    <label>总计Token:</label>
                    <span>{{ usage.total_tokens }}</span>
                </div>
                <div class="usage-item">
                    <label>预估费用:</label>
                    <span
                        >${{
                            ((usage.total_tokens * 0.002) / 1000).toFixed(4)
                        }}</span
                    >
                </div>
            </div>
        </div>

        <!-- API说明 -->
        <div class="api-doc-section">
            <h3>OpenAI API使用说明</h3>

            <div class="doc-card">
                <h4>1. 获取API Key</h4>
                <p>
                    访问
                    <a
                        href="https://platform.openai.com/api-keys"
                        target="_blank"
                        >OpenAI官网</a
                    >
                    创建API密钥
                </p>
            </div>

            <div class="doc-card">
                <h4>2. 非流式调用</h4>
                <pre><code>const response = await client.chat([
  { role: 'system', content: '你是助手' },
  { role: 'user', content: '你好' }
])
console.log(response.content)</code></pre>
            </div>

            <div class="doc-card">
                <h4>3. 流式调用</h4>
                <pre><code>await client.chatStream(messages, {
  onChunk: (content) => {
    // 处理每个数据块
    appendToMessage(content)
  },
  onComplete: () => {
    console.log('完成')
  }
})</code></pre>
            </div>

            <div class="doc-card">
                <h4>4. 模型选择建议</h4>
                <ul>
                    <li>
                        <strong>GPT-4:</strong> 最强大,适合复杂任务,费用较高
                    </li>
                    <li>
                        <strong>GPT-3.5 Turbo:</strong> 性价比高,适合大多数场景
                    </li>
                    <li>
                        <strong>GPT-3.5 Turbo 16K:</strong>
                        更长上下文,适合长文本处理
                    </li>
                </ul>
            </div>

            <div class="doc-card">
                <h4>5. Temperature参数说明</h4>
                <ul>
                    <li><strong>0-0.3:</strong> 确定性强,适合事实性任务</li>
                    <li><strong>0.4-0.7:</strong> 平衡创造性和准确性</li>
                    <li><strong>0.8-2.0:</strong> 高创造性,适合创意写作</li>
                </ul>
            </div>
        </div>
    </div>
</template>

<style scoped>
.openai-demo {
    padding: 20px;
    background: #f5f7fa;
    min-height: 100vh;
}

.demo-header {
    text-align: center;
    margin-bottom: 40px;
}

.demo-header h2 {
    margin: 0 0 10px 0;
    font-size: 32px;
    color: #303133;
}

.subtitle {
    margin: 0;
    font-size: 16px;
    color: #909399;
}

/* 配置面板 */
.config-panel,
.preset-section,
.chat-section,
.usage-panel,
.api-doc-section {
    margin-bottom: 30px;
    padding: 24px;
    background: white;
    border-radius: 8px;
}

h3 {
    margin: 0 0 20px 0;
    font-size: 18px;
    color: #303133;
}

.config-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
}

.config-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.config-item label {
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.api-key-input,
.model-select {
    padding: 10px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
}

/* 预设问题 */
.preset-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 12px;
}

.preset-btn {
    padding: 12px 16px;
    background: #ecf5ff;
    color: #409eff;
    border: 1px solid #b3d8ff;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    text-align: left;
    transition: all 0.3s;
}

.preset-btn:hover {
    background: #409eff;
    color: white;
}

/* 对话区域 */
.messages-area {
    min-height: 400px;
    max-height: 600px;
    overflow-y: auto;
    padding: 20px;
    background: #fafafa;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    margin-bottom: 20px;
}

.message-item {
    margin-bottom: 20px;
    padding: 16px;
    border-radius: 8px;
}

.message-item.system {
    background: #f0f9ff;
    border-left: 4px solid #409eff;
}

.message-item.user {
    background: #f0f9ff;
    border-left: 4px solid #67c23a;
}

.message-item.assistant {
    background: #f5f7fa;
    border-left: 4px solid #e6a23c;
}

.message-role {
    font-size: 13px;
    font-weight: 600;
    color: #606266;
    margin-bottom: 8px;
}

.message-content {
    font-size: 14px;
    line-height: 1.8;
    color: #303133;
    white-space: pre-wrap;
}

.cursor {
    display: inline-block;
    width: 2px;
    height: 1.2em;
    background: #409eff;
    margin-left: 2px;
    animation: blink 1s infinite;
}

@keyframes blink {
    0%,
    50% {
        opacity: 1;
    }
    51%,
    100% {
        opacity: 0;
    }
}

.loading-indicator {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px;
    color: #909399;
    font-size: 14px;
}

.loading-dots {
    display: flex;
    gap: 4px;
}

.loading-dots span {
    width: 8px;
    height: 8px;
    background: #409eff;
    border-radius: 50%;
    animation: bounce 1.4s infinite ease-in-out both;
}

.loading-dots span:nth-child(1) {
    animation-delay: -0.32s;
}

.loading-dots span:nth-child(2) {
    animation-delay: -0.16s;
}

@keyframes bounce {
    0%,
    80%,
    100% {
        transform: scale(0);
    }
    40% {
        transform: scale(1);
    }
}

/* 输入区域 */
.input-area {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.message-input {
    width: 100%;
    min-height: 100px;
    padding: 12px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
    resize: vertical;
}

.button-group {
    display: flex;
    gap: 12px;
}

.send-btn,
.stream-btn,
.clear-btn {
    padding: 10px 24px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    transition: all 0.3s;
}

.send-btn {
    background: #409eff;
    color: white;
}

.stream-btn {
    background: #67c23a;
    color: white;
}

.clear-btn {
    background: #e6a23c;
    color: white;
}

.send-btn:disabled,
.stream-btn:disabled,
.clear-btn:disabled {
    background: #c0c4cc;
    cursor: not-allowed;
}

/* 使用统计 */
.usage-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}

.usage-item {
    padding: 16px;
    background: #f5f7fa;
    border-radius: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.usage-item label {
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.usage-item span {
    font-size: 18px;
    color: #409eff;
    font-weight: 700;
}

/* API文档 */
.doc-card {
    margin-bottom: 24px;
    padding: 20px;
    background: #f5f7fa;
    border-radius: 8px;
}

.doc-card h4 {
    margin: 0 0 12px 0;
    font-size: 16px;
    color: #303133;
}

.doc-card p {
    margin: 0 0 12px 0;
    font-size: 14px;
    line-height: 1.6;
    color: #606266;
}

.doc-card a {
    color: #409eff;
    text-decoration: none;
}

.doc-card a:hover {
    text-decoration: underline;
}

.doc-card pre {
    margin: 0;
    padding: 16px;
    background: #282c34;
    border-radius: 4px;
    overflow-x: auto;
}

.doc-card code {
    font-size: 13px;
    line-height: 1.6;
    color: #abb2bf;
    font-family: 'Courier New', monospace;
}

.doc-card ul {
    margin: 0;
    padding-left: 24px;
}

.doc-card li {
    font-size: 14px;
    line-height: 2;
    color: #606266;
}
</style>

三、国内大模型接入

3.1 统一适配器

ai-model-adapter.js

javascript
// 统一AI模型适配器
export class AIModelAdapter {
    constructor(provider, config) {
        this.provider = provider
        this.config = config
        this.client = this.createClient()
    }

    // 创建客户端
    createClient() {
        switch (this.provider) {
            case 'openai':
                return new OpenAIAdapter(this.config)
            case 'wenxin':
                return new WenxinAdapter(this.config)
            case 'qianwen':
                return new QianwenAdapter(this.config)
            case 'deepseek':
                return new DeepseekAdapter(this.config)
            default:
                throw new Error(`Unsupported provider: ${this.provider}`)
        }
    }

    // 统一聊天接口
    async chat(messages, options = {}) {
        return await this.client.chat(messages, options)
    }

    // 统一流式聊天接口
    async chatStream(messages, options = {}) {
        return await this.client.chatStream(messages, options)
    }
}

// 文心一言适配器
class WenxinAdapter {
    constructor(config) {
        this.apiKey = config.apiKey
        this.secretKey = config.secretKey
        this.baseURL =
            'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop'
        this.accessToken = null
    }

    // 获取Access Token
    async getAccessToken() {
        if (this.accessToken) return this.accessToken

        const response = await fetch(
            `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.secretKey}`,
            { method: 'POST' }
        )

        const data = await response.json()
        this.accessToken = data.access_token
        return this.accessToken
    }

    // 转换消息格式
    convertMessages(messages) {
        return messages.map((msg) => ({
            role: msg.role === 'assistant' ? 'assistant' : 'user',
            content: msg.content,
        }))
    }

    // 聊天
    async chat(messages, options = {}) {
        const token = await this.getAccessToken()

        const response = await fetch(
            `${this.baseURL}/chat/completions?access_token=${token}`,
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    messages: this.convertMessages(messages),
                    temperature: options.temperature || 0.7,
                    top_p: options.topP || 0.9,
                    stream: false,
                }),
            }
        )

        const data = await response.json()

        return {
            content: data.result,
            usage: {
                prompt_tokens: data.usage.prompt_tokens,
                completion_tokens: data.usage.completion_tokens,
                total_tokens: data.usage.total_tokens,
            },
        }
    }

    // 流式聊天
    async chatStream(messages, options = {}) {
        const token = await this.getAccessToken()

        const response = await fetch(
            `${this.baseURL}/chat/completions?access_token=${token}`,
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    messages: this.convertMessages(messages),
                    temperature: options.temperature || 0.7,
                    stream: true,
                }),
            }
        )

        const reader = response.body.getReader()
        const decoder = new TextDecoder()

        options.onStart?.()

        while (true) {
            const { done, value } = await reader.read()
            if (done) break

            const chunk = decoder.decode(value)
            const lines = chunk.split('\n').filter((line) => line.trim())

            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    try {
                        const data = JSON.parse(line.slice(6))
                        options.onChunk?.(data.result)
                    } catch (e) {
                        console.error('Parse error:', e)
                    }
                }
            }
        }

        options.onComplete?.()
    }
}

// 通义千问适配器
class QianwenAdapter {
    constructor(config) {
        this.apiKey = config.apiKey
        this.baseURL =
            'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation'
    }

    async chat(messages, options = {}) {
        const response = await fetch(this.baseURL, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                model: 'qwen-turbo',
                input: {
                    messages: messages,
                },
                parameters: {
                    temperature: options.temperature || 0.7,
                    top_p: options.topP || 0.9,
                },
            }),
        })

        const data = await response.json()

        return {
            content: data.output.text,
            usage: data.usage,
        }
    }

    async chatStream(messages, options = {}) {
        const response = await fetch(this.baseURL, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
                'X-DashScope-SSE': 'enable',
            },
            body: JSON.stringify({
                model: 'qwen-turbo',
                input: { messages: messages },
                parameters: {
                    incremental_output: true,
                },
            }),
        })

        const reader = response.body.getReader()
        const decoder = new TextDecoder()

        options.onStart?.()

        while (true) {
            const { done, value } = await reader.read()
            if (done) break

            const chunk = decoder.decode(value)
            const lines = chunk.split('\n').filter((line) => line.trim())

            for (const line of lines) {
                if (line.startsWith('data:')) {
                    try {
                        const data = JSON.parse(line.slice(5))
                        options.onChunk?.(data.output.text)
                    } catch (e) {
                        console.error('Parse error:', e)
                    }
                }
            }
        }

        options.onComplete?.()
    }
}

// Deepseek适配器
class DeepseekAdapter {
    constructor(config) {
        this.apiKey = config.apiKey
        this.baseURL = 'https://api.deepseek.com/v1'
    }

    async chat(messages, options = {}) {
        const response = await fetch(`${this.baseURL}/chat/completions`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                model: 'deepseek-chat',
                messages: messages,
                temperature: options.temperature || 0.7,
                stream: false,
            }),
        })

        const data = await response.json()

        return {
            content: data.choices[0].message.content,
            usage: data.usage,
        }
    }

    async chatStream(messages, options = {}) {
        const response = await fetch(`${this.baseURL}/chat/completions`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                model: 'deepseek-chat',
                messages: messages,
                stream: true,
            }),
        })

        const reader = response.body.getReader()
        const decoder = new TextDecoder()

        options.onStart?.()

        while (true) {
            const { done, value } = await reader.read()
            if (done) break

            const chunk = decoder.decode(value)
            const lines = chunk.split('\n').filter((line) => line.trim())

            for (const line of lines) {
                if (line.startsWith('data: ') && line !== 'data: [DONE]') {
                    try {
                        const data = JSON.parse(line.slice(6))
                        const content = data.choices[0]?.delta?.content || ''
                        if (content) {
                            options.onChunk?.(content)
                        }
                    } catch (e) {
                        console.error('Parse error:', e)
                    }
                }
            }
        }

        options.onComplete?.()
    }
}

export default AIModelAdapter

3.2 模型切换演示组件

ModelSwitchDemo.vue

vue
<script setup>
import { ref } from 'vue'
import AIModelAdapter from './ai-model-adapter.js'

const selectedProvider = ref('openai')
const apiConfig = ref({
    openai: { apiKey: '' },
    wenxin: { apiKey: '', secretKey: '' },
    qianwen: { apiKey: '' },
    deepseek: { apiKey: '' },
})

const messages = ref([])
const userInput = ref('')
const isLoading = ref(false)
const streamedResponse = ref('')

const providers = [
    { value: 'openai', name: 'OpenAI GPT', icon: '🤖' },
    { value: 'wenxin', name: '文心一言', icon: '🐻' },
    { value: 'qianwen', name: '通义千问', icon: '☁️' },
    { value: 'deepseek', name: 'Deepseek', icon: '🔍' },
]

let adapter = null

const initAdapter = () => {
    const config = apiConfig.value[selectedProvider.value]

    if (!config.apiKey) {
        alert('请输入API Key')
        return false
    }

    adapter = new AIModelAdapter(selectedProvider.value, config)
    return true
}

const sendMessage = async () => {
    if (!userInput.value.trim()) return
    if (!initAdapter()) return

    messages.value.push({
        role: 'user',
        content: userInput.value,
    })

    streamedResponse.value = ''
    isLoading.value = true
    userInput.value = ''

    try {
        await adapter.chatStream(messages.value, {
            onStart: () => {
                console.log('Stream started')
            },
            onChunk: (content) => {
                streamedResponse.value += content
            },
            onComplete: () => {
                messages.value.push({
                    role: 'assistant',
                    content: streamedResponse.value,
                })
                streamedResponse.value = ''
                isLoading.value = false
            },
        })
    } catch (error) {
        alert('错误: ' + error.message)
        messages.value.pop()
        isLoading.value = false
    }
}
</script>

<template>
    <div class="model-switch-demo">
        <div class="demo-header">
            <h2>多模型统一接入</h2>
            <p class="subtitle">支持OpenAI、文心一言、通义千问、Deepseek</p>
        </div>

        <!-- 模型选择 -->
        <div class="provider-section">
            <h3>选择AI模型</h3>
            <div class="provider-grid">
                <div
                    v-for="provider in providers"
                    :key="provider.value"
                    class="provider-card"
                    :class="{ active: selectedProvider === provider.value }"
                    @click="selectedProvider = provider.value"
                >
                    <div class="provider-icon">{{ provider.icon }}</div>
                    <div class="provider-name">{{ provider.name }}</div>
                </div>
            </div>
        </div>

        <!-- API配置 -->
        <div class="api-config-section">
            <h3>
                {{
                    providers.find((p) => p.value === selectedProvider)?.name
                }}
                配置
            </h3>

            <div v-if="selectedProvider === 'openai'" class="config-form">
                <input
                    v-model="apiConfig.openai.apiKey"
                    type="password"
                    placeholder="OpenAI API Key"
                />
            </div>

            <div v-if="selectedProvider === 'wenxin'" class="config-form">
                <input
                    v-model="apiConfig.wenxin.apiKey"
                    type="password"
                    placeholder="API Key"
                />
                <input
                    v-model="apiConfig.wenxin.secretKey"
                    type="password"
                    placeholder="Secret Key"
                />
            </div>

            <div v-if="selectedProvider === 'qianwen'" class="config-form">
                <input
                    v-model="apiConfig.qianwen.apiKey"
                    type="password"
                    placeholder="阿里云API Key"
                />
            </div>

            <div v-if="selectedProvider === 'deepseek'" class="config-form">
                <input
                    v-model="apiConfig.deepseek.apiKey"
                    type="password"
                    placeholder="Deepseek API Key"
                />
            </div>
        </div>

        <!-- 对话区域 -->
        <div class="chat-section">
            <h3>对话测试</h3>

            <div class="messages-area">
                <div
                    v-for="(msg, index) in messages"
                    :key="index"
                    class="message-item"
                    :class="msg.role"
                >
                    {{ msg.content }}
                </div>

                <div
                    v-if="streamedResponse"
                    class="message-item assistant streaming"
                >
                    {{ streamedResponse }}
                    <span class="cursor">|</span>
                </div>
            </div>

            <div class="input-area">
                <input
                    v-model="userInput"
                    @keyup.enter="sendMessage"
                    type="text"
                    placeholder="输入消息..."
                />
                <button @click="sendMessage" :disabled="isLoading">发送</button>
            </div>
        </div>
    </div>
</template>

<style scoped>
.model-switch-demo {
    padding: 20px;
    background: #f5f7fa;
    min-height: 100vh;
}

.demo-header {
    text-align: center;
    margin-bottom: 40px;
}

.demo-header h2 {
    margin: 0 0 10px 0;
    font-size: 32px;
    color: #303133;
}

.subtitle {
    margin: 0;
    font-size: 16px;
    color: #909399;
}

.provider-section,
.api-config-section,
.chat-section {
    margin-bottom: 30px;
    padding: 24px;
    background: white;
    border-radius: 8px;
}

h3 {
    margin: 0 0 20px 0;
    font-size: 18px;
    color: #303133;
}

.provider-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
}

.provider-card {
    padding: 24px;
    border: 2px solid #e4e7ed;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s;
    text-align: center;
}

.provider-card:hover {
    border-color: #409eff;
    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}

.provider-card.active {
    border-color: #409eff;
    background: #ecf5ff;
}

.provider-icon {
    font-size: 48px;
    margin-bottom: 12px;
}

.provider-name {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
}

.config-form {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.config-form input {
    padding: 12px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
}

.messages-area {
    min-height: 300px;
    max-height: 500px;
    overflow-y: auto;
    padding: 20px;
    background: #fafafa;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    margin-bottom: 20px;
}

.message-item {
    margin-bottom: 16px;
    padding: 12px 16px;
    border-radius: 8px;
    line-height: 1.6;
}

.message-item.user {
    background: #ecf5ff;
    color: #303133;
}

.message-item.assistant {
    background: #f5f7fa;
    color: #303133;
}

.cursor {
    display: inline-block;
    width: 2px;
    height: 1.2em;
    background: #409eff;
    margin-left: 2px;
    animation: blink 1s infinite;
}

@keyframes blink {
    0%,
    50% {
        opacity: 1;
    }
    51%,
    100% {
        opacity: 0;
    }
}

.input-area {
    display: flex;
    gap: 12px;
}

.input-area input {
    flex: 1;
    padding: 12px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
}

.input-area button {
    padding: 12px 32px;
    background: #409eff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
}

.input-area button:disabled {
    background: #c0c4cc;
    cursor: not-allowed;
}
</style>

四、Prompt工程设计

4.1 Prompt管理器

prompt-manager.js

javascript
export class PromptManager {
    constructor() {
        this.templates = new Map()
        this.variables = new Map()
    }

    // 注册Prompt模板
    registerTemplate(name, template) {
        this.templates.set(name, template)
    }

    // 获取Prompt模板
    getTemplate(name) {
        return this.templates.get(name)
    }

    // 渲染Prompt(替换变量)
    render(template, variables = {}) {
        let result = template

        for (const [key, value] of Object.entries(variables)) {
            const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g')
            result = result.replace(regex, value)
        }

        return result
    }

    // 构建系统提示
    buildSystemPrompt(role, rules = []) {
        let prompt = `你是一个${role}。\n\n`

        if (rules.length > 0) {
            prompt += '请遵循以下规则:\n'
            rules.forEach((rule, index) => {
                prompt += `${index + 1}. ${rule}\n`
            })
        }

        return prompt
    }

    // Few-shot示例
    buildFewShotPrompt(task, examples) {
        let prompt = `任务: ${task}\n\n`
        prompt += '示例:\n\n'

        examples.forEach((example, index) => {
            prompt += `示例${index + 1}:\n`
            prompt += `输入: ${example.input}\n`
            prompt += `输出: ${example.output}\n\n`
        })

        prompt += '现在请处理以下输入:\n'

        return prompt
    }

    // Chain of Thought
    buildCoTPrompt(question) {
        return `${question}\n\n让我们一步步思考:\n1.`
    }

    // 角色扮演
    buildRolePlayPrompt(role, scenario, task) {
        return `你现在扮演${role}。场景:${scenario}\n\n任务:${task}`
    }

    // 代码生成Prompt
    buildCodePrompt(language, description, requirements = []) {
        let prompt = `请用${language}编写代码。\n\n`
        prompt += `需求描述:${description}\n\n`

        if (requirements.length > 0) {
            prompt += '具体要求:\n'
            requirements.forEach((req, index) => {
                prompt += `${index + 1}. ${req}\n`
            })
        }

        prompt += '\n请提供完整的代码实现。'

        return prompt
    }

    // 优化Prompt(添加约束)
    optimizePrompt(basePrompt, constraints = {}) {
        let optimized = basePrompt

        if (constraints.length) {
            optimized += `\n\n请将回答控制在${constraints.length}字以内。`
        }

        if (constraints.format) {
            optimized += `\n\n输出格式:${constraints.format}`
        }

        if (constraints.tone) {
            optimized += `\n\n语气:${constraints.tone}`
        }

        if (constraints.language) {
            optimized += `\n\n请使用${constraints.language}回答。`
        }

        return optimized
    }

    // 预设Prompt模板
    getPresetPrompts() {
        return {
            translator: {
                name: '翻译助手',
                system: '你是一个专业的翻译助手,擅长中英文互译。',
                template: '请将以下文本翻译成{{target_language}}:\n\n{{text}}',
            },
            coder: {
                name: '代码助手',
                system: '你是一个经验丰富的程序员,擅长多种编程语言。',
                template:
                    '请用{{language}}实现以下功能:\n\n{{description}}\n\n要求:\n{{requirements}}',
            },
            writer: {
                name: '写作助手',
                system: '你是一个专业的内容创作者,擅长各类文案写作。',
                template:
                    '请撰写一篇关于{{topic}}的{{type}},要求:\n1. 字数约{{word_count}}字\n2. 风格:{{style}}\n3. 受众:{{audience}}',
            },
            analyzer: {
                name: '数据分析师',
                system: '你是一个数据分析专家,擅长从数据中提取洞察。',
                template:
                    '请分析以下数据并给出结论:\n\n{{data}}\n\n分析维度:\n{{dimensions}}',
            },
            teacher: {
                name: '教学助手',
                system: '你是一个耐心的老师,擅长用简单的语言解释复杂概念。',
                template:
                    '请解释{{concept}},要求:\n1. 用通俗易懂的语言\n2. 举具体例子\n3. 适合{{level}}水平的学习者',
            },
        }
    }
}

export default new PromptManager()

4.2 Prompt工程演示组件

PromptEngineeringDemo.vue

vue
<script setup>
import { ref, computed } from 'vue'
import promptManager from './prompt-manager.js'

const selectedPreset = ref('translator')
const presets = promptManager.getPresetPrompts()

const variables = ref({
    target_language: '英文',
    text: '人工智能正在改变世界',
    language: 'JavaScript',
    description: '实现一个防抖函数',
    requirements: '支持立即执行\n支持取消',
    topic: 'Vue 3新特性',
    type: '技术博客',
    word_count: '1000',
    style: '专业严谨',
    audience: '前端开发者',
})

const generatedPrompt = computed(() => {
    const preset = presets[selectedPreset.value]
    if (!preset) return ''

    return promptManager.render(preset.template, variables.value)
})

const fullPrompt = computed(() => {
    const preset = presets[selectedPreset.value]
    if (!preset) return ''

    return `系统提示:\n${preset.system}\n\n用户输入:\n${generatedPrompt.value}`
})

// Prompt优化建议
const optimizationTips = [
    {
        category: '明确性',
        tips: [
            '明确任务目标和期望输出',
            '提供具体的要求和约束',
            '避免模糊和歧义的表述',
        ],
    },
    {
        category: '上下文',
        tips: ['提供必要的背景信息', '给出示例输入和输出', '说明使用场景'],
    },
    {
        category: '结构',
        tips: [
            '使用清晰的格式和分段',
            '用序号列举多个要求',
            '重要信息放在前面',
        ],
    },
    {
        category: '约束',
        tips: ['限制输出长度', '指定输出格式', '规定语气和风格'],
    },
]

// Few-shot示例
const fewShotExample = ref({
    task: '情感分析',
    examples: [
        { input: '这个产品太棒了!', output: '正面' },
        { input: '质量很差,不推荐。', output: '负面' },
        { input: '还行吧,一般般。', output: '中性' },
    ],
    testInput: '物流很快,包装也很好。',
})

const fewShotPrompt = computed(() => {
    return (
        promptManager.buildFewShotPrompt(
            fewShotExample.value.task,
            fewShotExample.value.examples
        ) + fewShotExample.value.testInput
    )
})

// Chain of Thought示例
const cotQuestion = ref(
    '一个班级有30名学生,其中60%是男生,男生中有40%戴眼镜,问戴眼镜的男生有多少人?'
)

const cotPrompt = computed(() => {
    return promptManager.buildCoTPrompt(cotQuestion.value)
})

// 复制Prompt
const copyPrompt = (text) => {
    navigator.clipboard.writeText(text).then(() => {
        alert('已复制到剪贴板')
    })
}
</script>

<template>
    <div class="prompt-demo">
        <div class="demo-header">
            <h2>Prompt工程设计</h2>
            <p class="subtitle">优化Prompt提升AI输出质量</p>
        </div>

        <!-- Prompt模板 -->
        <div class="template-section">
            <h3>预设模板</h3>

            <div class="template-tabs">
                <button
                    v-for="(preset, key) in presets"
                    :key="key"
                    @click="selectedPreset = key"
                    class="tab-btn"
                    :class="{ active: selectedPreset === key }"
                >
                    {{ preset.name }}
                </button>
            </div>

            <div class="template-content">
                <h4>系统提示</h4>
                <div class="system-prompt">
                    {{ presets[selectedPreset].system }}
                </div>

                <h4>变量配置</h4>
                <div class="variables-form">
                    <div
                        v-for="(value, key) in variables"
                        :key="key"
                        v-show="
                            presets[selectedPreset].template.includes(
                                `{{${key}}}`
                            )
                        "
                        class="variable-item"
                    >
                        <label>{{ key }}:</label>
                        <textarea
                            v-if="
                                key === 'requirements' || key === 'dimensions'
                            "
                            v-model="variables[key]"
                            rows="3"
                        ></textarea>
                        <input v-else v-model="variables[key]" type="text" />
                    </div>
                </div>

                <h4>生成的Prompt</h4>
                <div class="generated-prompt">
                    <pre>{{ fullPrompt }}</pre>
                    <button @click="copyPrompt(fullPrompt)" class="copy-btn">
                        复制
                    </button>
                </div>
            </div>
        </div>

        <!-- Few-shot学习 -->
        <div class="fewshot-section">
            <h3>Few-shot学习</h3>
            <p class="section-desc">通过示例帮助AI理解任务</p>

            <div class="fewshot-config">
                <div class="config-item">
                    <label>任务描述:</label>
                    <input v-model="fewShotExample.task" type="text" />
                </div>

                <div class="examples-list">
                    <h4>示例</h4>
                    <div
                        v-for="(example, index) in fewShotExample.examples"
                        :key="index"
                        class="example-item"
                    >
                        <input
                            v-model="example.input"
                            type="text"
                            placeholder="输入"
                        />
                        <span>→</span>
                        <input
                            v-model="example.output"
                            type="text"
                            placeholder="输出"
                        />
                    </div>
                </div>

                <div class="config-item">
                    <label>测试输入:</label>
                    <input v-model="fewShotExample.testInput" type="text" />
                </div>

                <div class="prompt-output">
                    <h4>生成的Prompt</h4>
                    <pre>{{ fewShotPrompt }}</pre>
                    <button @click="copyPrompt(fewShotPrompt)" class="copy-btn">
                        复制
                    </button>
                </div>
            </div>
        </div>

        <!-- Chain of Thought -->
        <div class="cot-section">
            <h3>Chain of Thought (思维链)</h3>
            <p class="section-desc">引导AI逐步推理</p>

            <div class="cot-config">
                <div class="config-item">
                    <label>问题:</label>
                    <textarea v-model="cotQuestion" rows="3"></textarea>
                </div>

                <div class="prompt-output">
                    <h4>带思维链的Prompt</h4>
                    <pre>{{ cotPrompt }}</pre>
                    <button @click="copyPrompt(cotPrompt)" class="copy-btn">
                        复制
                    </button>
                </div>
            </div>
        </div>

        <!-- 优化建议 -->
        <div class="tips-section">
            <h3>Prompt优化建议</h3>

            <div class="tips-grid">
                <div
                    v-for="category in optimizationTips"
                    :key="category.category"
                    class="tip-card"
                >
                    <h4>{{ category.category }}</h4>
                    <ul>
                        <li v-for="tip in category.tips" :key="tip">
                            {{ tip }}
                        </li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- 最佳实践 -->
        <div class="practices-section">
            <h3>Prompt工程最佳实践</h3>

            <div class="practice-list">
                <div class="practice-item">
                    <h4>1. 明确角色定位</h4>
                    <div class="example-box">
                        <div class="bad">❌ "帮我写代码"</div>
                        <div class="good">
                            ✅ "你是一个资深的前端工程师,请帮我用Vue 3实现..."
                        </div>
                    </div>
                </div>

                <div class="practice-item">
                    <h4>2. 提供具体要求</h4>
                    <div class="example-box">
                        <div class="bad">❌ "写一篇文章"</div>
                        <div class="good">
                            ✅ "写一篇800字的技术博客,介绍Vue
                            3新特性,面向初学者,语气轻松"
                        </div>
                    </div>
                </div>

                <div class="practice-item">
                    <h4>3. 使用分隔符</h4>
                    <div class="example-box">
                        <div class="good">
                            ✅ 使用 ### 或 """ 分隔不同部分,避免混淆
                        </div>
                    </div>
                </div>

                <div class="practice-item">
                    <h4>4. 要求逐步思考</h4>
                    <div class="example-box">
                        <div class="good">
                            ✅ "让我们一步步分析:首先...然后...最后..."
                        </div>
                    </div>
                </div>

                <div class="practice-item">
                    <h4>5. 指定输出格式</h4>
                    <div class="example-box">
                        <div class="good">
                            ✅ "请以JSON格式输出,包含title、content、tags字段"
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<style scoped>
.prompt-demo {
    padding: 20px;
    background: #f5f7fa;
    min-height: 100vh;
}

.demo-header {
    text-align: center;
    margin-bottom: 40px;
}

.demo-header h2 {
    margin: 0 0 10px 0;
    font-size: 32px;
    color: #303133;
}

.subtitle {
    margin: 0;
    font-size: 16px;
    color: #909399;
}

.template-section,
.fewshot-section,
.cot-section,
.tips-section,
.practices-section {
    margin-bottom: 30px;
    padding: 24px;
    background: white;
    border-radius: 8px;
}

h3 {
    margin: 0 0 20px 0;
    font-size: 18px;
    color: #303133;
}

h4 {
    margin: 0 0 12px 0;
    font-size: 16px;
    color: #606266;
}

.section-desc {
    margin: -10px 0 20px 0;
    font-size: 14px;
    color: #909399;
}

/* 模板标签 */
.template-tabs {
    display: flex;
    gap: 8px;
    margin-bottom: 24px;
    flex-wrap: wrap;
}

.tab-btn {
    padding: 10px 20px;
    background: #f5f7fa;
    border: 1px solid #e4e7ed;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: all 0.3s;
}

.tab-btn:hover {
    border-color: #409eff;
    color: #409eff;
}

.tab-btn.active {
    background: #409eff;
    color: white;
    border-color: #409eff;
}

.system-prompt {
    padding: 16px;
    background: #f0f9ff;
    border-left: 4px solid #409eff;
    border-radius: 4px;
    margin-bottom: 24px;
    font-size: 14px;
    line-height: 1.6;
    color: #303133;
}

.variables-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-bottom: 24px;
}

.variable-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.variable-item label {
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.variable-item input,
.variable-item textarea {
    padding: 10px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
    font-family: inherit;
}

.generated-prompt,
.prompt-output {
    position: relative;
    margin-bottom: 24px;
}

.generated-prompt pre,
.prompt-output pre {
    margin: 0;
    padding: 20px;
    background: #282c34;
    border-radius: 8px;
    color: #abb2bf;
    font-size: 13px;
    line-height: 1.6;
    overflow-x: auto;
    font-family: 'Courier New', monospace;
    white-space: pre-wrap;
}

.copy-btn {
    position: absolute;
    top: 12px;
    right: 12px;
    padding: 6px 16px;
    background: #409eff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 13px;
}

/* Few-shot */
.examples-list {
    margin: 20px 0;
}

.example-item {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
}

.example-item input {
    flex: 1;
    padding: 10px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
}

.example-item span {
    color: #409eff;
    font-size: 18px;
}

.config-item {
    margin-bottom: 20px;
}

.config-item label {
    display: block;
    margin-bottom: 8px;
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.config-item input,
.config-item textarea {
    width: 100%;
    padding: 10px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
    font-family: inherit;
}

/* 优化建议 */
.tips-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 20px;
}

.tip-card {
    padding: 20px;
    background: #f5f7fa;
    border-radius: 8px;
    border-left: 4px solid #409eff;
}

.tip-card h4 {
    margin: 0 0 16px 0;
    font-size: 16px;
    color: #409eff;
}

.tip-card ul {
    margin: 0;
    padding-left: 20px;
}

.tip-card li {
    font-size: 14px;
    line-height: 2;
    color: #606266;
}

/* 最佳实践 */
.practice-list {
    display: flex;
    flex-direction: column;
    gap: 24px;
}

.practice-item {
    padding: 20px;
    background: #f5f7fa;
    border-radius: 8px;
}

.practice-item h4 {
    margin: 0 0 16px 0;
    font-size: 16px;
    color: #303133;
}

.example-box {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.bad,
.good {
    padding: 12px 16px;
    border-radius: 6px;
    font-size: 14px;
    line-height: 1.6;
}

.bad {
    background: #fef0f0;
    color: #f56c6c;
    border-left: 4px solid #f56c6c;
}

.good {
    background: #f0f9ff;
    color: #409eff;
    border-left: 4px solid #409eff;
}
</style>

五、Token计数与限流

5.1 Token计数器

token-counter.js

javascript
export class TokenCounter {
    constructor() {
        // 简化的Token估算(实际项目应使用tiktoken库)
        this.chineseTokenRatio = 1.5 // 中文字符约1.5 token
        this.englishTokenRatio = 0.25 // 英文单词约0.25 token
    }

    // 估算Token数量
    estimateTokens(text) {
        // 分离中英文
        const chinese = text.match(/[\u4e00-\u9fa5]/g) || []
        const english = text.match(/[a-zA-Z]+/g) || []
        const numbers = text.match(/\d+/g) || []

        const chineseTokens = chinese.length * this.chineseTokenRatio
        const englishTokens = english.length * this.englishTokenRatio
        const numberTokens = numbers.reduce(
            (sum, num) => sum + num.length * 0.5,
            0
        )

        return Math.ceil(chineseTokens + englishTokens + numberTokens)
    }

    // 估算消息列表的Token数
    estimateMessagesTokens(messages) {
        let total = 0

        for (const message of messages) {
            // 每条消息有固定开销
            total += 4 // role和结构开销
            total += this.estimateTokens(message.content)
        }

        total += 2 // 回复的固定开销

        return total
    }

    // 检查是否超出限制
    checkLimit(tokens, limit) {
        return {
            withinLimit: tokens <= limit,
            tokens: tokens,
            limit: limit,
            remaining: Math.max(0, limit - tokens),
            percentage: Math.min(100, (tokens / limit) * 100),
        }
    }

    // 截断文本以适应Token限制
    truncateToLimit(text, limit) {
        let truncated = text
        let tokens = this.estimateTokens(truncated)

        while (tokens > limit && truncated.length > 0) {
            // 每次减少10%
            const cutLength = Math.ceil(truncated.length * 0.1)
            truncated = truncated.slice(0, -cutLength)
            tokens = this.estimateTokens(truncated)
        }

        return truncated
    }

    // 计算成本(基于GPT-3.5定价)
    calculateCost(tokens, model = 'gpt-3.5-turbo') {
        const pricing = {
            'gpt-3.5-turbo': {
                input: 0.0015 / 1000,
                output: 0.002 / 1000,
            },
            'gpt-4': {
                input: 0.03 / 1000,
                output: 0.06 / 1000,
            },
        }

        const price = pricing[model] || pricing['gpt-3.5-turbo']

        return {
            inputCost: tokens * price.input,
            outputCost: tokens * price.output,
            totalCost: tokens * (price.input + price.output),
        }
    }
}

export default new TokenCounter()

5.2 速率限制器

rate-limiter.js

javascript
export class RateLimiter {
    constructor(options = {}) {
        this.maxRequests = options.maxRequests || 60 // 每分钟最大请求数
        this.windowMs = options.windowMs || 60000 // 时间窗口(毫秒)
        this.requests = []
    }

    // 检查是否允许请求
    allowRequest() {
        const now = Date.now()

        // 清理过期记录
        this.requests = this.requests.filter(
            (time) => now - time < this.windowMs
        )

        // 检查是否超限
        if (this.requests.length >= this.maxRequests) {
            const oldestRequest = this.requests[0]
            const waitTime = this.windowMs - (now - oldestRequest)

            return {
                allowed: false,
                waitTime: waitTime,
                remaining: 0,
                resetTime: oldestRequest + this.windowMs,
            }
        }

        // 记录请求
        this.requests.push(now)

        return {
            allowed: true,
            remaining: this.maxRequests - this.requests.length,
            resetTime: now + this.windowMs,
        }
    }

    // 获取当前状态
    getStatus() {
        const now = Date.now()

        // 清理过期记录
        this.requests = this.requests.filter(
            (time) => now - time < this.windowMs
        )

        return {
            current: this.requests.length,
            limit: this.maxRequests,
            remaining: this.maxRequests - this.requests.length,
            resetTime:
                this.requests.length > 0
                    ? this.requests[0] + this.windowMs
                    : now + this.windowMs,
        }
    }

    // 重置限制器
    reset() {
        this.requests = []
    }

    // 等待直到可以请求
    async waitForSlot() {
        const status = this.allowRequest()

        if (status.allowed) {
            return true
        }

        await new Promise((resolve) => setTimeout(resolve, status.waitTime))
        return this.waitForSlot()
    }
}

export default new RateLimiter()

5.3 Token计数演示组件

TokenCounterDemo.vue

vue
<script setup>
import { ref, computed } from 'vue'
import tokenCounter from './token-counter.js'
import rateLimiter from './rate-limiter.js'

const inputText = ref('这是一个测试文本。This is a test text.')
const tokenLimit = ref(4000)
const selectedModel = ref('gpt-3.5-turbo')

const messages = ref([
    { role: 'system', content: '你是一个有帮助的助手。' },
    { role: 'user', content: '介绍一下Vue 3的新特性' },
    {
        role: 'assistant',
        content:
            'Vue 3引入了Composition API、性能优化、更好的TypeScript支持等新特性。',
    },
])

// Token统计
const tokenStats = computed(() => {
    const tokens = tokenCounter.estimateTokens(inputText.value)
    const check = tokenCounter.checkLimit(tokens, tokenLimit.value)
    const cost = tokenCounter.calculateCost(tokens, selectedModel.value)

    return {
        tokens,
        check,
        cost,
    }
})

// 消息Token统计
const messagesTokens = computed(() => {
    return tokenCounter.estimateMessagesTokens(messages.value)
})

// 速率限制状态
const rateLimitStatus = ref(null)

const checkRateLimit = () => {
    rateLimitStatus.value = rateLimiter.getStatus()
}

const testRateLimit = () => {
    const result = rateLimiter.allowRequest()

    if (result.allowed) {
        alert(`请求成功!剩余: ${result.remaining}`)
    } else {
        alert(`请求被限流!等待时间: ${Math.ceil(result.waitTime / 1000)}秒`)
    }

    checkRateLimit()
}

// 截断文本
const truncateText = () => {
    inputText.value = tokenCounter.truncateToLimit(
        inputText.value,
        tokenLimit.value
    )
}

// 添加消息
const addMessage = () => {
    messages.value.push({
        role: 'user',
        content: '这是一条新消息',
    })
}
</script>

<template>
    <div class="token-counter-demo">
        <div class="demo-header">
            <h2>Token计数与限流</h2>
            <p class="subtitle">管理API调用的Token和频率</p>
        </div>

        <!-- Token计数 -->
        <div class="counter-section">
            <h3>Token计数器</h3>

            <div class="input-group">
                <label>输入文本:</label>
                <textarea
                    v-model="inputText"
                    rows="5"
                    placeholder="输入文本..."
                ></textarea>
            </div>

            <div class="config-group">
                <div class="config-item">
                    <label>Token限制:</label>
                    <input v-model.number="tokenLimit" type="number" />
                </div>

                <div class="config-item">
                    <label>模型:</label>
                    <select v-model="selectedModel">
                        <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
                        <option value="gpt-4">GPT-4</option>
                    </select>
                </div>

                <button @click="truncateText" class="truncate-btn">
                    截断至限制
                </button>
            </div>

            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-label">Token数量</div>
                    <div class="stat-value">{{ tokenStats.tokens }}</div>
                </div>

                <div class="stat-card">
                    <div class="stat-label">使用率</div>
                    <div class="stat-value">
                        {{ tokenStats.check.percentage.toFixed(1) }}%
                    </div>
                    <div class="progress-bar">
                        <div
                            class="progress-fill"
                            :style="{
                                width: tokenStats.check.percentage + '%',
                            }"
                            :class="{
                                normal: tokenStats.check.percentage < 80,
                                warning:
                                    tokenStats.check.percentage >= 80 &&
                                    tokenStats.check.percentage < 100,
                                danger: tokenStats.check.percentage >= 100,
                            }"
                        ></div>
                    </div>
                </div>

                <div class="stat-card">
                    <div class="stat-label">剩余Token</div>
                    <div class="stat-value">
                        {{ tokenStats.check.remaining }}
                    </div>
                </div>

                <div class="stat-card">
                    <div class="stat-label">预估费用</div>
                    <div class="stat-value">
                        ${{ tokenStats.cost.totalCost.toFixed(6) }}
                    </div>
                </div>
            </div>
        </div>

        <!-- 消息Token统计 -->
        <div class="messages-section">
            <h3>对话Token统计</h3>

            <div class="messages-list">
                <div
                    v-for="(msg, index) in messages"
                    :key="index"
                    class="message-item"
                >
                    <div class="message-role">{{ msg.role }}</div>
                    <div class="message-content">{{ msg.content }}</div>
                    <div class="message-tokens">
                        ~{{ tokenCounter.estimateTokens(msg.content) }} tokens
                    </div>
                </div>
            </div>

            <div class="messages-stats">
                <button @click="addMessage" class="add-btn">添加消息</button>
                <div class="total-tokens">
                    总计: {{ messagesTokens }} tokens
                </div>
            </div>
        </div>

        <!-- 速率限制 -->
        <div class="ratelimit-section">
            <h3>速率限制</h3>

            <div class="ratelimit-config">
                <div class="info-box">
                    <p>当前限制: 60 请求/分钟</p>
                    <p>时间窗口: 60 秒</p>
                </div>

                <div class="action-buttons">
                    <button @click="testRateLimit" class="test-btn">
                        测试请求
                    </button>
                    <button @click="checkRateLimit" class="check-btn">
                        检查状态
                    </button>
                </div>
            </div>

            <div v-if="rateLimitStatus" class="ratelimit-status">
                <div class="status-grid">
                    <div class="status-item">
                        <label>当前请求数:</label>
                        <span>{{ rateLimitStatus.current }}</span>
                    </div>
                    <div class="status-item">
                        <label>最大限制:</label>
                        <span>{{ rateLimitStatus.limit }}</span>
                    </div>
                    <div class="status-item">
                        <label>剩余额度:</label>
                        <span>{{ rateLimitStatus.remaining }}</span>
                    </div>
                    <div class="status-item">
                        <label>重置时间:</label>
                        <span>{{
                            new Date(
                                rateLimitStatus.resetTime
                            ).toLocaleTimeString()
                        }}</span>
                    </div>
                </div>
            </div>
        </div>

        <!-- 成本说明 -->
        <div class="pricing-section">
            <h3>API定价参考</h3>

            <table class="pricing-table">
                <thead>
                    <tr>
                        <th>模型</th>
                        <th>输入价格</th>
                        <th>输出价格</th>
                        <th>1000 tokens费用</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>GPT-3.5 Turbo</td>
                        <td>$0.0015/1K tokens</td>
                        <td>$0.002/1K tokens</td>
                        <td>~$0.002</td>
                    </tr>
                    <tr>
                        <td>GPT-4</td>
                        <td>$0.03/1K tokens</td>
                        <td>$0.06/1K tokens</td>
                        <td>~$0.045</td>
                    </tr>
                    <tr>
                        <td>文心一言</td>
                        <td>¥0.012/千tokens</td>
                        <td>-</td>
                        <td>~¥0.012</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

<style scoped>
.token-counter-demo {
    padding: 20px;
    background: #f5f7fa;
    min-height: 100vh;
}

.demo-header {
    text-align: center;
    margin-bottom: 40px;
}

.demo-header h2 {
    margin: 0 0 10px 0;
    font-size: 32px;
    color: #303133;
}

.subtitle {
    margin: 0;
    font-size: 16px;
    color: #909399;
}

.counter-section,
.messages-section,
.ratelimit-section,
.pricing-section {
    margin-bottom: 30px;
    padding: 24px;
    background: white;
    border-radius: 8px;
}

h3 {
    margin: 0 0 20px 0;
    font-size: 18px;
    color: #303133;
}

.input-group {
    margin-bottom: 20px;
}

.input-group label {
    display: block;
    margin-bottom: 8px;
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.input-group textarea {
    width: 100%;
    padding: 12px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
    font-family: inherit;
}

.config-group {
    display: flex;
    gap: 16px;
    align-items: flex-end;
    margin-bottom: 20px;
    flex-wrap: wrap;
}

.config-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.config-item label {
    font-size: 14px;
    color: #606266;
    font-weight: 600;
}

.config-item input,
.config-item select {
    padding: 10px;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    font-size: 14px;
}

.truncate-btn {
    padding: 10px 24px;
    background: #e6a23c;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}

.stat-card {
    padding: 20px;
    background: #f5f7fa;
    border-radius: 8px;
    text-align: center;
}

.stat-label {
    font-size: 13px;
    color: #909399;
    margin-bottom: 8px;
}

.stat-value {
    font-size: 28px;
    font-weight: 700;
    color: #409eff;
}

.progress-bar {
    margin-top: 12px;
    height: 8px;
    background: #e4e7ed;
    border-radius: 4px;
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    transition:
        width 0.3s,
        background 0.3s;
}

.progress-fill.normal {
    background: #67c23a;
}

.progress-fill.warning {
    background: #e6a23c;
}

.progress-fill.danger {
    background: #f56c6c;
}

.messages-list {
    margin-bottom: 20px;
}

.message-item {
    padding: 16px;
    background: #f5f7fa;
    border-radius: 8px;
    margin-bottom: 12px;
}

.message-role {
    font-size: 13px;
    color: #909399;
    font-weight: 600;
    margin-bottom: 8px;
}

.message-content {
    font-size: 14px;
    line-height: 1.6;
    color: #303133;
    margin-bottom: 8px;
}

.message-tokens {
    font-size: 12px;
    color: #409eff;
}

.messages-stats {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.add-btn {
    padding: 10px 24px;
    background: #409eff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
}

.total-tokens {
    font-size: 16px;
    font-weight: 600;
    color: #409eff;
}

.ratelimit-config {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}

.info-box p {
    margin: 0 0 8px 0;
    font-size: 14px;
    color: #606266;
}

.action-buttons {
    display: flex;
    gap: 12px;
}

.test-btn,
.check-btn {
    padding: 10px 24px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
}

.test-btn {
    background: #409eff;
    color: white;
}

.check-btn {
    background: #67c23a;
    color: white;
}

.ratelimit-status {
    padding: 20px;
    background: #f5f7fa;
    border-radius: 8px;
}

.status-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}

.status-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px;
    background: white;
    border-radius: 4px;
}

.status-item label {
    font-size: 14px;
    color: #606266;
}

.status-item span {
    font-size: 16px;
    font-weight: 600;
    color: #409eff;
}

.pricing-table {
    width: 100%;
    border-collapse: collapse;
}

.pricing-table th,
.pricing-table td {
    padding: 12px;
    text-align: left;
    border: 1px solid #e4e7ed;
}

.pricing-table th {
    background: #f5f7fa;
    font-weight: 600;
    color: #303133;
}

.pricing-table td {
    color: #606266;
}
</style>

六、简历描述模板

AI能力集成开发 (2024.03 - 至今)

负责公司AI能力的前端集成,实现多个大模型的统一接入,支持OpenAI、文心一言、通义千问等主流模型。

核心职责

  • 封装统一的AI模型适配器,支持5+种主流大模型
  • 实现流式响应处理,优化用户体验
  • 开发Prompt工程工具,提升AI输出质量
  • 实现Token计数和成本统计系统
  • 建立速率限制和错误重试机制
技术实现
  • 使用适配器模式统一不同模型的API差异
  • 通过SSE和Fetch API实现流式数据接收
  • 开发Prompt模板系统,支持变量替换和优化建议
  • 实现Token估算算法,误差控制在5%以内
  • 配置滑动窗口速率限制器,支持自动限流
项目成果
  • 成功集成OpenAI、文心一言、通义千问、Deepseek
  • 流式响应首字显示延迟<50ms,体验优异
  • Prompt模板库包含20+场景,复用率80%
  • Token估算准确率95%,成本控制精确
  • API成功率99.5%,自动重试机制有效

七、SOP标准回答

面试问题: 如何实现多个AI模型的统一接入?

标准回答

"多模型统一接入的核心是适配器模式。不同AI服务的API差异很大,需要统一封装。

我的实现分三层。第一层是统一接口层,定义chat和chatStream两个核心方法。所有模型适配器都实现这两个接口。调用方不需要关心底层是OpenAI还是文心一言,代码完全一样。

第二层是模型适配器。每个模型有自己的Adapter类,比如OpenAIAdapter、WenxinAdapter。适配器负责三件事:一是参数转换,把统一格式转成该模型的API格式。二是请求处理,调用实际的API。三是响应转换,把返回结果统一格式。

第三层是错误处理。不同模型的错误码和格式不同,需要统一。我定义了标准错误类型,包括认证失败、超限、服务不可用等。适配器捕获原始错误后转换成标准格式。

具体到实现,以文心一言为例。它需要先获取access_token,然后才能调用chat接口。我在WenxinAdapter里实现了getAccessToken方法,自动管理token的获取和缓存。调用方完全感知不到这个细节。

消息格式转换也是重点。OpenAI支持system、user、assistant三种role,但文心一言只有user和assistant。我在convertMessages方法里处理这个差异,把system消息合并到第一条user消息里。

流式响应的适配更复杂。OpenAI返回的是data: {delta:{content:''}}格式,文心一言是data: {result:''}格式。我统一处理成{content:''}格式给上层,屏蔽差异。

还有成本问题。不同模型定价不同,我实现了统一的成本计算接口。根据model参数查pricing配置,自动计算。这样业务方可以实时看到不同模型的成本对比,做决策更方便。

实际效果很好。新增一个模型只需要写个Adapter,业务代码完全不用改。我们现在支持5个模型,切换只要改个配置。上线后,根据不同场景选择最合适的模型,成本降低了30%。"

面试问题: 如何优化Prompt提升AI回答质量?

标准回答

"Prompt工程是提升AI输出质量的关键。我总结了几个核心原则。

第一是明确角色。不要直接问问题,而是先定义角色。比如'你是一个资深的前端工程师'比直接问'如何优化性能'效果好很多。AI有了角色定位后,回答会更专业更有针对性。

第二是提供上下文。不能假设AI知道所有背景。要把必要信息都说清楚。比如问Vue问题,要说明是Vue 2还是Vue 3,项目规模多大,团队水平怎样。上下文越充分,回答越准确。

第三是明确要求。不要问'如何做',要说'用什么语言做'、'代码风格是什么'、'要不要写注释'。约束越具体,输出越符合预期。

第四是使用Few-shot示例。对于格式化任务,给几个输入输出示例效果特别好。比如做情感分析,给3个正面、负面、中性的例子,AI就能准确分类新输入。

第五是要求逐步思考。对于复杂问题,在Prompt里加上'让我们一步步分析'这种引导,AI会先分解问题再回答,准确率显著提升。这叫Chain of Thought。

第六是指定输出格式。如果要JSON,明确说'以JSON格式输出,包含title和content字段'。如果要Markdown,说'用Markdown格式,代码用```包裹'。格式约束能避免很多解析问题。

实际项目中,我建立了Prompt模板库。常见场景像翻译、代码生成、文案写作,都有模板。模板里定义系统提示词和用户输入格式,只需要填充变量就能用。这样既保证质量又提高效率。

还做了A/B测试。同个任务,用不同Prompt测试,对比输出质量。最终总结出每个场景的最优Prompt,形成最佳实践文档。新人来了按文档写Prompt,不用摸索。

效果很明显。以代码生成为例,优化Prompt后,生成代码的可用率从60%提升到85%。用户满意度也大幅提高。"


八、难点与亮点分析

难点1: 如何处理不同模型的流式响应格式差异?

问题场景: OpenAI、文心一言、通义千问的流式响应格式完全不同。

解决方案

javascript
class StreamAdapter {
    // 统一的流式处理
    async handleStream(response, provider, callbacks) {
        const reader = response.body.getReader()
        const decoder = new TextDecoder()
        let buffer = ''

        while (true) {
            const { done, value } = await reader.read()
            if (done) break

            const chunk = decoder.decode(value, { stream: true })
            buffer += chunk

            // 根据provider选择解析策略
            const parser = this.getParser(provider)
            const messages = parser.parse(buffer)

            for (const msg of messages) {
                if (msg.content) {
                    callbacks.onChunk?.(msg.content)
                }
            }

            buffer = parser.getRemainingBuffer()
        }
    }

    getParser(provider) {
        const parsers = {
            openai: new OpenAIStreamParser(),
            wenxin: new WenxinStreamParser(),
            qianwen: new QianwenStreamParser(),
        }
        return parsers[provider]
    }
}

// OpenAI解析器
class OpenAIStreamParser {
    parse(buffer) {
        const messages = []
        const lines = buffer.split('\n')

        for (const line of lines) {
            if (line.startsWith('data: ')) {
                try {
                    const data = JSON.parse(line.slice(6))
                    const content = data.choices[0]?.delta?.content
                    if (content) {
                        messages.push({ content })
                    }
                } catch (e) {}
            }
        }

        return messages
    }

    getRemainingBuffer() {
        return ''
    }
}

// 文心一言解析器
class WenxinStreamParser {
    parse(buffer) {
        const messages = []
        const lines = buffer.split('\n')

        for (const line of lines) {
            if (line.startsWith('data: ')) {
                try {
                    const data = JSON.parse(line.slice(6))
                    if (data.result) {
                        messages.push({ content: data.result })
                    }
                } catch (e) {}
            }
        }

        return messages
    }

    getRemainingBuffer() {
        return ''
    }
}

难点2: 如何准确计算Token数量?

问题场景: Token计算直接影响成本估算,简单字符统计不准确。

解决方案

javascript
class AccurateTokenCounter {
  constructor() {
    // 加载分词表(简化版)
    this.tokenizer = this.loadTokenizer()
  }

  // BPE编码(简化实现)
  encode(text) {
    const tokens = []

    // 按字符分词
    const chars = Array.from(text)

    for (const char of chars) {
      // 中文字符
      if (/[\u4e00-\u9fa5]/.test(char)) {
        tokens.push(this.getChinese Token(char))
      }
      // 英文单词
      else if (/[a-zA-Z]/.test(char)) {
        tokens.push(this.getEnglishToken(char))
      }
      // 数字
      else if (/\d/.test(char)) {
        tokens.push(this.getNumberToken(char))
      }
      // 标点和空格
      else {
        tokens.push(this.getSpecialToken(char))
      }
    }

    return tokens
  }

  // 精确计数
  count(text) {
    const tokens = this.encode(text)
    return tokens.length
  }

  // 考虑模型差异
  countForModel(text, model) {
    const baseCount = this.count(text)

    // 不同模型的Token计算规则不同
    const modelFactors = {
      'gpt-3.5-turbo': 1.0,
      'gpt-4': 1.0,
      'wenxin': 1.2, // 文心一言Token略多
      'qianwen': 1.1
    }

    const factor = modelFactors[model] || 1.0
    return Math.ceil(baseCount * factor)
  }
}

亮点: 智能Prompt优化

创新点

  • 根据历史效果自动优化Prompt
  • A/B测试不同Prompt方案
  • 积累最佳实践库
javascript
class SmartPromptOptimizer {
    constructor() {
        this.history = []
        this.templates = new Map()
    }

    // 记录Prompt效果
    recordResult(prompt, result, feedback) {
        this.history.push({
            prompt,
            result,
            feedback, // 用户评分
            timestamp: Date.now(),
        })

        // 分析并优化
        this.analyzeAndOptimize()
    }

    // 分析历史数据
    analyzeAndOptimize() {
        // 找出高分Prompt的共同特征
        const highScorePrompts = this.history
            .filter((h) => h.feedback >= 4)
            .map((h) => h.prompt)

        // 提取关键模式
        const patterns = this.extractPatterns(highScorePrompts)

        // 更新模板库
        this.updateTemplates(patterns)
    }

    // 提取有效模式
    extractPatterns(prompts) {
        const patterns = {
            hasRole: 0,
            hasExamples: 0,
            hasConstraints: 0,
            hasFormat: 0,
        }

        for (const prompt of prompts) {
            if (prompt.includes('你是')) patterns.hasRole++
            if (prompt.includes('示例')) patterns.hasExamples++
            if (prompt.includes('要求')) patterns.hasConstraints++
            if (prompt.includes('格式')) patterns.hasFormat++
        }

        return patterns
    }

    // 生成优化建议
    getSuggestions(prompt) {
        const suggestions = []

        if (!prompt.includes('你是')) {
            suggestions.push('建议添加角色定义')
        }

        if (prompt.length < 50) {
            suggestions.push('Prompt太简短,建议添加更多上下文')
        }

        if (!prompt.includes('格式')) {
            suggestions.push('建议指定输出格式')
        }

        return suggestions
    }
}