|
@@ -0,0 +1,219 @@
|
|
|
|
|
+"""
|
|
|
|
|
+FastAPI SSE 接口 - Qwen 模型思维链展示
|
|
|
|
|
+实时返回模型的思考过程(think process)
|
|
|
|
|
+"""
|
|
|
|
|
+import asyncio
|
|
|
|
|
+import json
|
|
|
|
|
+from fastapi import FastAPI, Request
|
|
|
|
|
+from fastapi.responses import StreamingResponse
|
|
|
|
|
+from pydantic import BaseModel
|
|
|
|
|
+import httpx
|
|
|
|
|
+
|
|
|
|
|
+app = FastAPI(
|
|
|
|
|
+ title="Qwen Think Process API",
|
|
|
|
|
+ description="展示 Qwen 模型的思维链(Think Process),实时返回思考过程",
|
|
|
|
|
+ version="1.0.0"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+# Qwen API 配置(预留配置参数)
|
|
|
|
|
+QWEN_API_URL = "http://localhost:8000/v1/chat/completions" # Qwen 服务地址
|
|
|
|
|
+QWEN_API_KEY = "your-api-key-here" # API Key 预留
|
|
|
|
|
+QWEN_MODEL = "Qwen/Qwen2-7B-Instruct" # 模型名称
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class ThinkProcessRequest(BaseModel):
|
|
|
|
|
+ """请求体模型"""
|
|
|
|
|
+ question: str
|
|
|
|
|
+ max_tokens: int = 500
|
|
|
|
|
+ model: str = QWEN_MODEL
|
|
|
|
|
+ api_key: str = None # 允许通过请求覆盖 API Key
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def call_qwen_api(question: str, model: str = None, api_key: str = None):
|
|
|
|
|
+ """
|
|
|
|
|
+ 真实调用 Qwen API 获取思维链
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ question: 用户问题
|
|
|
|
|
+ model: 模型名称(可选,默认使用配置值)
|
|
|
|
|
+ api_key: API Key(可选,默认使用配置值)
|
|
|
|
|
+ """
|
|
|
|
|
+ # 使用传入值或默认配置
|
|
|
|
|
+ target_model = model or QWEN_MODEL
|
|
|
|
|
+ target_api_key = api_key or QWEN_API_KEY
|
|
|
|
|
+
|
|
|
|
|
+ # 构建提示词,引导模型输出思维链
|
|
|
|
|
+ think_prompt = f"""
|
|
|
|
|
+ 请用中文详细展示你的思考过程来回答以下问题:
|
|
|
|
|
+
|
|
|
|
|
+ 问题:{question}
|
|
|
|
|
+
|
|
|
|
|
+ 思考过程格式:
|
|
|
|
|
+ 1. 分析问题核心需求
|
|
|
|
|
+ 2. 检索相关知识
|
|
|
|
|
+ 3. 构建推理链条
|
|
|
|
|
+ 4. 验证逻辑正确性
|
|
|
|
|
+ 5. 生成最终回答
|
|
|
|
|
+
|
|
|
|
|
+ 请按步骤详细说明你的思考过程。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "model": target_model,
|
|
|
|
|
+ "messages": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "role": "system",
|
|
|
|
|
+ "content": "你是一个擅长展示思考过程的AI助手,请详细展示你的思考步骤。"
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "role": "user",
|
|
|
|
|
+ "content": think_prompt
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "stream": True,
|
|
|
|
|
+ "max_tokens": 1000,
|
|
|
|
|
+ "temperature": 0.7
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "Content-Type": "application/json"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # 如果有 API Key,添加认证头
|
|
|
|
|
+ if target_api_key and target_api_key != "your-api-key-here":
|
|
|
|
|
+ headers["Authorization"] = f"Bearer {target_api_key}"
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=120.0) as client:
|
|
|
|
|
+ async with client.stream(
|
|
|
|
|
+ "POST",
|
|
|
|
|
+ QWEN_API_URL,
|
|
|
|
|
+ json=payload,
|
|
|
|
|
+ headers=headers
|
|
|
|
|
+ ) as response:
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ try:
|
|
|
|
|
+ error_data = await response.json()
|
|
|
|
|
+ error_msg = error_data.get("error", {}).get("message", f"API返回错误: {response.status_code}")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ error_msg = f"API请求失败: {response.status_code}"
|
|
|
|
|
+
|
|
|
|
|
+ yield {"type": "error", "content": error_msg}
|
|
|
|
|
+ yield {"type": "completed", "content": ""}
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 解析流式响应
|
|
|
|
|
+ thinking_content = ""
|
|
|
|
|
+ async for chunk in response.aiter_bytes(chunk_size=64):
|
|
|
|
|
+ try:
|
|
|
|
|
+ chunk_text = chunk.decode("utf-8", errors="ignore")
|
|
|
|
|
+ # 解析 SSE 格式
|
|
|
|
|
+ for line in chunk_text.split("\n"):
|
|
|
|
|
+ if line.startswith("data: "):
|
|
|
|
|
+ data_str = line[6:].strip()
|
|
|
|
|
+ if data_str == "[DONE]":
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ data = json.loads(data_str)
|
|
|
|
|
+ choices = data.get("choices", [])
|
|
|
|
|
+ if choices:
|
|
|
|
|
+ delta = choices[0].get("delta", {})
|
|
|
|
|
+ content = delta.get("content", "")
|
|
|
|
|
+ if content:
|
|
|
|
|
+ thinking_content += content
|
|
|
|
|
+ yield {
|
|
|
|
|
+ "type": "thinking",
|
|
|
|
|
+ "content": content
|
|
|
|
|
+ }
|
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
|
+ continue
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 最终回答汇总
|
|
|
|
|
+ if thinking_content:
|
|
|
|
|
+ yield {"type": "answer", "content": thinking_content}
|
|
|
|
|
+
|
|
|
|
|
+ yield {"type": "completed", "content": ""}
|
|
|
|
|
+
|
|
|
|
|
+ except httpx.TimeoutException:
|
|
|
|
|
+ yield {"type": "error", "content": "API请求超时"}
|
|
|
|
|
+ yield {"type": "completed", "content": ""}
|
|
|
|
|
+ except httpx.ConnectError:
|
|
|
|
|
+ yield {"type": "error", "content": "无法连接到Qwen服务"}
|
|
|
|
|
+ yield {"type": "completed", "content": ""}
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ yield {"type": "error", "content": f"API调用异常: {str(e)}"}
|
|
|
|
|
+ yield {"type": "completed", "content": ""}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.post("/api/v1/think")
|
|
|
|
|
+async def think_stream(request: Request):
|
|
|
|
|
+ """
|
|
|
|
|
+ SSE 接口:实时返回 Qwen 模型的思维链
|
|
|
|
|
+
|
|
|
|
|
+ 请求体:
|
|
|
|
|
+ {
|
|
|
|
|
+ "question": "您的问题",
|
|
|
|
|
+ "max_tokens": 500,
|
|
|
|
|
+ "model": "Qwen/Qwen2-7B-Instruct", # 可选,覆盖默认模型
|
|
|
|
|
+ "api_key": "your-key" # 可选,覆盖默认API Key
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 响应格式(SSE):
|
|
|
|
|
+ - thinking: 思考过程(流式返回)
|
|
|
|
|
+ - answer: 最终回答汇总
|
|
|
|
|
+ - error: 错误信息
|
|
|
|
|
+ - completed: 完成标记
|
|
|
|
|
+ """
|
|
|
|
|
+ # 解析请求体
|
|
|
|
|
+ body = await request.body()
|
|
|
|
|
+ try:
|
|
|
|
|
+ data = json.loads(body.decode("utf-8"))
|
|
|
|
|
+ question = data.get("question", "")
|
|
|
|
|
+ model = data.get("model", None)
|
|
|
|
|
+ api_key = data.get("api_key", None)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ question = "用户提问"
|
|
|
|
|
+ model = None
|
|
|
|
|
+ api_key = None
|
|
|
|
|
+
|
|
|
|
|
+ async def stream_generator():
|
|
|
|
|
+ async for step in call_qwen_api(question, model, api_key):
|
|
|
|
|
+ yield f"data: {json.dumps(step, ensure_ascii=False)}\n\n"
|
|
|
|
|
+
|
|
|
|
|
+ return StreamingResponse(
|
|
|
|
|
+ stream_generator(),
|
|
|
|
|
+ media_type="text/event-stream",
|
|
|
|
|
+ headers={
|
|
|
|
|
+ "Cache-Control": "no-cache",
|
|
|
|
|
+ "Connection": "keep-alive",
|
|
|
|
|
+ "Access-Control-Allow-Origin": "*",
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.get("/")
|
|
|
|
|
+async def root():
|
|
|
|
|
+ return {
|
|
|
|
|
+ "service": "Qwen Think Process API",
|
|
|
|
|
+ "version": "1.0.0",
|
|
|
|
|
+ "endpoints": {
|
|
|
|
|
+ "think": "POST /api/v1/think (SSE)"
|
|
|
|
|
+ },
|
|
|
|
|
+ "description": "实时展示 Qwen 模型的思维链过程",
|
|
|
|
|
+ "config": {
|
|
|
|
|
+ "api_url": QWEN_API_URL,
|
|
|
|
|
+ "default_model": QWEN_MODEL
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.get("/health")
|
|
|
|
|
+async def health():
|
|
|
|
|
+ return {"status": "healthy"}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ import uvicorn
|
|
|
|
|
+ uvicorn.run("qwen:app", host="0.0.0.0", port=8000, reload=True)
|