fix: 经验库命中数算错(单条恒显示 0)+ 蒸馏前缀误杀 + 命中卡片带摘要
- _find_experiences 改为返回 (注入文本, 命中配方列表):此前卡片用
exp_ctx.count('\n- ') 计数,单条恒为 0、两条为 1(永远少 1),用户看到
「命中 0 条」误判为没参考;现用 len(列表) 得真实条数并附配方摘要
- 新增 _clean_recipe():清洗模型可能加的「操作配方:」前缀,替换旧判定
"配方" not in recipe[:50]——该判定与蒸馏提示词(要求以「操作配方」作答)
自相矛盾,模型照做即被整条丢弃(静默),是经验存不下来的主因
- doc/API.md:SSE step 说明补 🧠 伪卡片语义(N 为实际条数 + 摘要)
实测:同一指令由「命中 0 条」变为「命中 1 条同类历史经验,已注入参考:<摘要>」
This commit is contained in:
+1
-1
@@ -1193,7 +1193,7 @@ AI 可用设备列表(在线状态 + 是否有任务运行,前端据此把 b
|
|||||||
|
|
||||||
订阅事件流(SSE,EventSource)。事件:
|
订阅事件流(SSE,EventSource)。事件:
|
||||||
- `event: delta` `{text, kind: content|reasoning}` — 流式文本增量
|
- `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: done` `{answer}` — 完成
|
||||||
- `event: error` `{message}` — 失败(若因 MCP Server 未启动/不可达,message 为明确文案「MCP server(8033) 不可达 …」,不再是 SDK 原始的 `Server returned an error response`)
|
- `event: error` `{message}` — 失败(若因 MCP Server 未启动/不可达,message 为明确文案「MCP server(8033) 不可达 …」,不再是 SDK 原始的 `Server returned an error response`)
|
||||||
- 空闲时每 15s 发一行 `: keepalive` 注释防超时;`done`/`error` 后关流
|
- 空闲时每 15s 发一行 `: keepalive` 注释防超时;`done`/`error` 后关流
|
||||||
|
|||||||
+30
-13
@@ -186,23 +186,24 @@ def _qualify_experience(prompt, recipe):
|
|||||||
def _find_experiences(prompt, limit=2, threshold=0.10):
|
def _find_experiences(prompt, limit=2, threshold=0.10):
|
||||||
"""按 bigram 重叠检索相似历史经验(prompt 与任务描述的字符相似度)。
|
"""按 bigram 重叠检索相似历史经验(prompt 与任务描述的字符相似度)。
|
||||||
|
|
||||||
命中的经验 hits+1(回写),让被反复参考的有效经验浮到前面。
|
返回 (注入文本, 命中的配方列表);命中的经验 hits+1(回写),让被反复
|
||||||
|
参考的有效经验浮到前面。配方列表供调用方展示「命中 N 条」与摘要。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if _flask_app is None:
|
if _flask_app is None:
|
||||||
return ""
|
return "", []
|
||||||
with _flask_app.app_context():
|
with _flask_app.app_context():
|
||||||
_ensure_exp_table()
|
_ensure_exp_table()
|
||||||
rows = db.session.execute(db.text(
|
rows = db.session.execute(db.text(
|
||||||
"SELECT id, task_prompt, recipe, hits FROM agent_experience "
|
"SELECT id, task_prompt, recipe, hits FROM agent_experience "
|
||||||
"WHERE recipe != '' ORDER BY hits DESC, id DESC LIMIT 50")).fetchall()
|
"WHERE recipe != '' ORDER BY hits DESC, id DESC LIMIT 50")).fetchall()
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return "", []
|
||||||
if not rows:
|
if not rows:
|
||||||
return ""
|
return "", []
|
||||||
cur = _bigrams(prompt)
|
cur = _bigrams(prompt)
|
||||||
if not cur:
|
if not cur:
|
||||||
return ""
|
return "", []
|
||||||
scored = []
|
scored = []
|
||||||
for row_id, task_prompt, recipe, hits in rows:
|
for row_id, task_prompt, recipe, hits in rows:
|
||||||
sim = len(cur & _bigrams(task_prompt)) / len(cur)
|
sim = len(cur & _bigrams(task_prompt)) / len(cur)
|
||||||
@@ -220,10 +221,24 @@ def _find_experiences(prompt, limit=2, threshold=0.10):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
parts = []
|
picked = [recipe for _sim, _hits, recipe, _row_id in scored[:limit]]
|
||||||
for sim, _hits, recipe, _row_id in scored[:limit]:
|
parts = [f"- {r[:600]}" for r in picked]
|
||||||
parts.append(f"- {recipe[:600]}")
|
return "\n".join(parts), picked
|
||||||
return "\n".join(parts)
|
|
||||||
|
|
||||||
|
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):
|
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)
|
stop_evt = _stop_events.get(run_id)
|
||||||
|
|
||||||
# 经验检索:相似历史任务的操作配方注入 system(自进化记忆)
|
# 经验检索:相似历史任务的操作配方注入 system(自进化记忆)
|
||||||
exp_ctx = _find_experiences(prompt)
|
exp_ctx, exp_items = _find_experiences(prompt)
|
||||||
if exp_ctx:
|
if exp_ctx:
|
||||||
_log.info("命中历史经验,注入参考配方")
|
_log.info("命中历史经验,注入参考配方")
|
||||||
|
brief = ";".join("".join(x.split())[:16] for x in exp_items[:2])
|
||||||
q.put(("step", {"tool": "🧠 经验记忆",
|
q.put(("step", {"tool": "🧠 经验记忆",
|
||||||
"args": f"命中 {exp_ctx.count(chr(10) + '- ')} 条同类历史经验,已注入参考",
|
"args": f"命中 {len(exp_items)} 条同类历史经验,已注入参考"
|
||||||
|
+ (f":{brief}" if brief else ""),
|
||||||
"image": None}))
|
"image": None}))
|
||||||
|
|
||||||
mcp_url = getattr(getattr(agent, "s", None), "mcp_url", "")
|
mcp_url = getattr(getattr(agent, "s", None), "mcp_url", "")
|
||||||
@@ -935,8 +952,8 @@ def _agent_thread(run_id, prompt, serial, cfg):
|
|||||||
# 提炼/保存失败静默(不阻塞、不影响结果),只在成功时推送 🧠 卡片。
|
# 提炼/保存失败静默(不阻塞、不影响结果),只在成功时推送 🧠 卡片。
|
||||||
if tool_seq:
|
if tool_seq:
|
||||||
try:
|
try:
|
||||||
recipe = _distill_experience(cfg, prompt, " -> ".join(tool_seq))
|
recipe = _clean_recipe(_distill_experience(cfg, prompt, " -> ".join(tool_seq)))
|
||||||
if recipe and "配方" not in recipe[:50]:
|
if recipe:
|
||||||
if _qualify_experience(prompt, recipe):
|
if _qualify_experience(prompt, recipe):
|
||||||
if _save_experience(prompt, recipe, " -> ".join(tool_seq)):
|
if _save_experience(prompt, recipe, " -> ".join(tool_seq)):
|
||||||
_log.info("经验已写入记忆库,随事件流提示")
|
_log.info("经验已写入记忆库,随事件流提示")
|
||||||
|
|||||||
Reference in New Issue
Block a user