|
|
|
|
@ -0,0 +1,247 @@
|
|
|
|
|
# 前端聊天联调指南
|
|
|
|
|
|
|
|
|
|
本指南用于对接后端聊天接口,包括非流式与流式两种模式,涵盖鉴权、请求/响应契约、前端示例代码以及常见问题。
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
- 后端服务:FastAPI
|
|
|
|
|
- 核心接口:`POST /api/v1/ai/chat`(非流式)
|
|
|
|
|
- 鉴权:`Authorization: Bearer <token>`
|
|
|
|
|
- 模型:DeepSeek R1(支持推理内容 `reasoning_content`)
|
|
|
|
|
- 相关代码位置:
|
|
|
|
|
- 路由:`backend/app/routers/ai.py:57`
|
|
|
|
|
- 返回结构:`backend/app/routers/ai.py:64-67`
|
|
|
|
|
- LLM 客户端:`backend/app/services/llm.py:44`
|
|
|
|
|
- CORS 中间件:`backend/app/main.py:8-14`
|
|
|
|
|
- 鉴权依赖:`backend/app/deps/auth.py:9`
|
|
|
|
|
- 登录接口:`backend/app/routers/auth.py:25`
|
|
|
|
|
- 工具调用(诊断):`backend/app/agents/diagnosis_agent.py:33-54`
|
|
|
|
|
|
|
|
|
|
## 鉴权与获取 Token
|
|
|
|
|
- 登录接口:`POST /api/v1/user/login`
|
|
|
|
|
- 成功返回包含 `token` 字段
|
|
|
|
|
- 所有聊天请求需携带 Header:`Authorization: Bearer <token>`
|
|
|
|
|
|
|
|
|
|
示例(fetch):
|
|
|
|
|
```ts
|
|
|
|
|
async function login(username: string, password: string) {
|
|
|
|
|
const resp = await fetch(`${API_BASE}/api/v1/user/login`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ username, password }),
|
|
|
|
|
});
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
if (!resp.ok) throw new Error(data?.detail ?? "login_failed");
|
|
|
|
|
return data.token as string;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 接口契约(非流式)
|
|
|
|
|
- URL:`POST /api/v1/ai/chat`
|
|
|
|
|
- Header:`Authorization: Bearer <token>`,`Content-Type: application/json`
|
|
|
|
|
- Body:
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"messages": [
|
|
|
|
|
{ "role": "system", "content": "你是Hadoop运维诊断专家..." },
|
|
|
|
|
{ "role": "user", "content": "请分析 Yarn 任务失败原因" }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 响应:
|
|
|
|
|
```json
|
|
|
|
|
{ "reply": "回答内容", "reasoning": "推理过程(可能为空)" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
类型建议:
|
|
|
|
|
```ts
|
|
|
|
|
type ChatRole = "system" | "user" | "assistant";
|
|
|
|
|
type ChatMessage = { role: ChatRole; content: string };
|
|
|
|
|
type ChatRequest = { messages: ChatMessage[] };
|
|
|
|
|
type ChatResponse = { reply: string; reasoning?: string };
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
请求示例(axios):
|
|
|
|
|
```ts
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
|
|
|
|
async function chat(messages: ChatMessage[], token: string): Promise<ChatResponse> {
|
|
|
|
|
const resp = await axios.post<ChatResponse>(
|
|
|
|
|
`${API_BASE}/api/v1/ai/chat`,
|
|
|
|
|
{ messages },
|
|
|
|
|
{
|
|
|
|
|
headers: {
|
|
|
|
|
"Authorization": `Bearer ${token}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return resp.data;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
请求示例(fetch):
|
|
|
|
|
```ts
|
|
|
|
|
async function chatFetch(messages: ChatMessage[], token: string): Promise<ChatResponse> {
|
|
|
|
|
const resp = await fetch(`${API_BASE}/api/v1/ai/chat`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Authorization": `Bearer ${token}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"Accept": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ messages }),
|
|
|
|
|
});
|
|
|
|
|
if (!resp.ok) throw new Error(`Chat failed: ${resp.status}`);
|
|
|
|
|
return await resp.json();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 流式对接指南(前端实现约定)
|
|
|
|
|
后端当前为非流式响应。为便于先行集成,前端可按以下约定实现流式客户端;当后端提供流式端点后即可直接联通。
|
|
|
|
|
|
|
|
|
|
两种常见流式方案:
|
|
|
|
|
- SSE(Server-Sent Events)
|
|
|
|
|
- 响应头:`Content-Type: text/event-stream`
|
|
|
|
|
- 行格式:`data: <JSON>`;完成标记:`data: [DONE]`
|
|
|
|
|
- 事件内容(参考 OpenAI/DeepSeek):
|
|
|
|
|
- 增量文本:`{"choices":[{"delta":{"content":"..."}}]}`
|
|
|
|
|
- 增量推理:`{"choices":[{"delta":{"reasoning_content":"..."}}]}`
|
|
|
|
|
- 完成:`{"choices":[{"finish_reason":"stop"}]}`
|
|
|
|
|
- 注意:原生 `EventSource` 不支持自定义 Header(鉴权)。可用 polyfill 支持 Header 或后端改用查询参数传 token。
|
|
|
|
|
- Fetch 可读流(ReadableStream)
|
|
|
|
|
- 响应:分块传输(chunked),逐行 NDJSON;完成行:`[DONE]`
|
|
|
|
|
- 支持自定义 Header,适配当前鉴权方式(推荐)
|
|
|
|
|
|
|
|
|
|
SSE 方案(使用支持 Header 的 polyfill):
|
|
|
|
|
```ts
|
|
|
|
|
type StreamDelta = { content?: string; reasoning_content?: string };
|
|
|
|
|
|
|
|
|
|
function startChatSSE(
|
|
|
|
|
messages: ChatMessage[],
|
|
|
|
|
token: string,
|
|
|
|
|
onDelta: (d: StreamDelta) => void,
|
|
|
|
|
onDone: () => void,
|
|
|
|
|
onError: (e: any) => void
|
|
|
|
|
) {
|
|
|
|
|
const url = `${API_BASE}/api/v1/ai/chat/stream`;
|
|
|
|
|
const es = new EventSourcePolyfill(url, {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
payload: JSON.stringify({ messages, stream: true }),
|
|
|
|
|
method: "POST",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
es.onmessage = (evt) => {
|
|
|
|
|
const t = evt.data;
|
|
|
|
|
if (t === "[DONE]") { onDone(); es.close(); return; }
|
|
|
|
|
try {
|
|
|
|
|
const j = JSON.parse(t);
|
|
|
|
|
const delta = j?.choices?.[0]?.delta || {};
|
|
|
|
|
onDelta({ content: delta.content, reasoning_content: delta.reasoning_content });
|
|
|
|
|
} catch (e) { onError(e); }
|
|
|
|
|
};
|
|
|
|
|
es.onerror = (e) => { onError(e); es.close(); };
|
|
|
|
|
return () => es.close();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Fetch 可读流方案(推荐,支持 Header):
|
|
|
|
|
```ts
|
|
|
|
|
async function startChatStream(
|
|
|
|
|
messages: ChatMessage[],
|
|
|
|
|
token: string,
|
|
|
|
|
onDelta: (d: StreamDelta) => void,
|
|
|
|
|
onDone: () => void
|
|
|
|
|
) {
|
|
|
|
|
const resp = await fetch(`${API_BASE}/api/v1/ai/chat?stream=true`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Authorization": `Bearer ${token}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"Accept": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ messages }),
|
|
|
|
|
});
|
|
|
|
|
if (!resp.body) throw new Error("No stream body");
|
|
|
|
|
|
|
|
|
|
const reader = resp.body.getReader();
|
|
|
|
|
const decoder = new TextDecoder("utf-8");
|
|
|
|
|
let buffer = "";
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
if (done) break;
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
|
|
|
|
let idx;
|
|
|
|
|
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
|
|
|
const line = buffer.slice(0, idx).trim();
|
|
|
|
|
buffer = buffer.slice(idx + 1);
|
|
|
|
|
if (!line) continue;
|
|
|
|
|
if (line === "[DONE]") { onDone(); return; }
|
|
|
|
|
try {
|
|
|
|
|
const j = JSON.parse(line);
|
|
|
|
|
const delta = j?.choices?.[0]?.delta || {};
|
|
|
|
|
onDelta({ content: delta.content, reasoning_content: delta.reasoning_content });
|
|
|
|
|
} catch { /* 忽略半包或非 JSON 行 */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
onDone();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## React 组件集成建议
|
|
|
|
|
- 状态管理:
|
|
|
|
|
- `messages: ChatMessage[]` 会话消息
|
|
|
|
|
- `partialReply: string` 流式拼接的回答
|
|
|
|
|
- `partialReasoning: string` 流式拼接的推理
|
|
|
|
|
- `loading: boolean` 是否正在生成
|
|
|
|
|
- 可选 `AbortController` 用于取消
|
|
|
|
|
- 交互流程:
|
|
|
|
|
- 用户输入后 `messages.push({ role: "user", content: input })`
|
|
|
|
|
- 触发流式接口,增量拼接 `partialReply` 与 `partialReasoning`
|
|
|
|
|
- 完成后将 `partialReply` 作为 `assistant` 消息加入 `messages` 并清空临时态
|
|
|
|
|
- 推理内容建议以折叠/切换方式显示,避免干扰主对话
|
|
|
|
|
|
|
|
|
|
## 错误处理与重试策略
|
|
|
|
|
- 401 未鉴权:跳转登录或刷新 token(参考 `backend/app/deps/auth.py:9-31`)
|
|
|
|
|
- 403 用户未激活:提示管理员处理
|
|
|
|
|
- 502 模型不可用:提示“模型服务不可用”,支持重试
|
|
|
|
|
- 500 服务错误:toast 提示,记录时间戳便于排查
|
|
|
|
|
- 超时/网络中断:提供“停止生成/重试”按钮,保留已生成内容
|
|
|
|
|
|
|
|
|
|
## 提示词注入建议
|
|
|
|
|
在会话首条注入系统提示词,以提升回答质量:
|
|
|
|
|
```ts
|
|
|
|
|
const systemPrompt: ChatMessage = {
|
|
|
|
|
role: "system",
|
|
|
|
|
content: "你是Hadoop运维诊断专家。你需要根据系统日志进行分析,输出中文,优先给出根因、影响范围与修复建议。"
|
|
|
|
|
};
|
|
|
|
|
const msgs: ChatMessage[] = [systemPrompt, { role: "user", content: "请分析 Yarn 应用失败原因" }];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 本地联调自检
|
|
|
|
|
- 登录获取 token
|
|
|
|
|
- 使用 curl 验证非流式接口:
|
|
|
|
|
```
|
|
|
|
|
curl -X POST 'http://localhost:8000/api/v1/ai/chat' \
|
|
|
|
|
-H 'Authorization: Bearer <TOKEN>' \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d '{"messages":[{"role":"user","content":"你好"}]}'
|
|
|
|
|
```
|
|
|
|
|
- 前端设置 `API_BASE` 指向后端地址,确保浏览器端 CORS 正常(后端已允许,参见 `backend/app/main.py:8-14`)
|
|
|
|
|
|
|
|
|
|
## 常见问题
|
|
|
|
|
- 原生 `EventSource` 如何携带 token?
|
|
|
|
|
- 原生不支持 Header,可改用 query 传 token(需后端支持)或使用支持 Header 的 SSE polyfill;更推荐 Fetch 可读流。
|
|
|
|
|
- `reasoning` 为空怎么办?
|
|
|
|
|
- DeepSeek R1 某些回答可能不返回推理,前端需兼容空值并在 UI 中隐藏推理区域。
|
|
|
|
|
- 如何展示工具调用过程?
|
|
|
|
|
- 参考 `backend/app/agents/diagnosis_agent.py:33-54` 的返回结构,前端可在“诊断/修复”场景增加步骤视图。
|
|
|
|
|
|
|
|
|
|
## 配置项建议
|
|
|
|
|
- `API_BASE`:后端基础地址(如 `http://localhost:8000`)
|
|
|
|
|
- `STREAM_ENABLED`:是否启用流式(后端未提供流式端点前为 `false`)
|
|
|
|
|
- `TIMEOUT_MS`:请求超时时间(建议 30s)
|
|
|
|
|
- UI:是否显示推理内容(默认折叠)
|
|
|
|
|
|