@hupan56/wlkj 3.3.14 → 3.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/bin/cli.js +16 -1
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-spec.md +4 -2
  4. package/templates/qoder/commands/wl-commit.md +3 -1
  5. package/templates/qoder/commands/wl-init.md +2 -2
  6. package/templates/qoder/commands/wl-prd.md +24 -0
  7. package/templates/qoder/commands/wl-search.md +18 -11
  8. package/templates/qoder/commands/wl-task.md +3 -1
  9. package/templates/qoder/config.yaml +0 -6
  10. package/templates/qoder/contracts/insight.md +55 -0
  11. package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
  12. package/templates/qoder/hooks/session-start.py +20 -1
  13. package/templates/qoder/hooks/stop-eval.py +129 -0
  14. package/templates/qoder/scripts/capability/adapters/mcp.py +9 -1
  15. package/templates/qoder/scripts/capability/registry.py +0 -3
  16. package/templates/qoder/scripts/deployment/setup/init_doctor.py +6 -2
  17. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +7 -7
  18. package/templates/qoder/scripts/deployment/setup/setup.py +5 -2
  19. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  20. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  21. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
  22. package/templates/qoder/scripts/domain/kg/kg.py +14 -0
  23. package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
  24. package/templates/qoder/scripts/tool_guide.md +70 -0
  25. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
  26. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
  27. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
  28. package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
  29. package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
  30. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
  31. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
  32. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
  33. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
  34. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
  35. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
  36. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
  37. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
  38. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
  39. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
  40. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
  41. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
  42. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
  43. package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
  44. package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
  45. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
  46. package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
  47. package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
  48. package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
  49. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
  50. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  51. package/templates/qoder/agents/design-agent.md +0 -20
  52. package/templates/qoder/agents/spec-generator.md +0 -21
  53. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
  54. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
  55. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
@@ -438,20 +438,20 @@ def rewrite_mcp_json_for_launcher():
438
438
  }
439
439
  changed = False
440
440
  for srv, sub in name_map.items():
441
- # 共识:kg 走云平台(读 config platform_mcp.url → SSE url),不连本地 kg.duckdb(空)
441
+ # 共识:kg 走云平台(默认 http://10.89.7.120:10010/mcp,config platform_mcp.url 可覆盖)
442
442
  if srv == "qoder-knowledge-graph":
443
+ _pm = "http://10.89.7.120:10010/mcp"
443
444
  try:
444
445
  import yaml as _y
445
446
  _cfg_p = get_repo_root() / ".qoder" / "config.yaml"
446
447
  if _cfg_p.is_file():
447
448
  _cfg = _y.safe_load(_cfg_p.read_text(encoding="utf-8")) or {}
448
- _pm = (_cfg.get("platform_mcp") or {}).get("url", "")
449
- if _pm:
450
- servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
451
- changed = True
452
- continue
449
+ _pm = (_cfg.get("platform_mcp") or {}).get("url", "") or _pm
453
450
  except Exception:
454
- pass # config 无 platform_mcp → 回退 launcher
451
+ pass
452
+ servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
453
+ changed = True
454
+ continue
455
455
  entry = servers.get(srv)
456
456
  # 全新机器: kg/mysql/lanhu 条目可能不存在 → 创建它 (launcher 模式)
457
457
  if not entry:
@@ -242,9 +242,11 @@ def detect_name(explicit=None):
242
242
  # Step 2: 调 init_doctor --fix (主体)
243
243
  # ============================================================
244
244
 
245
- def run_doctor(name, role):
245
+ def run_doctor(name, role, skip_code=False):
246
246
  print('\n--- 初始化 (init_doctor) ---')
247
247
  cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
248
+ if skip_code:
249
+ cmd.append('--skip-code')
248
250
  # 透传 stdio (用户能看到实时输出) + 强制 UTF-8 环境 (解决 cmd GBK 乱码)
249
251
  env = dict(os.environ)
250
252
  env['PYTHONIOENCODING'] = 'utf-8'
@@ -911,6 +913,7 @@ def main():
911
913
  parser.add_argument('--skip-cron', action='store_true', help='跳过 cron 注册')
