diff --git a/doc/API.md b/doc/API.md index 369fba6..c143a20 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1193,7 +1193,7 @@ AI 可用设备列表(在线状态 + 是否有任务运行,前端据此把 b 订阅事件流(SSE,EventSource)。事件: - `event: delta` `{text, kind: content|reasoning}` — 流式文本增量 -- `event: step` `{tool, args, image?}` — 工具调用完成(MCP 步骤,image 为缩略截图) +- `event: step` `{tool, args, image?}` — 工具调用完成(MCP 步骤,image 为缩略截图)。另有两类伪卡片:`tool="🧠 经验记忆"` 表示命中历史经验(args 形如「命中 N 条同类历史经验,已注入参考:<配方摘要>」,N 为实际条数)或本轮已写入经验库 - `event: done` `{answer}` — 完成 - `event: error` `{message}` — 失败(若因 MCP Server 未启动/不可达,message 为明确文案「MCP server(8033) 不可达 …」,不再是 SDK 原始的 `Server returned an error response`) - 空闲时每 15s 发一行 `: keepalive` 注释防超时;`done`/`error` 后关流 diff --git a/web/agent_api.py b/web/agent_api.py index e374562..29a0fde 100644 --- a/web/agent_api.py +++ b/web/agent_api.py @@ -186,23 +186,24 @@ def _qualify_experience(prompt, recipe): def _find_experiences(prompt, limit=2, threshold=0.10): """按 bigram 重叠检索相似历史经验(prompt 与任务描述的字符相似度)。 - 命中的经验 hits+1(回写),让被反复参考的有效经验浮到前面。 + 返回 (注入文本, 命中的配方列表);命中的经验 hits+1(回写),让被反复 + 参考的有效经验浮到前面。配方列表供调用方展示「命中 N 条」与摘要。 """ try: if _flask_app is None: - return "" + return "", [] with _flask_app.app_context(): _ensure_exp_table() rows = db.session.execute(db.text( "SELECT id, task_prompt, recipe, hits FROM agent_experience " "WHERE recipe != '' ORDER BY hits DESC, id DESC LIMIT 50")).fetchall() except Exception: - return "" + return "", [] if not rows: - return "" + return "", [] cur = _bigrams(prompt) if not cur: - return "" + return "", [] scored = [] for row_id, task_prompt, recipe, hits in rows: sim = len(cur & _bigrams(task_prompt)) / len(cur) @@ -220,10 +221,24 @@ def _find_experiences(prompt, limit=2, threshold=0.10): db.session.commit() except Exception: pass - parts = [] - for sim, _hits, recipe, _row_id in scored[:limit]: - parts.append(f"- {recipe[:600]}") - return "\n".join(parts) + picked = [recipe for _sim, _hits, recipe, _row_id in scored[:limit]] + parts = [f"- {r[:600]}" for r in picked] + return "\n".join(parts), picked + + +def _clean_recipe(recipe): + """清洗蒸馏输出:去掉模型可能加的「操作配方:」之类前缀标签与包裹引号。 + + 旧逻辑用 `"配方" not in recipe[:50]` 直接拒绝——而蒸馏提示词本就要求以 + 「操作配方」作答,模型一旦照做就会被整条丢弃(静默),导致经验很难存下。 + 改为清洗前缀而非丢弃。 + """ + t = (recipe or "").strip() + if not t: + return "" + t = re.sub(r'^[^\n]{0,12}?配方[::]\s*', '', t, count=1).strip() + t = re.sub(r'^(操作步骤|步骤|流程)[::]\s*', '', t).strip() + return t.strip('"“”\'').strip() def _distill_experience(cfg, prompt, tool_seq): @@ -862,11 +877,13 @@ def _agent_thread(run_id, prompt, serial, cfg): stop_evt = _stop_events.get(run_id) # 经验检索:相似历史任务的操作配方注入 system(自进化记忆) - exp_ctx = _find_experiences(prompt) + exp_ctx, exp_items = _find_experiences(prompt) if exp_ctx: _log.info("命中历史经验,注入参考配方") + brief = ";".join("".join(x.split())[:16] for x in exp_items[:2]) q.put(("step", {"tool": "🧠 经验记忆", - "args": f"命中 {exp_ctx.count(chr(10) + '- ')} 条同类历史经验,已注入参考", + "args": f"命中 {len(exp_items)} 条同类历史经验,已注入参考" + + (f":{brief}" if brief else ""), "image": None})) mcp_url = getattr(getattr(agent, "s", None), "mcp_url", "") @@ -935,8 +952,8 @@ def _agent_thread(run_id, prompt, serial, cfg): # 提炼/保存失败静默(不阻塞、不影响结果),只在成功时推送 🧠 卡片。 if tool_seq: try: - recipe = _distill_experience(cfg, prompt, " -> ".join(tool_seq)) - if recipe and "配方" not in recipe[:50]: + recipe = _clean_recipe(_distill_experience(cfg, prompt, " -> ".join(tool_seq))) + if recipe: if _qualify_experience(prompt, recipe): if _save_experience(prompt, recipe, " -> ".join(tool_seq)): _log.info("经验已写入记忆库,随事件流提示")