fix(v2): 修复预审模型字段表三处缺陷并留出工具预算余量

- 补 8 个标准字段(还款来源/涉诉/增信/保证金/保险/客户名称),对齐原生方法文档声明的 inputs
- 字段守卫改为比对标准字段表,不再把"标准字段但本次无数据"误判为未知字段
- 向模型下发 standard_fields 并在提示词约束字段名,避免自造派生指标
- 补 insurance./deposit. 维度前缀;方案预算 12→18、120s→180s,版本升 1.1.0

实测(临时库 + DeepSeek):模型类技能由 failed 转为 partial(部分 unknown 属如实标注),
降级记录为空,工具调用 11 次。字段表三处为代码层改动,对既有库即时生效。
验证:scripts/verify.py 11/11 PASS,531 项单测。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:38:28 +08:00
co-authored by Claude Code
parent 962d4250dd
commit 4df80c0053
4 changed files with 24 additions and 9 deletions
+10 -2
View File
@@ -20,8 +20,16 @@ FIELDS = {
"financial_assets": "financial.total_assets", "credit_overdue_count": "credit.overdue_count",
"credit_report_date": "credit.report_date",
"facility_term": "facility.term",
# 补齐原生方法文档声明、而 S1 字段表原本缺失的字段族:还款来源、涉诉、增信、保证金、保险、客户名称。
# 依据:skills/*/SKILL.md 的 inputs 声明,以及 REVIEW_TOPICS 已预设的 customer./repayment./legal./guarantee. 前缀。
"customer_name": "customer.name",
"repayment_source_type": "repayment.source_type", "repayment_annual_amount": "repayment.annual_amount",
"legal_case_count": "legal.case_count", "legal_amount": "legal.amount",
"guarantee_amount": "guarantee.amount", "deposit_amount": "deposit.amount",
"insurance_amount": "insurance.amount",
}
NUMERIC = set(FIELDS.values()) - {"collection.dependency", "collection.evidence", "credit.report_date"}
NUMERIC = set(FIELDS.values()) - {"collection.dependency", "collection.evidence", "credit.report_date",
"customer.name", "repayment.source_type"}
LIMIT = 10 * 1024 * 1024
@@ -30,7 +38,7 @@ def scalar(field, value):
return None
if field in NUMERIC:
result = number(value)
if field in ("lease.duplicate_count", "credit.overdue_count") and (result < 0 or result != result.to_integral_value()):
if field in ("lease.duplicate_count", "credit.overdue_count", "legal.case_count") and (result < 0 or result != result.to_integral_value()):
raise ValueError("数量必须为非负整数")
if field in ("facility.amount", "lease.total_value") and result <= 0:
raise ValueError("融资金额和租赁物价值必须为正数")
+10 -3
View File
@@ -13,6 +13,7 @@ from langchain_openai import ChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, field_validator
from graph.agent import agent_manager
from services.assistant_materials import FIELDS
class ModelCheck(BaseModel):
@@ -82,6 +83,8 @@ PROMPT = """你是兴邦项目核查助手,必须通过受限工具读取本
必须读取 read_project_snapshot、read_checks;每个选中方法都用 read_method 读取。
输出一个JSON对象,只含 findings 和 requirement_response。findings 每项字段为 code(稳定英文码)、title、result
(pass/issue/unknown/not_applicable)、evidence_ids(真实证据ID数组)、fields(标准字段数组)、suggested_action。
fields 每项必须**逐字**取自本次任务给出的 standard_fields 列表;不得自造字段名,也不得使用派生指标名
(如负债率、流动比率);派生值只能引用程序已计算的 metrics.* 。清单里没有的字段,说明该项无法核查。
没有依据不得判定pass/issue。缺失资料标unknown,证据只能引用冻结快照。不得引用其他项目经验中的客户信息。
requirement_response必须是一个普通字符串,不得输出对象或数组;逐项说明是否完成用户要求,无法验证要明确说明。
示例顶层结构:{"findings": [], "requirement_response": "已读取授权事实并完成核查。"}。不要Markdown围栏。"""
@@ -134,6 +137,7 @@ def invoke(skill, snapshot, checks, rules, requirement, experiences, model_confi
available = [read_project_snapshot, read_document, read_checks, read_method, read_experiences]
selected = [t for t in available if t.name in skill["allowed_tools"]]
task = {"instructions": skill.get("instructions"), "requirement": requirement, "methods": list(skill.get("methods", {})),
"standard_fields": sorted(FIELDS.values()),
"documents": [{"id": d["id"], "name": d["name"]} for d in snapshot["documents"]]}
prompt = PROMPT
if feedback:
@@ -153,7 +157,6 @@ def invoke(skill, snapshot, checks, rules, requirement, experiences, model_confi
if feedback and parsed.experience_proposal is None:
raise ValueError("模型未返回结构化经验草稿")
if parsed.experience_proposal:
from services.assistant_materials import FIELDS
proposal = parsed.experience_proposal
if not proposal.conditions or set(proposal.conditions) - set(FIELDS.values()) or proposal.required_field not in FIELDS.values() or proposal.exception_field not in FIELDS.values() or proposal.required_field in proposal.conditions:
raise ValueError("经验草稿的触发条件或执行字段无效")
@@ -168,7 +171,11 @@ def invoke(skill, snapshot, checks, rules, requirement, experiences, model_confi
raise ValueError("模型结论缺少证据")
if any("document:" + document_evidence[e] not in reads for e in finding.evidence_ids if e in document_evidence):
raise ValueError("模型引用了尚未读取的材料片段")
if set(finding.fields) - set(snapshot["values"]):
raise ValueError("模型引用未知字段")
# 白名单比的是**标准字段表**,不是"当前恰好有值的字段":
# 后者会把"标准字段但本次无数据"误判成未知字段,与提示词"缺失资料标 unknown"相矛盾。
# 仍是封闭白名单——表外字段一样被拒,模型不能自造指标。
unknown = sorted(set(finding.fields) - set(FIELDS.values()))
if unknown:
raise ValueError("模型引用未知字段:" + ",".join(unknown))
seen.add(finding.code)
return parsed.model_dump(), {"engine": result["engine"], "tool_calls": count[0], "tools_read": sorted(reads), "model": model_config["name"]}
+2 -2
View File
@@ -52,8 +52,8 @@ def seed_components():
{"id": "judgment", "title": "待决策事项", "dependencies": ["*"]}]},
]
items += [
{"id": "corporate.default", "kind": "plan", "name": "对公送审前检查(兼容)", "audience": ["manager"], "run_kind": "pre_review", "skills": [s["id"] for s in skills], "rules": ["demo.coverage"], "template": "template.due-diligence", "legacy_plan_id": "plan-default", "budget": {"timeout_seconds": 120, "max_tool_calls": 12}, "dependencies": []},
{"id": "corporate.pre-review", "kind": "plan", "name": "客户经理AI预审", "audience": ["manager"], "run_kind": "pre_review", "skills": [s["id"] for s in skills], "rules": ["demo.coverage"], "template": "template.ai-pre-review", "legacy_plan_id": "plan-default", "budget": {"timeout_seconds": 120, "max_tool_calls": 12}, "dependencies": []},
{"id": "corporate.default", "kind": "plan", "version": "1.1.0", "name": "对公送审前检查(兼容)", "audience": ["manager"], "run_kind": "pre_review", "skills": [s["id"] for s in skills], "rules": ["demo.coverage"], "template": "template.due-diligence", "legacy_plan_id": "plan-default", "budget": {"timeout_seconds": 180, "max_tool_calls": 18}, "dependencies": []},
{"id": "corporate.pre-review", "kind": "plan", "version": "1.1.0", "name": "客户经理AI预审", "audience": ["manager"], "run_kind": "pre_review", "skills": [s["id"] for s in skills], "rules": ["demo.coverage"], "template": "template.ai-pre-review", "legacy_plan_id": "plan-default", "budget": {"timeout_seconds": 180, "max_tool_calls": 18}, "dependencies": []},
{"id": "corporate.independent-review", "kind": "plan", "name": "评审员独立复核", "audience": ["reviewer"], "run_kind": "independent_review", "skills": [s["id"] for s in skills if s["id"] != "corporate.analysis"] + ["review.analysis"], "rules": ["demo.coverage"], "template": "template.independent-review", "legacy_plan_id": "plan-default", "budget": {"timeout_seconds": 180, "max_tool_calls": 18}, "dependencies": []},
{"id": "corporate.business-sql", "kind": "plan", "name": "多表业务库AI预审", "audience": ["manager"], "run_kind": "pre_review", "skills": [s["id"] for s in skills if s["id"] != "corporate.coverage"], "rules": ["demo.coverage"], "template": "template.ai-pre-review", "legacy_plan_id": "plan-business-sql", "budget": {"timeout_seconds": 120, "max_tool_calls": 12}, "dependencies": []},
]
+2 -2
View File
@@ -21,8 +21,8 @@ TERMINAL = {"completed", "partial", "failed", "cancelled"}
REVIEW_TOPICS = (
{"id": "subject", "title": "主体信息", "prefixes": ("project.", "customer.", "corporate.")},
{"id": "financial", "title": "财务与现金流", "prefixes": ("financial.", "due_diligence.", "metrics.")},
{"id": "lease_asset", "title": "租赁物", "prefixes": ("lease.", "asset.")},
{"id": "enhancement", "title": "增信措施", "prefixes": ("guarantee.", "enhancement.")},
{"id": "lease_asset", "title": "租赁物", "prefixes": ("lease.", "asset.", "insurance.")},
{"id": "enhancement", "title": "增信措施", "prefixes": ("guarantee.", "enhancement.", "deposit.")},
{"id": "credit_legal", "title": "征信与涉诉", "prefixes": ("credit.", "legal.", "lawsuit.")},
{"id": "transaction", "title": "交易结构与还款", "prefixes": ("facility.", "collection.", "repayment.")},
)