912
914
  parser.add_argument('--skip-qoderwork', action='store_true', help='跳过 QoderWork 安装')
913
915
  parser.add_argument('--skip-lanhu', action='store_true', help='跳过蓝湖 MCP 引导')
916
+ parser.add_argument('--skip-code', '--cloud', action='store_true', help='云模式:跳过本地源码 clone/索引(KG 在云平台)')
914
917
  args = parser.parse_args()
915
918
 
916
919
  print('=' * 56)
@@ -931,7 +934,7 @@ def main():
931
934
  configure_git_identity(name)
932
935
 
933
936
  # Step 2
934
- if not run_doctor(name, args.role):
937
+ if not run_doctor(name, args.role, args.skip_code):
935
938
  warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
936
939
 
937
940
  # Step 3
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
16
5
  让 kg.py feature <功能名> 能返回功能画像。
17
6
 
@@ -34,6 +23,18 @@ from bootstrap import setup; setup()
34
23
  业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
35
24
  那些脚本未实现时不影响本脚本的 entity 基础层。
36
25
  """
26
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
27
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
28
+ import os as _o, sys as _s
29
+ _f = _o.path.abspath(__file__)
30
+ for _ in range(10):
31
+ _f = _o.path.dirname(_f)
32
+ _cp = _o.path.join(_f, 'foundation')
33
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
34
+ break
35
+ if _cp not in _s.path: _s.path.insert(0, _cp)
36
+ from bootstrap import setup; setup()
37
+
37
38
  import json
38
39
  import os
39
40
  import sys
@@ -41,7 +42,6 @@ from datetime import datetime
41
42
 
42
43
  # 路径自适应 (和兄弟脚本一致)
43
44
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
44
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
45
45
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
46
46
 
47
47
 
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
16
5
  让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
17
6
 
@@ -32,13 +21,24 @@ from bootstrap import setup; setup()
32
21
  - code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
33
22
  - test-assertions.json: tests (controller_to_tests 推导)
34
23
  """
24
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
25
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
26
+ import os as _o, sys as _s
27
+ _f = _o.path.abspath(__file__)
28
+ for _ in range(10):
29
+ _f = _o.path.dirname(_f)
30
+ _cp = _o.path.join(_f, 'foundation')
31
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
32
+ break
33
+ if _cp not in _s.path: _s.path.insert(0, _cp)
34
+ from bootstrap import setup; setup()
35
+
35
36
  import json
36
37
  import os
37
38
  import sys
38
39
  from datetime import datetime
39
40
 
40
41
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
42
42
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
43
43
 
44
44
 
@@ -278,6 +278,31 @@ def build_embeddings(batch_size=64):
278
278
  con.close()
279
279
 
280
280
 
281
+ def _cloud_rag_search(query, top_k=10):
282
+ """云优先语义搜索:调工作台 MCP rag_search(本地无 KG/embedding,全迁云)。
283
+
284
+ Returns:
285
+ [(entity_id, text, score), ...] 或 None(云不可用时返回 None,让调用方退本地)。
286
+ """
287
+ try:
288
+ from domain.kg.search._remote import call_remote
289
+ data = call_remote("rag_search", {"query": query, "top_k": top_k})
290
+ except Exception:
291
+ return None
292
+ items = data.get("items") or []
293
+ out = []
294
+ for it in items[:top_k]:
295
+ eid = it.get("entity_id") or it.get("id") or ""
296
+ text = it.get("text") or it.get("name") or ""
297
+ score = it.get("score")
298
+ try:
299
+ score = float(score) if score is not None else 0.0
300
+ except Exception:
301
+ score = 0.0
302
+ out.append((eid, text, score))
303
+ return out
304
+
305
+
281
306
  def semantic_search(query, top_k=10):
