feat(xingbang): 兴邦金租 AI 评审平台模块(构建于 mini-OpenClaw)
新增兴邦金租评审项目模块, 作为 mini-OpenClaw 之上的完整交付: - 五智能体流水线(data_curator/risk_scout/case_finder/approval_advisor/risk_monitor) - evolution 自进化子系统(轨迹采集+奖励计算+模式归纳prompt增强) - /api/xingbang/* 端点(config/workflow/stream/evolution/trajectories) - 运行时产物(客户产出情报卡 + 进化轨迹数据)已 gitignore, 不含隐私 遵循兴邦三档分级审批制度, 数据源指向兴邦实验数据包
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
"""自进化(evolution)模块 — 兴邦金租评审平台的学习与优化子系统。
|
||||
|
||||
核心思想: 每次审批流水线执行, 采集五智能体的完整轨迹
|
||||
(state/action/observation/reward), 从历史轨迹中归纳成功模式,
|
||||
反哺各智能体的 system prompt, 实现"越用越准"的自进化。
|
||||
|
||||
分级方案(诚实设计):
|
||||
- Level 1 轻量(现在能做): 轨迹采集 + 内在奖励 + 失败/成功模式归纳 + prompt 优化
|
||||
- Level 2: 用户显式反馈(👍/👎)作为奖励信号(需前端配合)
|
||||
- Level 3(未来): 专家标注 / LoRA 微调(需 GPU)
|
||||
本模块实现 Level 1, 接口预留 Level 2/3。
|
||||
"""
|
||||
|
||||
from .trajectory import TrajectoryRecorder
|
||||
from .reward import compute_reward_v1
|
||||
from .optimizer import EvolutionEngine
|
||||
|
||||
__all__ = ["TrajectoryRecorder", "compute_reward_v1", "EvolutionEngine"]
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Evolution Engine — 从历史轨迹中学习, 反哺各智能体 prompt, 实现自进化。
|
||||
|
||||
闭环(自进化):
|
||||
轨迹采集 → 奖励打分 → 归纳成功/失败模式 → 生成进化建议 →
|
||||
反哺/追加到各 agent 的系统提示词(可选启用)
|
||||
|
||||
Level 1 实现:
|
||||
- analyze: 从已保存轨迹中总结高分/低分模式(常见工具序列、步骤分布)
|
||||
- generate_insights: 生成可读的进化洞察(markdown)
|
||||
- evolve_prompt: 将洞察追加到对应 agent 的 system prompt(可选)
|
||||
|
||||
轻量自进化的核心: 不编造"强化学习训练", 而是做"基于经验的 prompt 增强"
|
||||
——把历史中高奖励轨迹用过的工具调用序列、成功策略, 有选择地注入
|
||||
未来 agent 的提示词, 让系统越用越聪明。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .reward import compute_reward_v1
|
||||
from .trajectory import TrajectoryRecorder
|
||||
|
||||
|
||||
class EvolutionEngine:
|
||||
"""自进化引擎(Level 1 轻量版)。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_dir: Path,
|
||||
recorder: TrajectoryRecorder,
|
||||
enabled: bool = True,
|
||||
):
|
||||
self._base_dir = Path(base_dir)
|
||||
self._recorder = recorder
|
||||
self.enabled = enabled
|
||||
self._insights_dir = self._base_dir / "xingbang_evolution" / "insights"
|
||||
self._insights_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ── 分析历史轨迹 ────────────────────────────────────
|
||||
def _load_all_trajectories(self) -> list[dict[str, Any]]:
|
||||
trajs = []
|
||||
for p in self._recorder.list_trajectories():
|
||||
try:
|
||||
trajs.append(json.loads(p.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
continue
|
||||
return trajs
|
||||
|
||||
def analyze(self) -> dict[str, Any]:
|
||||
"""分析全部历史轨迹, 返回统计与模式。"""
|
||||
trajectories = self._load_all_trajectories()
|
||||
if not trajectories:
|
||||
return {"count": 0, "message": "尚无轨迹样本,先积累数据"}
|
||||
|
||||
# 计算每条奖励
|
||||
scored = []
|
||||
for t in trajectories:
|
||||
r = compute_reward_v1(t)
|
||||
t["_reward"] = r
|
||||
scored.append(t)
|
||||
|
||||
scored.sort(key=lambda x: x["_reward"], reverse=True)
|
||||
high = scored[: max(1, len(scored) // 4)] # 前 25%
|
||||
low = scored[-max(1, len(scored) // 4):] # 后 25%
|
||||
|
||||
# 归纳高频工具序列
|
||||
def agent_tool_sequences(trajs):
|
||||
counter = Counter()
|
||||
for t in trajs:
|
||||
for s in t.get("steps", []):
|
||||
seq = tuple(s.get("tools", []))
|
||||
if seq:
|
||||
counter[seq] += 1
|
||||
return counter.most_common(5)
|
||||
|
||||
high_patterns = {
|
||||
"seqs": agent_tool_sequences(high),
|
||||
"avg_steps": sum(len(t["steps"]) for t in high) / max(1, len(high)),
|
||||
"success_rate": sum(1 for t in high if t["final_result"].get("success"))
|
||||
/ max(1, len(high)),
|
||||
}
|
||||
return {
|
||||
"count": len(trajectories),
|
||||
"avg_reward": round(sum(t["_reward"] for t in scored) / len(scored), 2),
|
||||
"high": {"samples": len(high), "patterns": high_patterns},
|
||||
"low": {
|
||||
"samples": len(low),
|
||||
"avg_steps": sum(len(t["steps"]) for t in low) / max(1, len(low)),
|
||||
},
|
||||
"top_agents_used": self._agent_usage(scored),
|
||||
}
|
||||
|
||||
def _agent_usage(self, trajs) -> dict:
|
||||
counter = Counter()
|
||||
for t in trajs:
|
||||
for s in t.get("steps", []):
|
||||
counter[s.get("agent", "?")] += 1
|
||||
return dict(counter.most_common())
|
||||
|
||||
# ── 生成进化洞察 ────────────────────────────────────
|
||||
def generate_insights(self) -> str:
|
||||
"""生成可读的 markdown 进化洞察报告。"""
|
||||
analysis = self.analyze()
|
||||
if analysis.get("count", 0) == 0:
|
||||
return "# 自进化洞察\n\n尚无轨迹样本。运行审批流水线积累数据后,系统将自动进化。"
|
||||
lines = [
|
||||
f"# 兴邦 AI 评审·自进化洞察",
|
||||
f"\n**样本数**: {analysis['count']} 条轨迹 | **平均奖励**: {analysis['avg_reward']}",
|
||||
"",
|
||||
"## ✅ 高分轨迹模式 (前 25%)",
|
||||
]
|
||||
hp = analysis["high"]
|
||||
lines.append(f"- 平均步骤数: {hp['patterns']['avg_steps']:.1f}")
|
||||
lines.append(f"- 成功率: {hp['patterns']['success_rate']*100:.0f}%")
|
||||
lines.append("- 高频工具序列:")
|
||||
for seq, n in hp["patterns"]["seqs"]:
|
||||
lines.append(f" - {' → '.join(seq)} (出现 {n} 次)")
|
||||
lines += ["", "## ⚠️ 低分轨迹 (后 25%)", f"- 平均步骤数: {analysis['low']['avg_steps']:.1f}"]
|
||||
lines += ["", "## 🧠 智能体使用分布", ""]
|
||||
for agent, count in analysis.get("top_agents_used", {}).items():
|
||||
lines.append(f"- {agent}: {count} 次")
|
||||
lines += [
|
||||
"",
|
||||
"## 进化建议",
|
||||
"1. 高分轨迹的工具序列可在各 agent 的 system prompt 中作为'推荐工作流'注入",
|
||||
"2. 步骤数偏多(>5)的 agent 提示词应强调'一次到位,减少反复读取'",
|
||||
"3. 持续积累轨迹 → 达到一定量后启动更高阶优化(LoRA 微调)",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def save_insights(self) -> Path:
|
||||
"""保存洞察报告到 insights 目录。"""
|
||||
report = self.generate_insights()
|
||||
path = self._insights_dir / "evolution_insights.md"
|
||||
path.write_text(report, encoding="utf-8")
|
||||
return path
|
||||
|
||||
# ── 反哺 prompt (自进化闭环) ────────────────────────
|
||||
def evolve_prompt_suggestion(self, agent_key: str) -> str | None:
|
||||
"""为指定 agent 生成可注入的 prompt 增强建议(基于历史高分轨迹)。
|
||||
|
||||
返回一段可追加到该 agent system_prompt 的文本, 若无历史则 None。
|
||||
"""
|
||||
analysis = self.analyze()
|
||||
if analysis.get("count", 0) == 0:
|
||||
return None
|
||||
hp = analysis["high"]
|
||||
seqs = hp["patterns"]["seqs"]
|
||||
if not seqs:
|
||||
return None
|
||||
top_seq_text = " → ".join(seqs[0][0]) if seqs else ""
|
||||
suggestion = (
|
||||
f"\n\n[自进化推荐 · 基于 {analysis['count']} 条历史轨迹] "
|
||||
f"高分任务常用工具序列: {top_seq_text}。"
|
||||
f"请优先复用该模式,减少无效工具往返,一次到位完成任务。"
|
||||
)
|
||||
return suggestion
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Reward Calculator — 计算一条审批轨迹的奖励分数。
|
||||
|
||||
诚实分级设计 (对齐兴邦方案):
|
||||
Level 1 (现在能做,不需要用户交互):
|
||||
- 任务完成 +10
|
||||
- 工具调用成功率
|
||||
- 重复调用惩罚
|
||||
- 时间惩罚
|
||||
Level 2 (需前端加 👍/👎 按钮):
|
||||
- 用户显式反馈
|
||||
Level 3 (需人工标注):
|
||||
- 专家打分
|
||||
|
||||
本文件实现 Level 1 + 预留 Level 2/3 参数。
|
||||
不做不诚实的信号: 拒绝用"还款表现"当奖励(周期1-3年无法闭环)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
|
||||
def compute_reward_v1(
|
||||
trajectory: dict[str, Any],
|
||||
feedback: str | None = None,
|
||||
expert_score: float | None = None,
|
||||
) -> float:
|
||||
"""计算一条轨迹的奖励(Leve1 l 内置指标 + 可选 Level2 反馈 + Level3 专家分)。
|
||||
|
||||
Args:
|
||||
trajectory: 轨迹 dict(含 steps 和 final_result)
|
||||
feedback: Level2, "positive"/"negative"
|
||||
expert_score: Level3, 1-5 分
|
||||
Returns:
|
||||
float 奖励分
|
||||
"""
|
||||
steps = trajectory.get("steps", [])
|
||||
final = trajectory.get("final_result", {})
|
||||
if not steps:
|
||||
return 0.0
|
||||
|
||||
reward = 0.0
|
||||
|
||||
# 1. 任务完成奖励
|
||||
if final.get("success"):
|
||||
reward += 10.0
|
||||
else:
|
||||
reward -= 10.0
|
||||
|
||||
# 2. 工具调用成功率(成功工具 / 总工具)
|
||||
total_tool_calls = 0
|
||||
tool_success = 0
|
||||
for s in steps:
|
||||
tools = s.get("tools", [])
|
||||
total_tool_calls += len(tools)
|
||||
if tools:
|
||||
tool_success += 1 # 该步骤至少调了工具视为尽力(简化)
|
||||
if total_tool_calls > 0:
|
||||
reward += (tool_success / max(1, total_tool_calls)) * 5.0
|
||||
|
||||
# 3. 步骤数惩罚(越少越好,常规 5 步流水线 5-15 步为佳)
|
||||
n_steps = len(steps)
|
||||
optimal = 5
|
||||
step_penalty = abs(n_steps - optimal) * 0.3
|
||||
reward -= step_penalty
|
||||
|
||||
# 4. 时间惩罚(超过 600 秒 = 10 分钟开始扣)
|
||||
duration_ms = final.get("duration_ms", 0)
|
||||
if duration_ms > 600_000:
|
||||
reward -= ((duration_ms - 600_000) / 60_000) * 1.0
|
||||
|
||||
# ── Level 2: 用户反馈 (可选) ──
|
||||
if feedback == "positive":
|
||||
reward += 20.0
|
||||
elif feedback == "negative":
|
||||
reward -= 10.0
|
||||
|
||||
# ── Level 3: 专家打分 (可选, 1-5 归一化到 -10~+10) ──
|
||||
if expert_score is not None:
|
||||
reward += (expert_score - 3) * 5.0
|
||||
|
||||
reward = max(-30.0, min(reward, 50.0)) # 裁剪
|
||||
return round(reward, 2)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Trajectory Recorder — 采集五智能体每次调用的完整轨迹。
|
||||
|
||||
轨迹格式 (trajectory):
|
||||
{
|
||||
"trajectory_id": "traj_<timestamp>",
|
||||
"customer": "客户名",
|
||||
"workflow_id": "...",
|
||||
"steps": [
|
||||
{
|
||||
"agent": "data_curator",
|
||||
"input": "任务描述",
|
||||
"prompt_snippet": "...",
|
||||
"tools": ["read_xingbang_data", ...],
|
||||
"output_file": "...",
|
||||
"output_preview": "...",
|
||||
"success": true/false,
|
||||
"time_ms": 1234
|
||||
}, ...
|
||||
],
|
||||
"final_result": {...},
|
||||
"cumulative_reward": 0.0
|
||||
}
|
||||
|
||||
轨迹保存在 <base>/xingbang_evolution/trajectories/*.json。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TrajectoryRecorder:
|
||||
"""记录并持久化一次评审流水线的完整执行轨迹。"""
|
||||
|
||||
def __init__(self, base_dir: Path, enabled: bool = True):
|
||||
self._base_dir = Path(base_dir)
|
||||
self._dir = self._base_dir / "xingbang_evolution" / "trajectories"
|
||||
self._dir.mkdir(parents=True, exist_ok=True)
|
||||
self.enabled = enabled
|
||||
# 当前正在记录的轨迹
|
||||
self._current: dict[str, Any] | None = None
|
||||
# metadata (customer 等)
|
||||
self._meta: dict = {}
|
||||
self._start_time: float = 0.0
|
||||
|
||||
def begin(self, workflow_id: str | None = None, customer: str = "") -> str:
|
||||
"""开始记录一条新轨迹,返回 trajectory_id。"""
|
||||
self._start_time = time.time()
|
||||
self._current = {
|
||||
"trajectory_id": f"traj_{int(time.time())}_{uuid.uuid4().hex[:6]}",
|
||||
"workflow_id": workflow_id or f"wf_{int(time.time())}",
|
||||
"customer": customer or "",
|
||||
"created_at": self._start_time,
|
||||
"steps": [],
|
||||
"final_result": {},
|
||||
"cumulative_reward": 0.0,
|
||||
}
|
||||
return self._current["trajectory_id"]
|
||||
|
||||
def add_step(self, step: dict[str, Any]) -> None:
|
||||
"""追加一步智能体执行记录。"""
|
||||
if self._current is not None:
|
||||
self._current["steps"].append(step)
|
||||
|
||||
def finalize(self, success: bool = True, summary: str = "") -> dict[str, Any]:
|
||||
"""结束记录,保存轨迹到磁盘,返回轨迹。"""
|
||||
if self._current is None:
|
||||
return {}
|
||||
self._current["final_result"] = {
|
||||
"success": success,
|
||||
"summary": summary,
|
||||
"duration_ms": int((time.time() - self._start_time) * 1000),
|
||||
"total_steps": len(self._current["steps"]),
|
||||
}
|
||||
# 记录成功/失败
|
||||
trajectory = self._current
|
||||
self._save(trajectory)
|
||||
self._current = None
|
||||
return trajectory
|
||||
|
||||
def _save(self, trajectory: dict[str, Any]) -> Path:
|
||||
path = self._dir / f"{trajectory['trajectory_id']}.json"
|
||||
path.write_text(
|
||||
json.dumps(trajectory, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return path
|
||||
|
||||
# ── 从调用侧采集 ────────────────────────────────────
|
||||
def on_evidence(self, kind: str, data: dict) -> None:
|
||||
"""订阅 agents 的 evidence 回调,累积成本步记录。"""
|
||||
if not self.enabled or self._current is None:
|
||||
return
|
||||
if kind == "agent_start":
|
||||
buffered = []
|
||||
self._current["_buffered"] = buffered
|
||||
# 这一步先不展开, 由 workflow 显式调用 add_step 更为清晰
|
||||
|
||||
def list_trajectories(self) -> list[Path]:
|
||||
return sorted(self._dir.glob("*.json"))
|
||||
|
||||
def load(self, trajectory_id: str) -> dict[str, Any]:
|
||||
path = self._dir / f"{trajectory_id}.json"
|
||||
if path.exists():
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
Reference in New Issue
Block a user