一、技术实现方案
1.1 AI集成架构
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
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
<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
// 统一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
<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
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
<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
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
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
<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、文心一言、通义千问的流式响应格式完全不同。
解决方案
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计算直接影响成本估算,简单字符统计不准确。
解决方案
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方案
- 积累最佳实践库
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
}
}