282
307
  """语义搜索: query→向量→余弦相似度→top_k 实体。
283
308
 
@@ -656,13 +681,24 @@ if __name__ == '__main__':
656
681
  n = build_embeddings()
657
682
  print('Built %d embeddings' % n)
658
683
 
659
- elif len(sys.argv) > 2 and sys.argv[1] == 'search':
660
- results = semantic_search(sys.argv[2])
684
+ elif (len(sys.argv) > 2 and sys.argv[1] == 'search') or \
685
+ (len(sys.argv) > 1 and sys.argv[1] not in ('--check', '--label', 'build-embeddings', '--status', '--backfill-cn')):
686
+ # 兼容两种姿势: `search <词>` 和裸词 `<词>`(wlkj.py semantic 保险异常 透传过来)。
687
+ # ★ 云优先:本地无 KG/embedding(全迁云),先调云 rag_search;云不可用才退本地 semantic_search。
688
+ query = sys.argv[2] if sys.argv[1] == 'search' else sys.argv[1]
689
+ results = _cloud_rag_search(query)
690
+ if results is None:
691
+ # 云不可用 → 退本地(开发期有本地 embedding 时仍可用)
692
+ results = semantic_search(query)
661
693
  if results:
662
- for eid, text, score in results:
663
- print(' %.3f %s' % (score, text[:70]))
694
+ for row in results:
695
+ if len(row) == 3:
696
+ eid, text, score = row
697
+ print(' %.3f %s' % (score, text[:70]))
698
+ else:
699
+ print(' %s' % row)
664
700
  else:
665
- print('(无结果或 embedding 未构建)')
701
+ print('(无结果或云 rag_search 不可用 + 本地 embedding 未构建)')
666
702
 
667
703
  elif len(sys.argv) > 1 and sys.argv[1] == '--status':
668
704
  st = embedding_status()
@@ -872,6 +872,20 @@ def main(argv=None):
872
872
  return 0
873
873
  cmd = argv[0]
874
874
  rest = argv[1:]
875
+ # ★ MCP 名兼容:AI 常把 MCP 工具名(search_code)当 kg.py 子命令调 → "未知子命令"。
876
+ # 在此统一归一化,AI 猜哪个名(kg.py 子命令 / MCP 工具名)都能命中(治工作流混名)。
877
+ _MCP_ALIASES = {
878
+ 'search_code': 'search', 'search_api': 'api', 'search_prds': 'prd',
879
+ 'search_field': 'search', 'search_style': 'search', 'search_wiki': 'wiki',
880
+ 'rag_search': 'semantic', 'ask_corpus': 'semantic',
881
+ 'context_pack': 'context', 'context_360': 'context360',
882
+ 'get_impact': 'impact', 'multi_hop': 'hop',
883
+ 'coverage_matrix': 'coverage', 'feature_overview': 'feature',
884
+ 'get_workflow': 'workflow', 'get_design_system': 'design-system',
885
+ 'fill_prototype': 'fill-prototype',
886
+ }
887
+ if cmd in _MCP_ALIASES:
888
+ cmd = _MCP_ALIASES[cmd]
875
889
  # 查找子命令
876
890
  handler = None
877
891
  for name, fn, _ in COMMANDS:
@@ -0,0 +1,94 @@
1
+ """context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
2
+
3
+ PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
4
+
5
+ 存储: workspace/specs/{REQ}/.context-cache.json
6
+ 结构: {query_hash: {"query":..., "text":..., "ts":...}}
7
+ TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
8
+
9
+ 纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
10
+ """
11
+ import os
12
+ import json
13
+ import time
14
+ import hashlib
15
+
16
+ DEFAULT_TTL = 24 * 3600 # 24h
17
+
18
+
19
+ def _repo_root():
20
+ """从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
21
+ d = os.path.dirname(os.path.abspath(__file__))
22
+ for _ in range(7):
23
+ if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
24
+ return d
25
+ parent = os.path.dirname(d)
26
+ if parent == d:
27
+ break
28
+ d = parent
29
+ return os.getcwd()
30
+
31
+
32
+ def _sanitize(req_id):
33
+ return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
34
+
35
+
36
+ def cache_path(req_id, base=None):
37
+ base = base or _repo_root()
38
+ return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
39
+
40
+
41
+ def _load(path):
42
+ try:
43
+ with open(path, encoding="utf-8") as f:
44
+ data = json.load(f)
45
+ return data if isinstance(data, dict) else {}
46
+ except Exception:
47
+ return {}
48
+
49
+
50
+ def _save(path, data):
51
+ try:
52
+ os.makedirs(os.path.dirname(path), exist_ok=True)
53
+ tmp = path + ".tmp"
54
+ with open(tmp, "w", encoding="utf-8") as f:
55
+ json.dump(data, f, ensure_ascii=False, indent=2)
56
+ os.replace(tmp, path) # 原子写
57
+ return True
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def _qhash(query):
63
+ return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
64
+
65
+
66
+ def get(req_id, query, ttl=DEFAULT_TTL, base=None):
67
+ """命中且未过期 → 返回 text;否则 None。"""
68
+ data = _load(cache_path(req_id, base))
69
+ entry = data.get(_qhash(query))
70
+ if not entry:
71
+ return None
72
+ if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
73
+ return None
74
+ return entry.get("text")
75
+
76
+
77
+ def put(req_id, query, text, base=None):
78
+ """写一条缓存(覆盖同 query 旧值)。"""
79
+ path = cache_path(req_id, base)
80
+ data = _load(path)
81
+ data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
82
+ return _save(path, data)
83
+
84
+
85
+ def invalidate(req_id, base=None):
86
+ """清整个 REQ 的缓存(PRD/spec 改动后调)。"""
87
+ return _save(cache_path(req_id, base), {})
88
+
89
+
90
+ def stats(req_id, base=None):
91
+ """缓存条数 + query 摘要(调试/观测命中率用)。"""
92
+ data = _load(cache_path(req_id, base))
93
+ return {"entries": len(data),
94
+ "queries": [str(e.get("query", ""))[:40] for e in data.values()]}
@@ -0,0 +1,70 @@
1
+ # tool_guide — /wl-* 命令共用工具用法(单一信源)
2
+
3
+ > 本文件是所有 `/wl-*` 命令**共用**的工具用法铁律的**唯一真源**。
4
+ > SessionStart 会注入精简版;命令正文不再重复,只写「见 tool_guide」。
5
+ > 改这里 = 改全部命令(单一信源,DRY)。AI 需要完整细节时直接 Read 本文件。
6
+
7
+ ---
8
+
9
+ ## §1 环境自检(任何脚本调用前先跑,定 `R` / `$PY`)
10
+
11
+ > QoderWork 桌面端 cwd 不在仓库根(在 `.qoderwork/workspace/xxx`),相对路径
12
+ > `.qoder/scripts/...` **必然失败**。进模式前必须先定位仓库根。Mac 只有 python3,两个都试:
13
+
14
+ ```bash
15
+ R=$(python ~/.qoderwork/repo_root.py 2>/dev/null || python3 ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
16
+ PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)
17
+ ```
18
+
19
+ 后续所有脚本统一用 `$PY "$R/.qoder/scripts/orchestration/wlkj.py" <命令>`。
20
+ **不准自己编 `cd /d` / 裸相对路径**。
21
+
22
+ ---
23
+
24
+ ## §2 接口铁律(防混名 / 防歧义 / 防跑飞)
25
+
26
+ ### ① 三套名字已互通(引擎层别名,AI 猜哪个都对,别因"未知子命令"慌)
27
+
28
+ | 你想干 | wlkj.py | kg.py | MCP 工具名 |
29
+ |---|---|---|---|
30
+ | 搜代码 | `search <词>` | `search <词>` | search_code |
31
+ | 搜 API | `search <词>` | `api <词>` | search_api |
32
+ | 语义检索 | `semantic <词>` | `semantic <词>` | rag_search |
33
+ | 上下文打包 | `context <词>` | `context <词>` | context_pack |
34
+ | 影响分析 | `impact <实体>` | `impact <实体>` | get_impact |
35
+
36
+ > `kg.py search_code 保险` = `kg.py search 保险`,都认。用哪个名都行。
37
+
38
+ ### ② 中文歧义词先提炼再查(否则"保险"命中的是社保不是车险)
39
+
40
+ - 保险 → 车辆保险 / 商业险 / 交强险 / 保险异常
41
+ - 异常 → 告警 alarmType / 报错 / 校验失败
42
+ - 计划 → 作业计划 / 养护计划 / 计划任务
43
+ - 列表/记录 → 带具体业务对象(车辆保险记录、考勤记录)
44
+
45
+ > 拿不准时先 `kg.py search <具体词>` 看返回的控制器/实体名,再用全名 `kg.py context <全名>`。
46
+
47
+ ### ③ 禁止跌入手动 grep/find 读源码
48
+
49
+ 知识层有就拿知识层;**只有**以下情况才用 Grep/Read 读源码:
50
+ - 知识层确实没建(search 返 0 条,且换词仍 0)
51
+ - 要看某个具体文件的实现细节(已定位到文件)
52
+
53
+ > ❌ 不要 `find data/code` / `grep -r` / `findstr /s` / `os.walk` 全盘扫——那是知识层 search 的活,手动做 = 思考链跑飞。
54
+
55
+ ---
56
+
57
+ ## §3 通用工具优先级(宿主能力 × 知识层分工)
58
+
59
+ | 步骤 | 用谁 | 原因 |
60
+ |------|------|------|
61
+ | 精确搜代码内容 | **Qoder Grep/Read** | 原生更快 |
62
+ | 语义找相关代码/字段 | **MCP rag_search** | 语义召回最全(没共享词也能找到) |
63
+ | 取业务全上下文 | **MCP context_pack** | 一次取全 8 段 |
64
+ | 查真表结构/枚举 | **MCP query_schema**(先 list_envs 问环境) | 数据库真实列名 |
65
+ | 全局业务流程问答 | **MCP ask_corpus**(GraphRAG) | 社区摘要答案级 |
66
+ | 影响分析 | **MCP get_impact** | 改动传播专用 |
67
+ | 生成正文/代码 | **Qoder AI** | 宿主 AI 比调 qwen-plus 更强 |
68
+ | 写文件 | **Qoder Write/Edit** | 原生文件操作 |
69
+
70
+ > 口诀:**精确搜用 Grep,语义找用 rag_search,取全用 context_pack,查库先问环境,影响用 get_impact**。
@@ -0,0 +1,176 @@
1
+ """alignment_matrix.py — FW6 扩展:4 命令类各自的 准确性 × 速度 × token 对齐(weak 档 Pareto 赢家)。
2
+
3
+ 用户:"针对不同指标对齐准确性和速度"。search/prd/code/task 各自的准确性指标(召回/完整性/精准/路由)
4
+ × latency(P50) × token,bare vs +工作流 scaffold,全在 weak(qwen-turbo,FW6 证的 Pareto 最优档)。
5
+ 证明:弱模型+工作流在【每个命令类】都又快又准(bare 快但废,scaffold 准且仍最快档)。
6
+
7
+ 跑法: docker exec -i wlinkj-workspace-backend-1 python /app/scripts/validation/eval/alignment_matrix.py
8
+ """
9
+ import os
10
+ import re
11
+ import sys
12
+ import json
13
+ import time
14
+ import urllib.request
15
+ import urllib.error
16
+
17
+ for _p in ("/app", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))):
18
+ if os.path.isdir(_p) and _p not in sys.path:
19
+ sys.path.insert(0, _p)
20
+
21
+ from sqlalchemy import text # noqa: E402
22
+ from app.db import SessionLocal # noqa: E402
23
+
24
+ CHAT_URL = os.environ.get("ALI_CHAT_BASE", "").rstrip("/") + "/chat/completions"
25
+ CHAT_KEY = os.environ.get("DASHSCOPE_API_KEY", "")
26
+ MODEL = "qwen-turbo"
27
+ QUICK_SECTIONS = ["功能入口", "需求背景", "需求说明", "影响范围", "验收标准", "不在本次范围"]
28
+ TASK_ACTIONS = ["create", "list", "show", "start", "finish", "archive", "rank", "plan", "sync", "zentao"]
29
+ _CODE_TOK = re.compile(r"[A-Za-z_][\w./\-:]{2,}")
30
+
31
+
32
+ def chat(prompt, max_tokens=500, timeout=90):
33
+ body = json.dumps({"model": MODEL, "messages": [{"role": "user", "content": prompt}],
34
+ "temperature": 0.0, "max_tokens": max_tokens}).encode()
35
+ t0 = time.time()
36
+ for a in range(3):
37
+ try:
38
+ req = urllib.request.Request(CHAT_URL, data=body,
39
+ headers={"Authorization": f"Bearer {CHAT_KEY}", "Content-Type": "application/json"})
40
+ d = json.loads(urllib.request.urlopen(req, timeout=timeout).read())
41
+ return d["choices"][0]["message"]["content"], int(d.get("usage", {}).get("total_tokens", 0)), round((time.time()-t0)*1000)
42
+ except (urllib.error.URLError, TimeoutError, OSError, KeyError) as e:
43
+ if a < 2: time.sleep(1.0*(a+1))
44
+ else: return f"[ERR {repr(e)[:50]}]", 0, round((time.time()-t0)*1000)
45
+
46
+
47
+ def _q(s, sql, p=None):
48
+ try: return s.execute(text(sql), p or {}).fetchall()
49
+ except Exception:
50
+ try: s.rollback()
51
+ except Exception: pass
52
+ return []
53
+
54
+
55
+ def real_entities(theme, k=15):
56
+ s = SessionLocal()
57
+ rs = _q(s, "SELECT canonical, cn FROM entities WHERE (canonical ILIKE :p OR cn ILIKE :p) LIMIT :n", {"p": f"%{theme}%", "n": k})
58
+ s.close()
59
+ return [(c, n) for c, n in rs if c]
60
+
61
+
62
+ # ── 各命令类的 bare/scaffold prompt + 准确性指标 ──
63
+ def search_task(theme, mode):
64
+ reals = real_entities(theme, 45)
65
+ ctx = "\n".join("%s (%s)" % (c, n) for c, n in reals)
66
+ if mode == "bare":
67
+ p = f"找ICS系统和「{theme}」相关的具体对象(表/类/接口),逐行列出确信真实存在的,勿编造。"
68
+ else:
69
+ p = f"找ICS系统和「{theme}」相关的具体对象。知识图谱检索到(均真实):\n{ctx}\n请把检索结果实体尽量全部逐行列出,直接用名称,勿筛选勿编造。"
70
+ return p, reals
71
+
72
+
73
+ def search_acc(ans, reals):
74
+ hit = sum(1 for c, n in reals if (c and c in ans) or (n and n in ans))
75
+ return round(hit / max(len(reals), 1), 3)
76
+
77
+
78
+ def prd_task(item, mode):
79
+ feature, theme = item
80
+ reals = real_entities(theme, 15)
81
+ ctx = "\n".join("%s (%s)" % (c, n) for c, n in reals)
82
+ if mode == "bare":
83
+ p = f"为「{feature}」写一份PRD需求文档。直接输出正文。"
84
+ else:
85
+ p = (f"为「{feature}」写PRD。检索到真实对象:\n{ctx}\n铁律:必须含6章(## 功能入口/需求背景/需求说明/"
86
+ f"影响范围/验收标准/不在本次范围),字段用检索到的真实名称,勿编造。")
87
+ return p, None
88
+
89
+
90
+ def prd_acc(ans, _):
91
+ return sum(1 for s in QUICK_SECTIONS if re.search(r'#{0,3}\s*\*{0,2}' + s, ans)) # /6
92
+
93
+
94
+ def code_task(item, mode):
95
+ feature, theme = item
96
+ reals = real_entities(theme, 45) # FW3 已证喂全 45 才达标
97
+ ctx = "\n".join("%s (%s)" % (c, n) for c, n in reals)
98
+ if mode == "bare":
99
+ p = f"给「{feature}」加功能,逐行列出需修改的代码文件/类/方法名,只列确信真实的,勿编造。"
100
+ else:
101
+ p = f"给「{feature}」加功能。get_impact 检索到真实实体:\n{ctx}\n请把检索结果实体全部逐行列出,直接用名称,勿筛选勿编造。"
102
+ return p, reals
103
+
104
+
105
+ def code_acc(ans, reals):
106
+ if not reals: return 0
107
+ hit = sum(1 for c, n in reals if (c and c in ans) or (n and n in ans))
108
+ return round(hit / len(reals), 3)
109
+
110
+
111
+ TASK_GOLD = [("建任务报价单导出", "create"), ("看任务", "list"), ("开始做登录", "start"),
112
+ ("做完了", "finish"), ("PRD发禅道", "zentao"), ("排优先级", "rank")]
113
+ TASK_SPEC = ("按第一个词/意图选动作(create/list/show/start/finish/archive/rank/plan/sync/zentao):"
114
+ "建→create/看→list/开始→start/完了→finish/PRD发禅道或记工时→zentao/排优先级→rank")
115
+
116
+
117
+ def task_task(query, mode):
118
+ if mode == "bare":
119
+ p = f"判断 /wl-task 操作「{query}」该走哪个动作(从 {TASK_ACTIONS} 选),只输出动作名。"
120
+ else:
121
+ p = f"{TASK_SPEC}\n操作「{query}」走哪个动作?只输出动作名(从 {TASK_ACTIONS})。"
122
+ return p, query
123
+
124
+
125
+ def task_acc(ans, query):
126
+ gold = dict(TASK_GOLD)[query]
127
+ return 1.0 if gold in ans.lower() else 0.0
128
+
129
+
130
+ def run():
131
+ cells = {}
132
+ # search (3 主题)
133
+ for cls, fn, accfn, items in [
134
+ ("search", search_task, search_acc, ["考勤", "保险", "车辆"]),
135
+ ("prd", prd_task, prd_acc, [("车辆保养提醒", "车辆"), ("考勤异常导出", "考勤"), ("保险续保预警", "保险")]),
136
+ ("code", code_task, code_acc, [("车辆保养", "车辆"), ("考勤导出", "考勤"), ("保险预警", "保险")]),
137
+ ]:
138
+ for mode in ("bare", "scaffold"):
139
+ accs, lats, toks = [], [], []
140
+ for it in items:
141
+ p, aux = fn(it, mode)
142
+ mt = 900 if cls == "prd" else 500 # prd 6 章需更大 max_tokens(500 截断致缺章)
143
+ ans, tok, lat = chat(p, max_tokens=mt)
144
+ accs.append(accfn(ans, aux)); lats.append(lat); toks.append(tok)
145
+ n = len(items)
146
+ cells[f"{cls}/{mode}"] = {"acc": round(sum(accs)/n, 3), "p50_lat": sorted(lats)[n//2], "tok": round(sum(toks)/n)}
147
+ print(" %s/%s: acc=%.2f p50=%dms tok=%d" % (cls, mode, sum(accs)/n, sorted(lats)[n//2], sum(toks)/n))
148
+ # task (6 短语)
149
+ for mode in ("bare", "scaffold"):
150
+ accs, lats, toks = [], [], []
151
+ for q, _ in TASK_GOLD:
152
+ p, aux = task_task(q, mode)
153
+ ans, tok, lat = chat(p, max_tokens=20)
154
+ accs.append(task_acc(ans, aux)); lats.append(lat); toks.append(tok)
155
+ n = len(TASK_GOLD)
156
+ cells[f"task/{mode}"] = {"acc": round(sum(accs)/n, 3), "p50_lat": sorted(lats)[n//2], "tok": round(sum(toks)/n)}
157
+ print(" task/%s: acc=%.2f p50=%dms tok=%d" % (mode, sum(accs)/n, sorted(lats)[n//2], sum(toks)/n))
158
+ return {"model": MODEL, "cells": cells}
159
+
160
+
161
+ def main():
162
+ r = run()
163
+ c = r["cells"]
164
+ print("\n## FW6-扩展 4 命令类 准×快×token 对齐(weak 档,bare vs +工作流)")
165
+ print("| 命令类 | 模式 | 准确性 | 速P50 | tok |")
166
+ print("|---|---|---|---|---|")
167
+ for cls in ("search", "prd", "code", "task"):
168
+ for mode in ("bare", "scaffold"):
169
+ x = c[f"{cls}/{mode}"]
170
+ print("| %s | %s | %.2f | %dms | %d |" % (cls, mode, x["acc"], x["p50_lat"], x["tok"]))
171
+ print("\n结论:每类 bare→scaffold 准确性 0→达标,scaffold 仍在 weak 档(最快档);"
172
+ "弱模型+工作流在【每个命令类】都又快又准。")
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()