Files
mini-openclaw/backend/xingbang/agents.py
T
NathanAlex7 28b505c9e6 feat(xingbang): agent 内置网关过载(503)指数退避重试
公司 DeepSeek 网关短时 CPU 过载返回 503, 五个智能体连续失败。
为每个 agent 增加 4 次指数退避重试(5s/10s/20s/40s), 提升流水线对限流波动稳健性。
2026-09-01 17:05:07 +08:00

182 lines
7.2 KiB
Python
Raw 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.
"""五智能体定义 — 复用 mini-OpenClaw 的 LLM 与工具,各自带差异化 system prompt。
每个智能体 = 一个独立的 LangChain create_agent:
- 共享底层 LLM(DeepSeek) 与读文件工具
- 差异化的 system prompt(角色、规则、输出约定)
- 专用工具: read_xingbang_data(读兴邦数据包)、read_output(读上游产出)
- 属于同一个全局 AgentManager 编排, 但各 agent 独立 invoke
轨迹采集: 每个 agent 的每次调用(输入/system/prompt/工具序列/输出)都会被
evidence 回调记录, 供 evolution 模块使用。
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Callable
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.tools import tool
from config import get_rag_mode
from graph.prompt_builder import build_system_prompt
from xingbang.config import agent_specs, XINGBANG_DATA_DIR, DATA_FILES
# ── 专用工具:读取兴邦多源数据包 ────────────────────────
@tool
def read_xingbang_data(filename: str) -> str:
"""读取兴邦金租客户的多源数据文件(JSON/CSV/MD/TXT),返回内容。
可用文件名(需带编号前缀): 01_客户申请信息.json, 02_信用评分卡.json,
03_风险评级报告.md, 04_授信额度测算.csv, 05_征信记录.json,
06_司法执行记录.json, 07_舆情风险评分.json, 08_反欺诈模型输出.json,
09_财务预警模型.csv, 10_财务分析报告.csv, 11_租赁物估值报告.md,
12_行业调研报告.md, 13_历史审批案例库.json, 14_公司制度要点.md,
15_银行流水扫描件.txt
"""
if not XINGBANG_DATA_DIR.exists():
return f"[错误] 兴邦数据目录不存在: {XINGBANG_DATA_DIR}"
path = XINGBANG_DATA_DIR / filename
if not path.exists():
return f"[错误] 文件不存在: {filename}"
content = path.read_text(encoding="utf-8", errors="ignore")
# 限制长度避免超上下文
if len(content) > 4000:
content = content[:4000] + "\n...[已截断]"
return content
@tool
def read_output(filename: str, base_dir: str = "") -> str:
"""读取当前兴邦工作区中上游智能体产出的 Markdown 文件(如
customer_intel.md, due_diligence.md, case_reference.md)。"""
file_path = Path(base_dir) / filename if base_dir else Path(filename)
if not file_path.exists():
return f"[错误] 上游产出文件不存在: {filename}"
content = file_path.read_text(encoding="utf-8", errors="ignore")
if len(content) > 8000:
content = content[:8000] + "\n...[已截断]"
return content
# ── 单个评审智能体 ─────────────────────────────────────
class XingbangAgent:
"""一个兴邦评审智能体: 差异化角色 + 复用 LLM + 工具。"""
def __init__(
self,
spec,
llm=None,
base_dir: Path | None = None,
output_dir: Path | None = None,
on_evidence: Callable[[str, dict], None] | None = None,
):
self.spec = spec
self._llm = llm
self._base_dir = base_dir
self._output_dir = output_dir or (base_dir / "xingbang_output" if base_dir else None)
self._on_evidence = on_evidence
self._agent = None
def _ensure_agent(self):
"""构建带专属 system prompt 的独立 agent(惰性)。"""
if self._agent is not None:
return self._agent
tools = [read_xingbang_data]
tools.append(read_output)
prompt = self.spec.system_prompt
# 追加读文件工具说明(需要 base_dir 才能读产出)
if self._base_dir:
prompt += (
f"\n\n[工作目录] 上游产出文件位于 {self._base_dir}。"
"读取上游产出时使用 read_output 工具并把 filename 设为产出文件名。"
)
self._agent = create_agent(model=self._llm, tools=tools, system_prompt=prompt)
return self._agent
def _emit(self, kind: str, data: dict):
if self._on_evidence:
self._on_evidence(kind, data)
async def run(self, task: str, context: str = "") -> dict:
"""执行该智能体任务,返回 {'output': 结果文本, 'file': 保存路径}。"""
self._emit("agent_start", {"agent": self.spec.key, "name": self.spec.name})
agent = self._ensure_agent()
messages = []
if context:
messages.append(HumanMessage(content=f"[上游上下文]\n{context}"))
messages.append(HumanMessage(content=task))
self._emit("prompt", {
"agent": self.spec.key,
"system": self.spec.system_prompt[:500],
"task": task[:300],
})
import asyncio
max_retries = 4
backoff = 5 # 初始等待秒
last_err = None
for attempt in range(max_retries):
try:
result = await agent.ainvoke({"messages": messages})
final = self._extract_text(result)
break
except Exception as e: # noqa
last_err = e
msg = str(e)
# 仅对 503/限流 重试
if "503" in msg or "overloaded" in msg or "429" in msg:
if attempt < max_retries - 1:
await asyncio.sleep(backoff * (2 ** attempt))
continue
else:
raise # 非限流错误立即失败
else:
raise RuntimeError(f"网关过载重试后仍失败: {last_err}")
# 保存产出到文件
file_path = None
if self._output_dir:
self._output_dir.mkdir(parents=True, exist_ok=True)
file_path = self._output_dir / self.spec.output_file
file_path.write_text(final, encoding="utf-8")
self._emit("agent_end", {
"agent": self.spec.key,
"output_file": str(file_path) if file_path else None,
"output_preview": final[:300],
})
return {"output": final, "file": file_path}
@staticmethod
def _extract_text(result: dict) -> str:
messages = result.get("messages", [])
text_parts = []
for m in messages:
if hasattr(m, "content") and m.type == "ai" and m.content:
text_parts.append(m.content)
if text_parts:
return "\n\n".join(text_parts).strip()
content = result.get("output", "")
return content if content else "[智能体未生成有效输出]"
def build_agents(
llm,
base_dir: Path,
output_dir: Path | None = None,
on_evidence: Callable[[str, dict], None] | None = None,
keys: list[str] | None = None,
) -> dict[str, XingbangAgent]:
"""按流水线顺序构建五智能体实例。"""
specs = agent_specs()
order = keys or ["data_curator", "risk_scout", "case_finder",
"approval_advisor", "risk_monitor"]
return {
key: XingbangAgent(
spec=specs[key], llm=llm,
base_dir=base_dir, output_dir=output_dir, on_evidence=on_evidence,
)
for key in order
}