diff --git a/demo/web/server.py b/demo/web/server.py index 77eb486..fd4ef6e 100644 --- a/demo/web/server.py +++ b/demo/web/server.py @@ -910,19 +910,47 @@ def generate_paradigm( {dialog_text} ## 输出要求 -请基于上述产品详情(定位、卖点、目标人群)和真实对话,提炼并生成一套**标准范式对话模板**,即针对此类客户的最优销售话术流程。话术要自然融入产品的核心卖点和定位,不要生硬推销。输出严格 JSON: -- customer_type: 客户类型描述(如"湿疹宝妈-价格敏感型") -- product_positioning: 对该客户的产品推荐理由(结合产品定位和客户需求,1-2句话) -- stages: 按销售阶段排列的对话步骤数组,每个元素包含: - - stage: 阶段名称(建立联系/需求发现/报价/异议处理/成交) - - goal: 该阶段目标 - - sales_script: 销售话术(1-3句,具体可执行,自然融入产品卖点) - - expected_response: 预期客户回应 - - tips: 该阶段技巧提示 -- key_techniques: 核心销售技巧总结(数组,每条一句话) -- improvement_points: 真实对话中可改进的点(数组,每条一句话) +请基于上述产品详情(定位、卖点、目标人群)和真实对话,提炼并生成一套**标准范式对话模板**,即针对此类客户的最优销售话术流程。话术要自然融入产品的核心卖点和定位,不要生硬推销。 -只输出 JSON,不要其他文字。""" +请用 Markdown 格式输出,结构如下: + +## 客户类型 +(描述客户类型,如"湿疹宝妈-价格敏感型") + +## 产品推荐理由 +(结合产品定位和客户需求,1-2句话说明为什么推荐这个产品) + +## 标准销售流程 + +### 1. 建立联系 +- **目标**:xxx +- **销售话术**:xxx +- **预期回应**:xxx +- **技巧提示**:xxx + +### 2. 需求发现 +(同上格式) + +### 3. 报价 +(同上格式) + +### 4. 异议处理 +(同上格式) + +### 5. 成交 +(同上格式) + +## 核心销售技巧 +- 技巧1 +- 技巧2 +- ... + +## 真实对话可改进的点 +- 改进点1 +- 改进点2 +- ... + +直接输出 Markdown 内容,不要输出 JSON。""" body = json.dumps({ "model": "qwen-plus", @@ -930,7 +958,6 @@ def generate_paradigm( {"role": "system", "content": "你是微信销售对话分析专家,擅长从真实对话中提炼标准范式。"}, {"role": "user", "content": prompt}, ], - "response_format": {"type": "json_object"}, "temperature": 0.4, "stream": True, "stream_options": {"include_usage": False}, diff --git a/demo/web/static/index.html b/demo/web/static/index.html index 211e6df..b39b81d 100644 --- a/demo/web/static/index.html +++ b/demo/web/static/index.html @@ -1138,10 +1138,10 @@ async function generateParadigm() { 正在调用千问 LLM 生成标准范式对话... -
+ `; - const streamText = document.getElementById('paradigm-stream-text'); + const mdContainer = document.getElementById('paradigm-md'); let fullContent = ''; try { const productId = document.getElementById('paradigm-product-select').value; @@ -1152,6 +1152,15 @@ async function generateParadigm() { const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; + let renderRaf = null; + function scheduleRender() { + if (renderRaf) return; + renderRaf = requestAnimationFrame(() => { + renderRaf = null; + mdContainer.innerHTML = renderMarkdown(fullContent); + mdContainer.scrollTop = mdContainer.scrollHeight; + }); + } while (true) { const { done, value } = await reader.read(); if (done) break; @@ -1167,28 +1176,19 @@ async function generateParadigm() { if (obj.error) throw new Error(obj.error); if (obj.content) { fullContent += obj.content; - streamText.textContent = fullContent; - streamText.scrollTop = streamText.scrollHeight; + scheduleRender(); } } catch (e) { // 忽略解析错误的行 } } } - // 尝试解析完整 JSON 并渲染结构化结果 - try { - const data = JSON.parse(fullContent); - renderParadigm(data); - } catch (e) { - // JSON 解析失败,保留原始流式文本 - result.innerHTML = ` -LLM 返回内容无法解析为 JSON,以下为原始输出:
-${escapeHtml(fullContent)}
- 生成失败: ${e.message}
`; } finally { @@ -1197,6 +1197,60 @@ async function generateParadigm() { } } +/** 轻量 Markdown 转 HTML 渲染器 */ +function renderMarkdown(md) { + if (!md) return ''; + const lines = md.split('\n'); + let html = ''; + let inList = false; + let inOrderedList = false; + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + // 标题 + if (/^### /.test(line)) { + if (inList) { html += ''; inList = false; } + if (inOrderedList) { html += ''; inOrderedList = false; } + html += `${inlineMd(line)}
`; + } + } + if (inList) html += ''; + if (inOrderedList) html += ''; + return html; +} + +/** 行内 Markdown:粗体、行内代码、链接 */ +function inlineMd(text) { + // 转义 HTML + text = escapeHtml(text); + // 粗体 **text** + text = text.replace(/\*\*(.+?)\*\*/g, '$1'); + // 行内代码 `code` + text = text.replace(/`(.+?)`/g, '$1');
+ return text;
+}
+
function renderParadigm(data) {
const result = document.getElementById('paradigm-result');
const stageColors = {