ai-courseware/.trae/documents/04-AI实战演示与落地实施篇.md

620 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

用户界面层
├── Web界面Streamlit构建的交互界面
├── API接口FastAPI提供的RESTful API
└── 管理后台Flask构建的管理界面
业务逻辑层
├── 问答引擎LangChain核心逻辑
├── 文档处理:文档解析和向量化
├── 检索系统:向量检索和重排序
└── 对话管理:多轮对话状态管理
数据存储层
├── 向量数据库ChromaDB存储文档向量
├── 关系数据库PostgreSQL存储元数据
├── 文件存储MinIO存储原始文档
└── 缓存系统Redis缓存热点数据
模型服务层
├── 嵌入模型text-embedding-ada-002
├── 大语言模型gpt-3.5-turbo
├── 重排序模型bge-reranker-base
└── 摘要模型gpt-3.5-turbo
前端架构设计
**技术栈选择:**
```
前端框架:
├── Vue 3.3组合式API更好的TypeScript支持
├── TypeScript 5类型安全开发体验好
├── Vite 4快速的构建工具开发体验极佳
└── Vue Router 4路由管理支持历史模式
UI组件库
├── Element Plus企业级UI组件库
├── Tailwind CSS实用优先的CSS框架
├── Iconify丰富的图标库
└── VueUse实用的Vue组合式函数库
AI交互
├── SSE支持Server-Sent Events实时通信
├── Markdown渲染支持富文本展示
├── 代码高亮Prism.js代码语法高亮
└── 文件上传:支持拖拽上传和进度显示
状态管理:
├── PiniaVue官方状态管理库
├── LocalStorage本地数据持久化
├── Session管理用户会话状态管理
└── 缓存策略智能的API响应缓存
```
**核心界面实现:**
#### 主聊天界面
```vue
<!-- ChatInterface.vue -->
<template>
<div class="chat-interface">
<!-- 头部导航 -->
<div class="chat-header">
<div class="header-left">
<el-icon class="header-icon"><ChatDotRound /></el-icon>
<span class="header-title">智能文库助手</span>
</div>
<div class="header-right">
<el-button
type="text"
:icon="Delete"
@click="clearChat"
class="clear-btn"
>
清空对话
</el-button>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="chat-messages" ref="messagesContainer">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message-wrapper', message.role]"
>
<!-- 用户消息 -->
<div v-if="message.role === 'user'" class="user-message">
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<div class="message-avatar">
<el-icon><UserFilled /></el-icon>
</div>
</div>
<!-- AI回复消息 -->
<div v-else class="ai-message">
<div class="message-avatar">
<el-icon><ChatLineRound /></el-icon>
</div>
<div class="message-content">
<!-- 消息文本 -->
<div class="message-text" v-html="renderMarkdown(message.content)"></div>
<!-- 加载状态 -->
<div v-if="message.isLoading" class="loading-indicator">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在思考中...</span>
</div>
<!-- 错误状态 -->
<div v-if="message.error" class="error-message">
<el-icon><Warning /></el-icon>
<span>{{ message.error }}</span>
</div>
<!-- 消息来源 -->
<div v-if="message.sources && message.sources.length > 0" class="message-sources">
<div class="sources-title">参考文档:</div>
<div
v-for="(source, sourceIndex) in message.sources"
:key="sourceIndex"
class="source-item"
@click="viewSource(source)"
>
<el-icon><Document /></el-icon>
<span class="source-name">{{ source.name }}</span>
<span class="source-score">相似度: {{ (source.score * 100).toFixed(1) }}%</span>
</div>
</div>
<!-- 消息时间 -->
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input">
<div class="input-wrapper">
<el-input
v-model="inputMessage"
type="textarea"
:rows="2"
placeholder="请输入您的问题..."
class="message-input"
@keydown="handleKeydown"
:disabled="isSending"
/>
<div class="input-actions">
<el-upload
class="file-upload"
action="/api/documents/upload"
:show-file-list="false"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload"
accept=".pdf,.txt,.doc,.docx"
>
<el-button type="text" :icon="Paperclip" :disabled="isSending">
上传文档
</el-button>
</el-upload>
<el-button
type="primary"
:icon="Position"
@click="sendMessage"
:loading="isSending"
:disabled="!inputMessage.trim()"
class="send-button"
>
发送
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
ChatDotRound, Delete, UserFilled, ChatLineRound,
Loading, Warning, Document, Paperclip, Position
} from '@element-plus/icons-vue'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
// 消息接口
interface Message {
role: 'user' | 'assistant'
content: string
timestamp: Date
isLoading?: boolean
error?: string
sources?: DocumentSource[]
}
// 文档来源接口
interface DocumentSource {
id: string
name: string
score: number
content?: string
}
// 响应式数据
const messages = ref<Message[]>([])
const inputMessage = ref('')
const isSending = ref(false)
const messagesContainer = ref<HTMLElement>()
// 初始化marked配置
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
langPrefix: 'hljs language-',
breaks: true,
gfm: true
})
// Markdown渲染函数
const renderMarkdown = (content: string): string => {
const rawHtml = marked(content)
const cleanHtml = DOMPurify.sanitize(rawHtml)
return cleanHtml
}
// 格式化时间
const formatTime = (timestamp: Date): string => {
const now = new Date()
const diff = now.getTime() - timestamp.getTime()
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}小时前`
return timestamp.toLocaleString()
}
// 发送消息
const sendMessage = async () => {
if (!inputMessage.value.trim() || isSending.value) return
const userMessage = inputMessage.value.trim()
inputMessage.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userMessage,
timestamp: new Date()
})
// 添加AI回复占位
const aiMessage: Message = {
role: 'assistant',
content: '',
timestamp: new Date(),
isLoading: true
}
messages.value.push(aiMessage)
isSending.value = true
try {
// 调用AI服务
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: userMessage,
history: messages.value.slice(-10).map(msg => ({
role: msg.role,
content: msg.content
}))
})
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// 处理SSE流式响应
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let aiContent = ''
let sources: DocumentSource[] = []
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') {
aiMessage.isLoading = false
break
}
try {
const parsed = JSON.parse(data)
if (parsed.type === 'content') {
aiContent += parsed.content
aiMessage.content = aiContent
} else if (parsed.type === 'sources') {
sources = parsed.sources
aiMessage.sources = sources
}
} catch (e) {
console.error('Parse error:', e)
}
}
}
// 滚动到底部
await nextTick()
scrollToBottom()
}
}
} catch (error) {
console.error('Chat error:', error)
aiMessage.error = error instanceof Error ? error.message : '发送消息失败'
aiMessage.isLoading = false
} finally {
isSending.value = false
}
}
// 处理键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 清空对话
const clearChat = () => {
messages.value = []
ElMessage.success('对话已清空')
}
// 查看文档源
const viewSource = (source: DocumentSource) => {
// 实现查看文档详情的逻辑
console.log('View source:', source)
}
// 文件上传处理
const beforeUpload = (file: File) => {
const isAllowedType = ['application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isAllowedType) {
ElMessage.error('只能上传 PDF、TXT、Word 文档!')
return false
}
if (!isLt10M) {
ElMessage.error('文档大小不能超过 10MB!')
return false
}
return true
}
const handleUploadSuccess = (response: any) => {
ElMessage.success('文档上传成功')
// 可以添加刷新文档列表等逻辑
}
// 初始化
onMounted(() => {
// 添加欢迎消息
messages.value.push({
role: 'assistant',
content: '您好!我是智能文库助手,可以帮助您快速查找公司文档中的信息。请问有什么可以帮助您的吗?',
timestamp: new Date()
})
})
</script>
<style scoped>
.chat-interface {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-icon {
font-size: 24px;
color: #409eff;
}
.header-title {
font-size: 18px;
font-weight: 500;
color: #303133;
}
.header-right {
display: flex;
align-items: center;
}
.clear-btn {
color: #909399;
}
.clear-btn:hover {
color: #409eff;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.message-wrapper.user {
align-items: flex-end;
}
.message-wrapper.assistant {
align-items: flex-start;
}
.user-message {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 70%;
}
.ai-message {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 70%;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #409eff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
flex-shrink: 0;
}
.message-wrapper.assistant .message-avatar {
background-color: #67c23a;
}
.message-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.message-text {
background-color: #fff;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
line-height: 1.6;
word-wrap: break-word;
}
.user-message .message-text {
background-color: #409eff;
color: white;
}
.message-time {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.loading-indicator {
display: flex;
align-items: center;
gap: 8px;
color: #909399;
font-size: 14px;
margin-top: 8px;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
color: #f56c6c;
font-size: 14px;
margin-top: 8px;
}
.message-sources {
margin-top: 12px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 6px;
border-left: 4px solid #409eff;
}
.sources-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.source-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background-color: white;
border-radius: 4px;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.source-item:hover {
background-color: #ecf5ff;
transform: translateX(4px);
}
.source-item:last-child {
margin-bottom: 0;
}
.source-name {
flex: 1;
font-size: 14px;
color: #303133;
}
.source-score {
font-size: 12px;
color: #67c23a;
font-weight: 500;
}
.chat-input {
padding: 24px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
.input-wrapper {
max-width: 800px;
margin: 0 auto;
}
.message-input {
margin-bottom: 12px;
}
.message-input :deep(textarea) {
font-size: 16px;
line-height: 1