diff --git a/demo/web/server.py b/demo/web/server.py index be5fbcd..77eb486 100644 --- a/demo/web/server.py +++ b/demo/web/server.py @@ -18,7 +18,7 @@ import psycopg2 import psycopg2.pool from fastapi import FastAPI, HTTPException, Query from fastapi.staticfiles import StaticFiles -from fastapi.responses import RedirectResponse +from fastapi.responses import RedirectResponse, StreamingResponse from pydantic import BaseModel # --------------------------------------------------------------------------- @@ -932,6 +932,8 @@ def generate_paradigm( ], "response_format": {"type": "json_object"}, "temperature": 0.4, + "stream": True, + "stream_options": {"include_usage": False}, }).encode("utf-8") req = _urllib.Request( @@ -944,14 +946,40 @@ def generate_paradigm( method="POST", ) - try: - with _urllib.urlopen(req, timeout=60) as resp: - result = json.loads(resp.read().decode("utf-8")) - content = result["choices"][0]["message"]["content"] - parsed = json.loads(content) - return parsed - except Exception as exc: - raise HTTPException(status_code=502, detail=f"LLM 调用失败: {exc}") + def stream_generator(): + """SSE 流式生成器,逐块推送 LLM content delta。""" + try: + resp = _urllib.urlopen(req, timeout=120) + buffer = b"" + while True: + chunk = resp.read(4096) + if not chunk: + break + buffer += chunk + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + line = line.strip() + if not line: + continue + if line.startswith(b"data:"): + data_str = line[5:].strip() + if data_str == b"[DONE]": + yield "data: [DONE]\n\n" + return + try: + chunk_obj = json.loads(data_str) + delta = chunk_obj.get("choices", [{}])[0].get("delta", {}) + content = delta.get("content", "") + if content: + yield f"data: {json.dumps({'content': content}, ensure_ascii=False)}\n\n" + except (json.JSONDecodeError, IndexError, KeyError): + continue + resp.close() + except Exception as exc: + yield f"data: {json.dumps({'error': str(exc)}, ensure_ascii=False)}\n\n" + yield "data: [DONE]\n\n" + + return StreamingResponse(stream_generator(), media_type="text/event-stream") @app.get("/api/sync/status") diff --git a/demo/web/static/index.html b/demo/web/static/index.html index 1323509..211e6df 100644 --- a/demo/web/static/index.html +++ b/demo/web/static/index.html @@ -1132,13 +1132,63 @@ async function generateParadigm() { const result = document.getElementById('paradigm-result'); btn.disabled = true; btn.textContent = '生成中...'; - result.innerHTML = '

正在调用千问 LLM 生成标准范式对话,请稍候...

'; + result.innerHTML = ` +
+
+ + 正在调用千问 LLM 生成标准范式对话... +
+

+    
+ `; + const streamText = document.getElementById('paradigm-stream-text'); + let fullContent = ''; try { const productId = document.getElementById('paradigm-product-select').value; - let paradigmPath = `/conversations/${customerId}/paradigm`; - if (productId) paradigmPath += `?product_id=${productId}`; - const data = await api(paradigmPath, { method: 'POST' }); - renderParadigm(data); + let paradigmUrl = `${API}/conversations/${customerId}/paradigm`; + if (productId) paradigmUrl += `?product_id=${productId}`; + const resp = await fetch(paradigmUrl, { method: 'POST' }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const reader = resp.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); + for (const line of lines) { + if (!line.startsWith('data: ')) continue; + const dataStr = line.slice(6).trim(); + if (dataStr === '[DONE]') continue; + try { + const obj = JSON.parse(dataStr); + if (obj.error) throw new Error(obj.error); + if (obj.content) { + fullContent += obj.content; + streamText.textContent = fullContent; + streamText.scrollTop = streamText.scrollHeight; + } + } catch (e) { + // 忽略解析错误的行 + } + } + } + // 尝试解析完整 JSON 并渲染结构化结果 + try { + const data = JSON.parse(fullContent); + renderParadigm(data); + } catch (e) { + // JSON 解析失败,保留原始流式文本 + result.innerHTML = ` +
+

标准范式对话模板

+

LLM 返回内容无法解析为 JSON,以下为原始输出:

+
${escapeHtml(fullContent)}
+
+ `; + } } catch (e) { result.innerHTML = `

生成失败: ${e.message}

`; } finally {