基于后端实际代码 + 前端已实现的 SSE 消费逻辑生成。
Base URL:
http://127.0.0.1:8000认证方式: 请求头携带
Authorization: Bearer <token>
| 模块 | 接口 | 方法 | 路径 | 认证 | 状态 |
|---|---|---|---|---|---|
| 认证 | 登录 | POST | /auth/login |
否 | 已实现 |
| 认证 | 注册 | POST | /auth/register |
否 | 已实现 |
| 认证 | 获取当前用户 | GET | /auth/me |
是 | 已实现 |
| 认证 | 退出登录 | POST | /auth/logout |
是 | 已实现 |
| 对话 | 获取对话列表 | GET | /chats |
是 | 已实现 |
| 对话 | 创建对话 | POST | /chats |
是 | 已实现 |
| 对话 | 获取对话消息 | GET | /chats/{chat_id} |
是 | 已实现 |
| 对话 | 修改对话标题 | PUT | /chats/{chat_id} |
是 | 已实现 |
| 对话 | 自动生成标题 | GET | /chats/{chat_id}/craete_title |
是 | 已实现 |
| 对话 | 删除对话 | DELETE | /chats/{chat_id} |
是 | 已实现 |
| 对话 | 清空对话消息 | DELETE | /chats/{chat_id}/messages |
是 | 已实现 |
| 消息 | 发送消息(SSE流式) | POST | /message/send |
是 | 已实现 |
| 文件 | 上传文件 | POST | /files/upload |
是 | 已实现 |
| 知识库 | 创建知识库 | POST | /knowledge-bases |
是 | 待实现 |
| 知识库 | 获取知识库列表 | GET | /knowledge-bases |
是 | 待实现 |
| 知识库 | 删除知识库 | DELETE | /knowledge-bases/{kb_id} |
是 | 待实现 |
| 知识库 | 上传知识库文档 | POST | /knowledge-bases/{kb_id}/documents |
是 | 待实现 |
| 知识库 | 获取文档列表 | GET | /knowledge-bases/{kb_id}/documents |
是 | 待实现 |
| 知识库 | 删除文档 | DELETE | /knowledge-bases/{kb_id}/documents/{doc_id} |
是 | 待实现 |
前端已适配:
sendMessage()会自动检测响应类型。如果后端返回text/event-stream则走流式渲染,如果返回application/json则走普通 JSON 解析。两种模式前端都支持,无需切换。
{
"code": 200,
"message": "描述信息",
"data": { ... }
}
Content-Type: text/event-stream
data: {"type": "start", "messageId": 6}
data: {"type": "chunk", "content": "递归"}
data: {"type": "chunk", "content": "是一种"}
data: {"type": "done", "content": "递归是一种编程技巧...", "messageId": 6}
| HTTP 状态码 | 含义 | 示例 |
|---|---|---|
| 401 | 未认证 / Token 无效 | {"detail": "token无效或已过期"} |
| 404 | 资源不存在 | {"detail": "对话不存在"} |
| 409 | 资源冲突 | {"detail": "用户名或邮箱已被注册!"} |
| 422 | 请求体验证失败 | FastAPI 自动生成的验证错误 |
/auth/login — 登录请求格式: application/x-www-form-urlencoded
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 用户名或邮箱 |
| password | string | 是 | 密码 |
请求示例:
POST /auth/login
Content-Type: application/x-www-form-urlencoded
username=李灏&password=123456
成功响应 (200):
{
"code": 200,
"message": "登录成功",
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 1,
"username": "李灏",
"email": "lihao@example.com",
"avatar": null
}
}
}
access_token供 Swagger 的 Authorize 功能使用,前端取data.token。
错误:
| 状态码 | detail |
|---|---|
| 401 | 账号不存在! |
| 401 | 密码错误! |
/auth/register — 注册请求格式: application/x-www-form-urlencoded
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| username | string | 是 | 2-20 字符 | 用户名 |
| password | string | 是 | 6-20 字符 | 密码 |
| string | 是 | 6-20 字符 | 邮箱 |
成功响应 (200): 与登录响应格式相同,message 为 “注册成功”。
错误:
| 状态码 | detail |
|---|---|
| 409 | 用户名或邮箱已被注册! |
/auth/me — 获取当前用户请求头: Authorization: Bearer <token>
成功响应 (200):
{
"code": 200,
"message": "成功",
"data": {
"id": 1,
"username": "李灏",
"email": "lihao@example.com",
"avatar": null
}
}
/auth/logout — 退出登录请求头: Authorization: Bearer <token>
成功响应 (200):
{
"code": 200,
"message": "已退出登录"
}
/chats — 获取对话列表请求头: Authorization: Bearer <token>
成功响应 (200):
{
"code": 200,
"message": "成功",
"data": [
{
"id": 1001,
"title": "关于 Python 的问答",
"messageCount": 6,
"createdAt": "2026-08-10T10:00:00",
"updatedAt": "2026-08-12T14:30:00"
}
]
}
/chats — 创建对话请求头: Authorization: Bearer <token>
请求格式: application/json
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| title | string | 否 | “新对话” | 对话标题 |
请求示例:
{ "title": "新对话" }
成功响应 (200):
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1002,
"title": "新对话",
"messageCount": 0,
"createdAt": "2026-08-14T10:00:00",
"updatedAt": "2026-08-14T10:00:00"
}
}
/chats/{chat_id} — 获取对话消息请求头: Authorization: Bearer <token>
路径参数: chat_id (int) — 对话 ID
成功响应 (200):
{
"code": 200,
"message": "成功",
"data": [
{
"id": 1,
"chat_id": 1001,
"role": "user",
"content": "你好,请介绍一下 Python",
"images": null,
"files": null,
"created_at": "2026-08-10T10:00:00"
},
{
"id": 2,
"chat_id": 1001,
"role": "ai",
"content": "Python 是一种广泛使用的高级编程语言...",
"images": null,
"files": null,
"created_at": "2026-08-10T10:00:05"
}
]
}
错误: 404 — 对话不存在
/chats/{chat_id} — 修改对话标题请求头: Authorization: Bearer <token>
请求格式: application/json
{ "title": "Python 学习笔记" }
成功响应 (200):
{
"code": 200,
"message": "修改成功",
"data": { "id": 1001, "title": "Python 学习笔记" }
}
/chats/{chat_id} — 删除对话删除对话及其所有消息。
成功响应 (200):
{ "code": 200, "message": "删除成功", "data": null }
/chats/{chat_id}/messages — 清空对话消息删除对话的所有消息,保留对话本身。
成功响应 (200):
{ "code": 200, "message": "已清空消息", "data": null }
/message/send — 发送消息(普通模式)当前后端已实现的模式,返回完整 JSON。
请求头: Authorization: Bearer <token>
请求格式: application/json
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | int | 是 | 对话 ID |
| content | string | 否 | 文本内容(与图片/文件至少有一个) |
| images | array | 否 | 图片数组 [{url, name}] |
| files | array | 否 | 文件数组 [{fileId, name, size}] |
请求示例:
{
"chat_id": 1001,
"content": "请帮我解释什么是递归",
"images": null,
"files": null
}
成功响应 (200) — Content-Type: application/json:
{
"code": 200,
"message": "成功",
"data": {
"userMessageId": 5,
"aiMessage": {
"id": 6,
"chat_id": 1001,
"role": "ai",
"content": "递归是一种编程技巧...",
"images": null,
"files": null,
"created_at": "2026-08-14T10:05:00"
}
}
}
/message/send — 发送消息(SSE 流式模式)前端已支持,后端待实现。 请求参数与普通模式完全相同,区别仅在于响应格式。
前端通过检测响应头
Content-Type自动判断模式:
text/event-stream→ 流式渲染application/json→ 普通 JSON 解析
响应格式: Content-Type: text/event-stream
SSE 事件格式:
每条事件以 data: 开头,以空行结尾。data 后面是 JSON 字符串。
| type | 说明 | 字段 |
|---|---|---|
start |
AI 开始回复 | messageId (int): AI 消息 ID |
chunk |
增量内容 | content (string): 本次的文本片段 |
done |
回复完成 | content (string): 完整回复内容(可选,用于校正) |
error |
回复出错 | content (string): 错误信息 |
data: {"type": "start", "messageId": 6}
data: {"type": "chunk", "content": "递归"}
data: {"type": "chunk", "content": "是一种"}
data: {"type": "chunk", "content": "编程技巧,"}
data: {"type": "chunk", "content": "函数在执行过程中调用自身..."}
data: {"type": "done", "messageId": 6}
from fastapi.responses import StreamingResponse
import json
@router.post("/message/send")
async def send_messages(message: UserMessages,
current_user: User = Depends(get_current_user),
session=Depends(get_session)):
# 1. 保存用户消息(与当前代码相同)
user_message = Message(chat_id=message.chat_id, role="user", content=message.content, ...)
session.add(user_message)
await session.flush()
# 2. 定义生成器函数,逐块产出 SSE 事件
async def event_stream():
# 发送 start 事件
yield f"data: {json.dumps({'type': 'start'})}\n\n"
# 调用 AI 流式接口
full_content = ""
async for chunk in agent.astream({
"messages": [{"role": "user", "content": message.content}]
}):
delta = chunk["messages"][-1].content
if delta:
full_content += delta
yield f"data: {json.dumps({'type': 'chunk', 'content': delta})}\n\n"
# 保存 AI 消息
ai_message = Message(chat_id=message.chat_id, role="ai", content=full_content)
session.add(ai_message)
await session.flush()
await session.commit()
# 发送 done 事件
yield f"data: {json.dumps({'type': 'done', 'messageId': ai_message.id})}\n\n"
# 3. 返回 StreamingResponse
return StreamingResponse(event_stream(), media_type="text/event-stream")
关键改动:
agent.invoke()→agent.astream()(同步变异步流式)return {json}→return StreamingResponse(event_stream(), ...)- 在生成器内部保存 AI 消息(流式完成后)
/files/upload — 上传文件尚未实现,以下为建议设计。
请求头: Authorization: Bearer <token>
请求格式: multipart/form-data
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | file | 是 | 上传的文件 |
建议的成功响应 (200):
{
"code": 200,
"message": "上传成功",
"data": {
"fileId": "uuid-string",
"url": "/uploads/xxx.png",
"name": "screenshot.png",
"size": 102400
}
}
在输入框中直接 Ctrl+V 粘贴图片或文件,自动添加到文件预览区。
将文件拖拽到聊天消息区域,释放后自动添加到文件预览区。拖拽时区域会高亮显示。
前端通过 fetch + ReadableStream 读取 SSE 流:
type: "start" — 创建空的 AI 消息气泡type: "chunk" — 将内容追加到气泡,实时渲染type: "done" — 完成渲染type: "error" — 显示错误提示向后兼容:如果后端返回普通 JSON,前端自动走 JSON 解析逻辑,不影响使用。
| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
| id | INTEGER | PK | 自增主键 |
| username | VARCHAR(50) | UNIQUE | 用户名 |
| VARCHAR(100) | UNIQUE | 邮箱 | |
| password_hash | VARCHAR | 密码哈希(bcrypt) | |
| avatar | VARCHAR(500) | 头像 URL | |
| created_at | DATETIME | 创建时间 |
| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
| id | INTEGER | PK | 自增主键 |
| user_id | INTEGER | FK → users.id | 所属用户 |
| title | VARCHAR(100) | 对话标题 | |
| created_at | DATETIME | 创建时间 | |
| updated_at | DATETIME | 最后更新时间(自动更新) |
| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
| id | INTEGER | PK | 自增主键 |
| chat_id | INTEGER | FK → chats.id | 所属对话 |
| role | VARCHAR(10) | "user" 或 "ai" |
|
| content | TEXT | 消息文本内容 | |
| images | JSON | 图片信息 | |
| files | JSON | 文件信息 | |
| created_at | DATETIME | 创建时间 |
当前问题:每次发消息只传当前一条消息给 AI,AI 不记得之前的对话。
建议:在 POST /message/send 接口中,查询该对话的历史消息,一起传给 AI:
# 查询最近 20 条消息作为上下文
history = (await session.exec(
select(Message).where(Message.chat_id == message.chat_id)
.order_by(Message.created_at.desc()).limit(20)
)).all()[::-1] # 反转成时间正序
# 构建带上下文的消息列表
messages = [{"role": m.role, "content": m.content} for m in history]
messages.append({"role": "user", "content": message.content})
response = await agent.ainvoke({"messages": messages})
当前问题:files.py 为空,前端上传的文件只存在浏览器本地,无法持久化。
建议实现:
import uuid, os
@router.post("/files/upload")
async def upload_file(file: UploadFile = File(...),
current_user: User = Depends(get_current_user)):
# 生成唯一文件名
ext = os.path.splitext(file.filename)[1]
filename = f"{uuid.uuid4()}{ext}"
filepath = f"uploads/{filename}"
# 保存文件
os.makedirs("uploads", exist_ok=True)
with open(filepath, "wb") as f:
f.write(await file.read())
return {
"code": 200,
"message": "上传成功",
"data": {
"fileId": filename,
"url": f"/uploads/{filename}",
"name": file.filename,
"size": os.path.getsize(filepath)
}
}
当前问题:blacklisted_tokens 是内存 set,服务器重启后清空。
建议:用数据库表存储黑名单,或用 Redis:
# 方案一:数据库表
class BlacklistedToken(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
token: str
expired_at: datetime
# 方案二:Redis(推荐)
import redis
r = redis.Redis()
r.setex(f"blacklist:{token}", 86400, "1") # 24小时后自动清除
场景:AI 回复太长,用户想中途停止。
前端实现:在流式读取循环中,提供一个”停止”按钮,调用 reader.cancel() 中断流。
后端实现:FastAPI 的 StreamingResponse 会自动检测客户端断开,生成器中捕获 asyncio.CancelledError 即可。
当前问题:GET /chats/{chat_id} 返回所有消息,消息多时性能差。
建议:加分页参数:
GET /chats/{chat_id}?page=1&page_size=20
当前问题:前端用用户消息前 30 字符作为标题。
建议:后端在 AI 回复后,调用 AI 生成一句话摘要作为标题:
title_response = await llm.ainvoke(f"请用10个字以内总结这个对话的主题:{message.content}")
chat.title = title_response.content
session.add(chat)
当前问题:AI Agent 只接收文本,不支持图片。
建议:使用支持视觉的模型(如 GPT-4V / Qwen-VL),将图片作为多模态输入:
from langchain_core.messages import HumanMessage
messages = [HumanMessage(content=[
{"type": "text", "text": message.content},
{"type": "image_url", "image_url": {"url": image.url}}
])]
前端已完整实现知识库管理 UI,后端需要实现以下 6 个接口。
RAG 流程:用户上传文档 → 后端解析+分块+向量化存储 → 用户聊天时选择知识库 → 后端检索相关片段 → 注入 AI 上下文 → AI 基于知识库回答
| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
| id | INTEGER | PK | 自增主键 |
| user_id | INTEGER | FK → users.id | 所属用户 |
| name | VARCHAR(100) | 知识库名称 | |
| description | VARCHAR(500) | 描述(可选) | |
| created_at | DATETIME | 创建时间 | |
| updated_at | DATETIME | 最后更新时间 |
| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
| id | INTEGER | PK | 自增主键 |
| kb_id | INTEGER | FK → knowledge_bases.id | 所属知识库 |
| filename | VARCHAR(255) | 原始文件名 | |
| file_path | VARCHAR(500) | 服务器存储路径 | |
| file_size | INTEGER | 文件大小(字节) | |
| status | VARCHAR(20) | processing / ready / failed |
|
| chunk_count | INTEGER | 分块数量 | |
| created_at | DATETIME | 创建时间 |
另外需要向量存储(如 ChromaDB / FAISS),用于存储文档分块的 embedding。
/knowledge-bases — 创建知识库请求头: Authorization: Bearer <token>
请求格式: application/json
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 知识库名称(≤100字符) |
| description | string | 否 | 描述(≤500字符) |
请求示例:
{
"name": "Python学习资料",
"description": "Python基础到进阶教程"
}
成功响应 (200):
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"name": "Python学习资料",
"description": "Python基础到进阶教程",
"documentCount": 0,
"createdAt": "2026-08-15T10:00:00",
"updatedAt": "2026-08-15T10:00:00"
}
}
/knowledge-bases — 获取知识库列表请求头: Authorization: Bearer <token>
成功响应 (200):
{
"code": 200,
"message": "成功",
"data": [
{
"id": 1,
"name": "Python学习资料",
"description": "Python基础到进阶教程",
"documentCount": 3,
"createdAt": "2026-08-15T10:00:00",
"updatedAt": "2026-08-15T10:30:00"
}
]
}
/knowledge-bases/{kb_id} — 删除知识库删除知识库及其所有文档和向量数据。
路径参数: kb_id (int)
成功响应 (200):
{
"code": 200,
"message": "删除成功",
"data": null
}
错误: 404 — 知识库不存在
/knowledge-bases/{kb_id}/documents — 上传知识库文档上传文档到指定知识库,后端异步解析、分块、向量化。
请求头: Authorization: Bearer <token>
请求格式: multipart/form-data
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | file | 是 | 文档文件(支持 PDF/Word/TXT/MD/CSV) |
成功响应 (200):
{
"code": 200,
"message": "上传成功,正在处理",
"data": {
"id": 1,
"filename": "tutorial.pdf",
"fileSize": 102400,
"status": "processing",
"createdAt": "2026-08-15T10:00:00"
}
}
注意:文档解析是耗时操作,建议异步处理。上传后立即返回
status: "processing",后台任务完成后更新为"ready"。前端可通过轮询GET /knowledge-bases/{kb_id}/documents获取最新状态。
/knowledge-bases/{kb_id}/documents — 获取文档列表请求头: Authorization: Bearer <token>
路径参数: kb_id (int)
成功响应 (200):
{
"code": 200,
"message": "成功",
"data": [
{
"id": 1,
"filename": "tutorial.pdf",
"fileSize": 102400,
"status": "ready",
"chunkCount": 25,
"createdAt": "2026-08-15T10:00:00"
},
{
"id": 2,
"filename": "notes.md",
"fileSize": 5120,
"status": "processing",
"chunkCount": 0,
"createdAt": "2026-08-15T10:05:00"
}
]
}
/knowledge-bases/{kb_id}/documents/{doc_id} — 删除文档删除文档及其所有向量数据。
路径参数: kb_id (int), doc_id (int)
成功响应 (200):
{
"code": 200,
"message": "删除成功",
"data": null
}
/message/send — 发送消息(新增 kb_id 字段)在现有
/message/send接口的请求体中新增kb_id字段(可选)。
请求体新增字段:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| kb_id | int | null | 否 | 知识库 ID。传入时 AI 会先检索知识库相关内容再回答 |
请求示例:
{
"chat_id": 1001,
"content": "解释一下Python的装饰器",
"images": null,
"files": null,
"kb_id": 1
}
后端处理逻辑:当
kb_id不为空时,在调用agent.astream()之前,先用用户的问题去向量数据库检索相关文档片段,将检索结果拼接到消息内容或系统提示中,再交给 AI 回答。
后端实现建议:
# messages.py 中的 send_messages 函数
if message.kb_id:
# 1. 检索相关文档片段
from app.ai.rag import retrieve_relevant_docs
relevant_chunks = await retrieve_relevant_docs(message.kb_id, message.content)
# 2. 将检索结果拼接到用户消息中
context = "\n\n".join(relevant_chunks)
enhanced_content = f"以下是知识库中的相关内容,请基于这些内容回答:\n\n{context}\n\n用户问题:{message.content}"
# 用 enhanced_content 替代 message.content 传给 AI
pip install chromadb langchain-chroma langchain-community
# 文档加载器
pip install pypdf python-docx markdown
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
import os
embeddings = OpenAIEmbeddings(
model="text-embedding-v3",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
)
# 每个知识库一个 collection
def get_vectorstore(kb_id: int) -> Chroma:
return Chroma(
collection_name=f"kb_{kb_id}",
embedding_function=embeddings,
persist_directory="data/chroma_db",
)
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
def load_and_split(file_path: str):
# 根据扩展名选择加载器
ext = file_path.rsplit('.', 1)[-1].lower()
if ext == 'pdf':
loader = PyPDFLoader(file_path)
elif ext == 'docx':
loader = Docx2txtLoader(file_path)
else:
loader = TextLoader(file_path)
docs = loader.load()
# 分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
)
return splitter.split_documents(docs)
from app.ai.vector_store import get_vectorstore
async def retrieve_relevant_docs(kb_id: int, query: str, k: int = 3):
"""检索与用户问题最相关的文档片段"""
vectorstore = get_vectorstore(kb_id)
results = vectorstore.similarity_search(query, k=k)
return [doc.page_content for doc in results]
from app.routers.knowledge import router as knowledge_router
app.include_router(knowledge_router)
章节编号已调整:原”八”认证流程 → “九”,新增”八”知识库模块。
1. 用户登录/注册 → 后端生成 JWT Token
2. 前端保存 Token 到 localStorage
3. 后续请求携带 Authorization: Bearer <token>
4. 后端 get_current_user 依赖验证 Token:
a. 检查 Token 是否在黑名单中
b. 解码 JWT,提取用户 ID
c. 查数据库确认用户存在
5. 验证通过 → 接口正常执行
验证失败 → 返回 401,前端跳转登录页
http://127.0.0.1:8000/docsPOST /auth/login,从响应中复制 access_token 值