Files
kwcode/kaiwu/validation/v6_expert_generation.py
Val-sss 5668419bdf Kaiwu v0.4: expert registry, flywheel, 3-layer memory, MCP router
Major additions:
- Expert registry with 12 builtin experts (keyword matching, lifecycle)
- 3-layer memory system (PROJECT.md / EXPERT.md / PATTERN.md)
- Expert flywheel (trajectory → pattern → generate → 3-gate → lifecycle)
- Expert packaging (.kwx import/export)
- KaiwuMCP Router (single kaiwu_execute tool)
- CLI subcommands: expert list/info/export/install/remove/create, status, serve-mcp
- V5/V6 validation script frameworks
- Install scripts (install.ps1 + install.sh) with Chinese mirror support
- README_zh.md Chinese documentation

Verified: 24/24 tests pass, E2E fibonacci bug fix (gemma3:4b, 22.4s),
10/10 CORE red line constraints satisfied, Windows cmd native OK.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:18:04 +08:00

363 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
V6 验证LLM 从轨迹生成专家 YAML 的质量测试。
目标:验证 ExpertGeneratorFlywheel 能从相似任务轨迹中提取有效专家定义。
验收标准:
- 生成的专家 YAML 结构完整name, trigger_keywords, system_prompt, pipeline
- trigger_keywords 能覆盖原始任务描述中的关键词
- pipeline 步骤合法locator/generator/verifier
- system_prompt 非空且包含领域知识
注意:需要 Ollama 在线才能运行 LLM 生成。
用法:
python -m kaiwu.validation.v6_expert_generation --ollama-model gemma3:4b
"""
import argparse
import io
import json
import os
import sys
import time
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
from kaiwu.flywheel.trajectory_collector import TaskTrajectory
# ── 3 组 × 5 条合成轨迹 ──────────────────────────────────────
TRAJECTORY_GROUPS = {
"fastapi_crud": {
"label": "FastAPI CRUD endpoint",
"expected_keywords": ["fastapi", "crud", "endpoint", "api", "pydantic"],
"trajectories": [
TaskTrajectory(
task_id="fa-001", user_input="帮我写一个FastAPI的用户CRUD接口",
gate_result={"expert_type": "codegen", "task_summary": "FastAPI user CRUD"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["src/api/users.py", "src/models/user.py"],
success=True, latency_s=12.3, model_used="gemma3:4b",
timestamp="2026-04-26T10:00:00Z", search_triggered=False, project_hash="abc123",
),
TaskTrajectory(
task_id="fa-002", user_input="用FastAPI实现商品列表的增删改查",
gate_result={"expert_type": "codegen", "task_summary": "FastAPI product CRUD"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["src/api/products.py", "src/schemas/product.py"],
success=True, latency_s=14.1, model_used="gemma3:4b",
timestamp="2026-04-26T10:05:00Z", search_triggered=False, project_hash="abc123",
),
TaskTrajectory(
task_id="fa-003", user_input="FastAPI订单接口包含创建、查询、更新状态",
gate_result={"expert_type": "codegen", "task_summary": "FastAPI order endpoints"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["src/api/orders.py"],
success=True, latency_s=11.8, model_used="gemma3:4b",
timestamp="2026-04-26T10:10:00Z", search_triggered=False, project_hash="abc123",
),
TaskTrajectory(
task_id="fa-004", user_input="写一个FastAPI的分类管理接口支持树形结构",
gate_result={"expert_type": "codegen", "task_summary": "FastAPI category tree CRUD"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["src/api/categories.py", "src/models/category.py"],
success=True, latency_s=15.2, model_used="gemma3:4b",
timestamp="2026-04-26T10:15:00Z", search_triggered=False, project_hash="abc123",
),
TaskTrajectory(
task_id="fa-005", user_input="FastAPI实现标签的CRUD带分页和搜索",
gate_result={"expert_type": "codegen", "task_summary": "FastAPI tag CRUD with pagination"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["src/api/tags.py"],
success=True, latency_s=13.0, model_used="gemma3:4b",
timestamp="2026-04-26T10:20:00Z", search_triggered=False, project_hash="abc123",
),
],
},
"pytest_fixture": {
"label": "pytest fixture",
"expected_keywords": ["pytest", "fixture", "test", "mock", "conftest"],
"trajectories": [
TaskTrajectory(
task_id="pt-001", user_input="帮我写一个pytest的数据库fixture每个测试自动回滚",
gate_result={"expert_type": "codegen", "task_summary": "pytest db fixture with rollback"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["tests/conftest.py"],
success=True, latency_s=8.5, model_used="gemma3:4b",
timestamp="2026-04-26T11:00:00Z", search_triggered=False, project_hash="def456",
),
TaskTrajectory(
task_id="pt-002", user_input="写pytest fixture mock掉外部HTTP请求",
gate_result={"expert_type": "codegen", "task_summary": "pytest fixture mock HTTP"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["tests/conftest.py", "tests/fixtures/http_mock.py"],
success=True, latency_s=9.2, model_used="gemma3:4b",
timestamp="2026-04-26T11:05:00Z", search_triggered=False, project_hash="def456",
),
TaskTrajectory(
task_id="pt-003", user_input="pytest conftest里加一个Redis fixture用fakeredis",
gate_result={"expert_type": "codegen", "task_summary": "pytest Redis fixture"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["tests/conftest.py"],
success=True, latency_s=7.8, model_used="gemma3:4b",
timestamp="2026-04-26T11:10:00Z", search_triggered=False, project_hash="def456",
),
TaskTrajectory(
task_id="pt-004", user_input="写一个pytest fixture提供临时文件目录测试后自动清理",
gate_result={"expert_type": "codegen", "task_summary": "pytest tmpdir fixture"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["tests/conftest.py"],
success=True, latency_s=6.9, model_used="gemma3:4b",
timestamp="2026-04-26T11:15:00Z", search_triggered=False, project_hash="def456",
),
TaskTrajectory(
task_id="pt-005", user_input="pytest fixture实现用户认证tokenparametrize不同角色",
gate_result={"expert_type": "codegen", "task_summary": "pytest auth fixture parametrize"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["tests/conftest.py", "tests/fixtures/auth.py"],
success=True, latency_s=10.1, model_used="gemma3:4b",
timestamp="2026-04-26T11:20:00Z", search_triggered=False, project_hash="def456",
),
],
},
"db_migration": {
"label": "database migration",
"expected_keywords": ["migration", "alembic", "database", "schema", "migrate"],
"trajectories": [
TaskTrajectory(
task_id="db-001", user_input="帮我写一个Alembic迁移脚本给users表加email字段",
gate_result={"expert_type": "codegen", "task_summary": "Alembic migration add email column"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["alembic/versions/001_add_email.py"],
success=True, latency_s=9.0, model_used="gemma3:4b",
timestamp="2026-04-26T12:00:00Z", search_triggered=False, project_hash="ghi789",
),
TaskTrajectory(
task_id="db-002", user_input="写数据库迁移创建orders表包含外键关联users",
gate_result={"expert_type": "codegen", "task_summary": "migration create orders table"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["alembic/versions/002_create_orders.py", "src/models/order.py"],
success=True, latency_s=11.5, model_used="gemma3:4b",
timestamp="2026-04-26T12:05:00Z", search_triggered=False, project_hash="ghi789",
),
TaskTrajectory(
task_id="db-003", user_input="Alembic迁移给products表加索引和唯一约束",
gate_result={"expert_type": "codegen", "task_summary": "Alembic add index and unique constraint"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["alembic/versions/003_add_product_index.py"],
success=True, latency_s=8.3, model_used="gemma3:4b",
timestamp="2026-04-26T12:10:00Z", search_triggered=False, project_hash="ghi789",
),
TaskTrajectory(
task_id="db-004", user_input="写迁移脚本把status字段从字符串改成枚举类型",
gate_result={"expert_type": "codegen", "task_summary": "migration change column type to enum"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["alembic/versions/004_status_enum.py"],
success=True, latency_s=10.7, model_used="gemma3:4b",
timestamp="2026-04-26T12:15:00Z", search_triggered=False, project_hash="ghi789",
),
TaskTrajectory(
task_id="db-005", user_input="数据库迁移拆分address字段为province/city/district",
gate_result={"expert_type": "codegen", "task_summary": "migration split address column"},
expert_used="codegen", pipeline_steps=["generator", "verifier"],
files_modified=["alembic/versions/005_split_address.py", "src/models/user.py"],
success=True, latency_s=12.0, model_used="gemma3:4b",
timestamp="2026-04-26T12:20:00Z", search_triggered=False, project_hash="ghi789",
),
],
},
}
def _check_keyword_coverage(expert: dict, group_info: dict) -> dict:
"""Check if generated expert's keywords cover the expected domain."""
generated_kws = [kw.lower() for kw in expert.get("trigger_keywords", [])]
expected_kws = [kw.lower() for kw in group_info["expected_keywords"]]
# Check: any expected keyword appears in generated keywords?
hits = [ek for ek in expected_kws if any(ek in gk or gk in ek for gk in generated_kws)]
# Check: do generated keywords match original task descriptions?
all_inputs = " ".join(t.user_input.lower() for t in group_info["trajectories"])
input_hits = [gk for gk in generated_kws if gk in all_inputs]
return {
"expected_keyword_coverage": len(hits) / len(expected_kws) if expected_kws else 0,
"expected_hits": hits,
"input_keyword_hits": input_hits,
"generated_keywords": generated_kws,
}
def _validate_expert_structure(expert: dict) -> dict:
"""Validate the generated expert has all required fields and valid values."""
checks = {}
# Required fields
required = ["name", "trigger_keywords", "system_prompt", "pipeline"]
for field in required:
checks[f"has_{field}"] = field in expert and bool(expert[field])
# Pipeline validity
valid_steps = {"locator", "generator", "verifier"}
pipeline = expert.get("pipeline", [])
checks["pipeline_valid"] = all(s in valid_steps for s in pipeline) and len(pipeline) > 0
# System prompt quality
sp = expert.get("system_prompt", "")
checks["system_prompt_nonempty"] = len(sp) > 20
checks["system_prompt_has_content"] = any(
kw in sp.lower() for kw in ["专家", "expert", "你是", "you are", "专注", "focus"]
)
# Trigger confidence
conf = expert.get("trigger_min_confidence", 0)
checks["confidence_reasonable"] = 0.5 <= conf <= 1.0
checks["all_passed"] = all(checks.values())
return checks
def run_validation(ollama_model: str = "gemma3:4b"):
from kaiwu.llm.llama_backend import LLMBackend
from kaiwu.flywheel.expert_generator import ExpertGeneratorFlywheel
print("=" * 60)
print("V6 验证LLM 专家生成质量")
print("=" * 60)
print(f"模型: {ollama_model}")
print(f"测试组: {len(TRAJECTORY_GROUPS)} 组 x 5 条轨迹")
print()
# Check Ollama availability
ollama_available = True
try:
import httpx
resp = httpx.get("http://localhost:11434/api/tags", timeout=3.0)
if resp.status_code != 200:
ollama_available = False
except Exception:
ollama_available = False
if not ollama_available:
print(" Ollama 不在线,无法运行 LLM 生成。")
print(" 请启动 Ollama 后重试: ollama serve")
conclusion = {
"status": "skipped",
"reason": "Ollama offline",
"groups": {},
}
_save_conclusion(conclusion)
return conclusion
llm = LLMBackend(ollama_model=ollama_model)
generator = ExpertGeneratorFlywheel(llm)
group_results = {}
total_passed = 0
total_groups = len(TRAJECTORY_GROUPS)
for group_key, group_info in TRAJECTORY_GROUPS.items():
print(f"── 组: {group_info['label']} ──")
# Build pattern dict matching PatternDetector output format
pattern = {
"expert_type": "codegen",
"count": len(group_info["trajectories"]),
"trajectories": group_info["trajectories"],
"pipeline": group_info["trajectories"][0].pipeline_steps,
}
t0 = time.time()
expert = generator.generate(pattern)
elapsed = time.time() - t0
if expert is None:
print(f" 生成失败 (LLM返回无法解析的结果) [{elapsed:.1f}s]")
group_results[group_key] = {
"label": group_info["label"],
"generated": False,
"elapsed_s": round(elapsed, 1),
"error": "generation returned None",
}
continue
print(f" 生成成功: {expert.get('name', '?')} [{elapsed:.1f}s]")
# Validate structure
structure_checks = _validate_expert_structure(expert)
print(f" 结构验证: {'PASS' if structure_checks['all_passed'] else 'FAIL'}")
for check_name, passed in structure_checks.items():
if check_name == "all_passed":
continue
status = "OK" if passed else "FAIL"
print(f" {check_name}: {status}")
# Check keyword coverage
kw_coverage = _check_keyword_coverage(expert, group_info)
coverage_pct = kw_coverage["expected_keyword_coverage"] * 100
print(f" 关键词覆盖: {coverage_pct:.0f}% ({kw_coverage['expected_hits']})")
print(f" 生成的关键词: {kw_coverage['generated_keywords']}")
print(f" 输入命中: {kw_coverage['input_keyword_hits']}")
group_passed = structure_checks["all_passed"] and coverage_pct >= 40
if group_passed:
total_passed += 1
print(f" 组判定: {'PASS' if group_passed else 'FAIL'}")
print()
group_results[group_key] = {
"label": group_info["label"],
"generated": True,
"expert_name": expert.get("name"),
"expert_keywords": expert.get("trigger_keywords", []),
"expert_pipeline": expert.get("pipeline", []),
"system_prompt_len": len(expert.get("system_prompt", "")),
"structure_checks": structure_checks,
"keyword_coverage": kw_coverage["expected_keyword_coverage"],
"elapsed_s": round(elapsed, 1),
"passed": group_passed,
}
# ── Summary ──
print("=" * 60)
print("验证结论")
print("=" * 60)
print(f" 通过组数: {total_passed}/{total_groups}")
overall = total_passed >= 2 # At least 2/3 groups pass
print(f" 总体判定: {'PASS' if overall else 'FAIL'} (需 >= 2/3 组通过)")
if not overall:
print(" 建议: 换更大模型重试,或优化 ExpertGeneratorFlywheel 的 prompt")
V6_CONCLUSION = {
"status": "completed",
"model": ollama_model,
"groups_passed": total_passed,
"groups_total": total_groups,
"overall_pass": overall,
"groups": group_results,
}
_save_conclusion(V6_CONCLUSION)
return V6_CONCLUSION
def _save_conclusion(conclusion: dict):
conclusion_path = os.path.join(os.path.dirname(__file__), "v6_conclusion.json")
with open(conclusion_path, "w", encoding="utf-8") as f:
json.dump(conclusion, f, indent=2, ensure_ascii=False)
print(f"\n 结论已保存到: {conclusion_path}")
def main():
parser = argparse.ArgumentParser(description="V6 专家生成质量验证")
parser.add_argument("--ollama-model", type=str, default="gemma3:4b")
args = parser.parse_args()
run_validation(ollama_model=args.ollama_model)
if __name__ == "__main__":
main()