chat前端联调指南

pull/48/head
echo 2 months ago
parent 2bda5cce94
commit 2eb858a743

@ -31,7 +31,7 @@ async def run_diagnose_and_repair(db: AsyncSession, operator: str, context: Dict
residual_risk = "medium"
for step in range(max_steps):
resp = llm.chat(messages, tools=tools, stream=False)
resp = await llm.chat(messages, tools=tools, stream=False)
choice = (resp.get("choices") or [{}])[0]
msg = choice.get("message", {})
tool_calls = msg.get("tool_calls") or []

@ -58,12 +58,14 @@ async def diagnose_repair(req: DiagnoseRepairReq, user=Depends(get_current_user)
async def ai_chat(req: ChatReq, user=Depends(get_current_user)):
try:
llm = LLMClient()
resp = llm.chat(req.messages, tools=None, stream=False)
resp = await llm.chat(req.messages, tools=None, stream=False)
choices = resp.get("choices") or []
if not choices:
raise HTTPException(status_code=502, detail="llm_unavailable")
msg = choices[0].get("message") or {}
return {"reply": msg.get("content") or ""}
reply = msg.get("content") or ""
reasoning = msg.get("reasoning_content") or ""
return {"reply": reply, "reasoning": reasoning}
except HTTPException:
raise
except Exception:

@ -57,7 +57,7 @@ class LLMClient:
"Content-Type": "application/json",
}
def chat(self, messages: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None, stream: bool = False) -> Dict[str, Any]:
async def chat(self, messages: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None, stream: bool = False) -> Dict[str, Any]:
if self.simulate or httpx is None:
return {
"choices": [
@ -74,7 +74,7 @@ class LLMClient:
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
with httpx.Client(timeout=self.timeout) as client:
resp = client.post(self.endpoint, headers=self._headers(), json=payload)
async with httpx.AsyncClient(timeout=self.timeout) as client:
resp = await client.post(self.endpoint, headers=self._headers(), json=payload)
resp.raise_for_status()
return resp.json()

@ -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();
}
```
## 流式对接指南(前端实现约定)
后端当前为非流式响应。为便于先行集成,前端可按以下约定实现流式客户端;当后端提供流式端点后即可直接联通。
两种常见流式方案:
- SSEServer-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是否显示推理内容默认折叠

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -0,0 +1,43 @@
import asyncio
import os
import sys
# Add backend directory to sys.path to import app modules
# Current file: backend/tests/test_llm.py
# Parent: backend/tests
# Grandparent: backend
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.llm import LLMClient
from dotenv import load_dotenv
async def main():
# Load .env from backend directory
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
load_dotenv(env_path)
print("Testing LLMClient...")
try:
llm = LLMClient()
print(f"Provider: {llm.provider}")
print(f"Endpoint: {llm.endpoint}")
print(f"Model: {llm.model}")
messages = [{"role": "user", "content": "你好"}]
print("Sending request...")
resp = await llm.chat(messages)
print("Response received:")
# Check for reasoning content since we use DeepSeek R1
if "choices" in resp and resp["choices"]:
msg = resp["choices"][0].get("message", {})
print(f"Reply: {msg.get('content')}")
if "reasoning_content" in msg:
print(f"Reasoning: {msg.get('reasoning_content')[:100]}...") # Print first 100 chars
else:
print(resp)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(main())

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save