feat: add quality evaluation center

This commit is contained in:
2026-05-21 20:54:46 +08:00
parent 2587d7a7a3
commit 5beff9af07
4 changed files with 532 additions and 28 deletions
+472
View File
@@ -0,0 +1,472 @@
"use client";
import {
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
ExperimentOutlined,
FileSearchOutlined,
LinkOutlined,
PlayCircleOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { Alert, Button, Card, Empty, Progress, Select, Space, Statistic, Table, Tag, Typography, message } from "antd";
import type { ColumnsType } from "antd/es/table";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getPromptAssetDetail, listPromptAssets, runPromptAssetEvaluation } from "@/services/prompt-asset";
import { useAuthStore } from "@/stores/auth.store";
import type { PromptAssetDetailResponse, PromptAssetEvaluationRunItem, PromptAssetItem, PromptAssetType } from "@/types/api";
const { Paragraph, Text, Title } = Typography;
const promptTypeOptions: Array<{ value: PromptAssetType | "all"; label: string }> = [
{ value: "all", label: "全部能力" },
{ value: "contract_generation", label: "合同/文书生成" },
{ value: "contract_review", label: "合同自动审查" },
{ value: "clause_recommendation", label: "条款推荐" },
{ value: "legal_chat", label: "法规问答" },
{ value: "wording_rewrite", label: "措辞改写" },
{ value: "regulation_impact", label: "法规影响分析" },
];
const evaluationStatusOptions = [
{ value: "all", label: "全部评测状态" },
{ value: "passed", label: "评测通过" },
{ value: "failed", label: "评测失败" },
{ value: "pending", label: "等待评测" },
{ value: "not_evaluated", label: "尚未评测" },
{ value: "blocked", label: "发布受阻" },
];
const statusOrder: Record<string, number> = {
failed: 0,
pending: 1,
not_evaluated: 2,
passed: 3,
};
function formatDateTime(value?: string | null) {
return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "-";
}
function getPromptTypeLabel(value?: string | null) {
return promptTypeOptions.find((item) => item.value === value)?.label ?? value ?? "-";
}
function getEvaluationLabel(value?: string | null) {
if (value === "passed") return "评测通过";
if (value === "failed") return "评测失败";
if (value === "pending") return "等待评测";
return "尚未评测";
}
function getEvaluationTag(value?: string | null) {
if (value === "passed") return <Tag color="green">评测通过</Tag>;
if (value === "failed") return <Tag color="red">评测失败</Tag>;
if (value === "pending") return <Tag color="gold">等待评测</Tag>;
return <Tag color="default">尚未评测</Tag>;
}
function getPublishTag(item: PromptAssetItem) {
if (item.status === "published") return <Tag color="green">已发布</Tag>;
if (item.latest_evaluation_status === "passed") return <Tag color="blue">可发布</Tag>;
return <Tag color="red">发布受阻</Tag>;
}
function getEvaluationModeLabel(report?: Record<string, unknown> | null) {
const mode = typeof report?.evaluation_mode === "string" ? report.evaluation_mode : "";
if (mode === "llm") return "逐条大模型";
if (mode === "local_fallback") return "本地兜底";
if (mode === "llm_required_not_configured") return "模型未配置";
if (mode === "local") return "本地规则";
return "未记录";
}
function getReportNumber(report: Record<string, unknown> | null | undefined, key: string) {
const value = report?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function getSampleCount(report?: Record<string, unknown> | null) {
const sampleCount = getReportNumber(report, "sample_count");
if (sampleCount != null) return sampleCount;
const sampleResults = report?.sample_results;
return Array.isArray(sampleResults) ? sampleResults.length : 0;
}
function getPassedSamples(report?: Record<string, unknown> | null) {
const passed = getReportNumber(report, "passed_samples");
if (passed != null) return passed;
const sampleResults = report?.sample_results;
return Array.isArray(sampleResults) ? sampleResults.filter((item) => Boolean((item as { passed?: unknown }).passed)).length : 0;
}
function getPassRateFromAsset(item: PromptAssetItem) {
const sampleCount = getSampleCount(item.latest_evaluation_report);
if (sampleCount > 0) {
return Math.round((getPassedSamples(item.latest_evaluation_report) / sampleCount) * 100);
}
const score = item.latest_evaluation_score;
if (typeof score === "number" && Number.isFinite(score)) {
return Math.max(0, Math.min(100, Math.round(score)));
}
return 0;
}
function sortByRisk(items: PromptAssetItem[]) {
return [...items].sort((left, right) => {
const leftOrder = statusOrder[left.latest_evaluation_status ?? "not_evaluated"] ?? 2;
const rightOrder = statusOrder[right.latest_evaluation_status ?? "not_evaluated"] ?? 2;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
return new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime();
});
}
export default function QualityEvalsPage() {
const [messageApi, contextHolder] = message.useMessage();
const currentUser = useAuthStore((state) => state.currentUser);
const [loading, setLoading] = useState(true);
const [runningId, setRunningId] = useState<string | null>(null);
const [items, setItems] = useState<PromptAssetItem[]>([]);
const [promptTypeFilter, setPromptTypeFilter] = useState<PromptAssetType | "all">("all");
const [evaluationStatusFilter, setEvaluationStatusFilter] = useState("all");
const [selectedDetail, setSelectedDetail] = useState<PromptAssetDetailResponse | null>(null);
const isAdmin = currentUser?.role === "admin";
const loadAssets = useCallback(async () => {
if (!isAdmin) {
setLoading(false);
return;
}
setLoading(true);
try {
const first = await listPromptAssets(1, 100, promptTypeFilter, "all");
if (first.code !== 0) {
messageApi.error(first.message || "质量评测数据加载失败");
return;
}
const allItems = [...first.data.items];
const totalPages = Math.ceil(first.data.total / first.data.page_size);
for (let page = 2; page <= totalPages; page += 1) {
const next = await listPromptAssets(page, 100, promptTypeFilter, "all");
if (next.code === 0) {
allItems.push(...next.data.items);
}
}
setItems(sortByRisk(allItems));
} catch (error) {
messageApi.error(error instanceof Error ? error.message : "质量评测数据加载失败");
} finally {
setLoading(false);
}
}, [isAdmin, messageApi, promptTypeFilter]);
const loadDetail = useCallback(
async (assetId: string) => {
try {
const result = await getPromptAssetDetail(assetId);
if (result.code !== 0) {
messageApi.error(result.message || "评测历史加载失败");
return null;
}
setSelectedDetail(result.data);
return result.data;
} catch (error) {
messageApi.error(error instanceof Error ? error.message : "评测历史加载失败");
return null;
}
},
[messageApi],
);
useEffect(() => {
void loadAssets();
}, [loadAssets]);
const filteredItems = useMemo(() => {
if (evaluationStatusFilter === "all") return items;
if (evaluationStatusFilter === "blocked") {
return items.filter((item) => item.status !== "published" && item.latest_evaluation_status !== "passed");
}
return items.filter((item) => (item.latest_evaluation_status ?? "not_evaluated") === evaluationStatusFilter);
}, [evaluationStatusFilter, items]);
useEffect(() => {
if (!selectedDetail && filteredItems[0]) {
void loadDetail(filteredItems[0].id);
}
if (selectedDetail && !filteredItems.some((item) => item.id === selectedDetail.asset.id)) {
setSelectedDetail(null);
}
}, [filteredItems, loadDetail, selectedDetail]);
const summary = useMemo(() => {
const total = items.length;
const passed = items.filter((item) => item.latest_evaluation_status === "passed").length;
const failed = items.filter((item) => item.latest_evaluation_status === "failed").length;
const pending = items.filter((item) => item.latest_evaluation_status === "pending").length;
const notEvaluated = items.filter((item) => !item.latest_evaluation_status || item.latest_evaluation_status === "not_evaluated").length;
const publishBlocked = items.filter((item) => item.status !== "published" && item.latest_evaluation_status !== "passed").length;
const totalSamples = items.reduce((sum, item) => sum + getSampleCount(item.latest_evaluation_report), 0);
const passedSamples = items.reduce((sum, item) => sum + getPassedSamples(item.latest_evaluation_report), 0);
return {
total,
passed,
failed,
pending,
notEvaluated,
publishBlocked,
samplePassRate: totalSamples > 0 ? Math.round((passedSamples / totalSamples) * 100) : 0,
};
}, [items]);
const handleRunEvaluation = async (item: PromptAssetItem) => {
setRunningId(item.id);
try {
const result = await runPromptAssetEvaluation(item.id);
if (result.code !== 0) {
messageApi.error(result.message || "运行评测失败");
return;
}
messageApi.success(`评测完成:${getEvaluationLabel(result.data.version.evaluation_status)}`);
await loadAssets();
await loadDetail(item.id);
} catch (error) {
messageApi.error(error instanceof Error ? error.message : "运行评测失败");
} finally {
setRunningId(null);
}
};
const columns: ColumnsType<PromptAssetItem> = [
{
title: "评测对象",
key: "asset",
width: 260,
render: (_, item) => (
<Space direction="vertical" size={0}>
<Text strong>{item.name}</Text>
<Text code>{item.prompt_key}</Text>
<Text type="secondary">{getPromptTypeLabel(item.prompt_type)}</Text>
</Space>
),
},
{
title: "质量门禁",
key: "gate",
width: 160,
render: (_, item) => (
<Space direction="vertical" size={0}>
{getEvaluationTag(item.latest_evaluation_status)}
{getPublishTag(item)}
</Space>
),
},
{
title: "样本通过率",
key: "passRate",
width: 180,
render: (_, item) => (
<Space direction="vertical" size={0} style={{ width: "100%" }}>
<Progress percent={getPassRateFromAsset(item)} size="small" status={item.latest_evaluation_status === "failed" ? "exception" : "normal"} />
<Text type="secondary">
{getPassedSamples(item.latest_evaluation_report)}/{getSampleCount(item.latest_evaluation_report)} 条样本
</Text>
</Space>
),
},
{
title: "执行方式",
key: "mode",
width: 130,
render: (_, item) => <Tag color="blue">{getEvaluationModeLabel(item.latest_evaluation_report)}</Tag>,
},
{
title: "当前版本",
key: "version",
width: 130,
render: (_, item) => (
<Space direction="vertical" size={0}>
<Text>{item.current_version_label}</Text>
<Text type="secondary">{item.version_count} 个版本</Text>
</Space>
),
},
{
title: "更新时间",
dataIndex: "updated_at",
key: "updated_at",
width: 180,
render: (value: string) => formatDateTime(value),
},
{
title: "操作",
key: "actions",
width: 260,
render: (_, item) => (
<Space wrap>
<Button icon={<FileSearchOutlined />} size="small" onClick={() => void loadDetail(item.id)}>
历史
</Button>
<Button icon={<PlayCircleOutlined />} loading={runningId === item.id} size="small" type="primary" onClick={() => void handleRunEvaluation(item)}>
运行评测
</Button>
<Button icon={<LinkOutlined />} size="small">
<Link href="/prompt-assets">治理</Link>
</Button>
</Space>
),
},
];
const evaluationRunColumns: ColumnsType<PromptAssetEvaluationRunItem> = [
{
title: "套件",
dataIndex: "suite_key",
key: "suite_key",
width: 180,
},
{
title: "状态",
key: "status",
width: 120,
render: (_, item) => getEvaluationTag(item.status),
},
{
title: "样本",
key: "samples",
width: 120,
render: (_, item) => `${item.passed_samples}/${item.sample_count}`,
},
{
title: "通过率",
key: "pass_rate",
width: 140,
render: (_, item) => `${Math.round((item.pass_rate ?? 0) * 100)}%`,
},
{
title: "分数",
key: "quality_score",
width: 100,
render: (_, item) => item.quality_score ?? "-",
},
{
title: "执行方式",
key: "mode",
width: 150,
render: (_, item) => <Tag color="blue">{getEvaluationModeLabel(item.report)}</Tag>,
},
{
title: "失败原因",
dataIndex: "failure_reason",
key: "failure_reason",
ellipsis: true,
render: (value: string | null) => value ?? "-",
},
{
title: "完成时间",
dataIndex: "finished_at",
key: "finished_at",
width: 180,
render: (value: string | null) => formatDateTime(value),
},
];
if (!isAdmin) {
return (
<main className="page-shell review-history-page">
{contextHolder}
<Card className="review-list-card">
<Alert title="只有管理员可以查看质量评测中心。" type="warning" showIcon />
</Card>
</main>
);
}
return (
<main className="page-shell review-page-grid">
{contextHolder}
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<section className="review-page-hero">
<div className="review-page-hero-copy">
<span className="landing-badge">质量评测中心</span>
<Title level={2}>黄金样本评测与发布门禁</Title>
<Text type="secondary">
汇总生成、审查、法规问答、条款推荐、措辞改写和法规影响分析的 Prompt 评测结果,把原来分散在 Prompt 治理里的质量状态变成独立入口。
</Text>
</div>
<Card className="review-side-card" variant="borderless">
<span className="landing-panel-eyebrow">Phase15-F</span>
<Title level={4} style={{ margin: 0, color: "inherit" }}>
发布前质量门禁
</Title>
<Paragraph style={{ color: "inherit", opacity: 0.82, marginBottom: 0 }}>
未通过评测的 Prompt 不能发布;已发布 Prompt 可在治理页继续回滚、下线和查看运行证据。
</Paragraph>
</Card>
</section>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 12 }}>
<Card>
<Statistic title="评测对象" value={summary.total} prefix={<ExperimentOutlined />} />
</Card>
<Card>
<Statistic title="通过门禁" value={summary.passed} prefix={<CheckCircleOutlined />} valueStyle={{ color: "#237804" }} />
</Card>
<Card>
<Statistic title="失败或待处理" value={summary.failed + summary.pending + summary.notEvaluated} prefix={<CloseCircleOutlined />} valueStyle={{ color: "#a8071a" }} />
</Card>
<Card>
<Statistic title="发布受阻" value={summary.publishBlocked} prefix={<ClockCircleOutlined />} valueStyle={{ color: "#ad6800" }} />
</Card>
<Card>
<Statistic title="样本通过率" value={summary.samplePassRate} suffix="%" prefix={<CheckCircleOutlined />} />
</Card>
</div>
<Card>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Space wrap>
<Select style={{ width: 190 }} options={promptTypeOptions} value={promptTypeFilter} onChange={(value) => setPromptTypeFilter(value)} />
<Select style={{ width: 170 }} options={evaluationStatusOptions} value={evaluationStatusFilter} onChange={setEvaluationStatusFilter} />
<Button icon={<ReloadOutlined />} loading={loading} onClick={() => void loadAssets()}>
刷新
</Button>
<Button icon={<LinkOutlined />}>
<Link href="/prompt-assets">进入 Prompt 治理</Link>
</Button>
</Space>
<Table
columns={columns}
dataSource={filteredItems}
loading={loading}
locale={{ emptyText: <Empty description="暂无质量评测对象" /> }}
pagination={{ pageSize: 10, showSizeChanger: true }}
rowKey="id"
/>
</Space>
</Card>
<Card title={selectedDetail ? `${selectedDetail.asset.name} 评测历史` : "评测历史"}>
{selectedDetail ? (
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Alert
message="当前版本质量状态"
description={`${selectedDetail.asset.current_version_label}:${getEvaluationLabel(selectedDetail.asset.latest_evaluation_status)},执行方式:${getEvaluationModeLabel(selectedDetail.asset.latest_evaluation_report)}。`}
type={selectedDetail.asset.latest_evaluation_status === "passed" ? "success" : "warning"}
showIcon
/>
<Table columns={evaluationRunColumns} dataSource={selectedDetail.evaluation_runs} pagination={false} rowKey="id" size="small" />
</Space>
) : (
<Empty description="请选择一个评测对象查看历史" />
)}
</Card>
</Space>
</main>
);
}
@@ -10,6 +10,7 @@ import {
DashboardOutlined,
DeploymentUnitOutlined,
DownOutlined,
ExperimentOutlined,
FileDoneOutlined,
FileTextOutlined,
FileSearchOutlined,
@@ -87,6 +88,7 @@ const adminNavItems = [
{ href: "/templates", label: "模板管理", icon: <BookOutlined /> },
{ href: "/knowledge-bases", label: "知识库管理", icon: <FolderOpenOutlined /> },
{ href: "/prompt-assets", label: "Prompt 治理", icon: <CodeOutlined /> },
{ href: "/quality-evals", label: "质量评测", icon: <ExperimentOutlined /> },
{ href: "/legal-change-impacts", label: "法规影响分析", icon: <AlertOutlined /> },
{ href: "/operation-kpis", label: "运营 KPI", icon: <BarChartOutlined /> },
{ href: "/sales-followups", label: "销售跟进", icon: <PhoneOutlined /> },
@@ -4211,7 +4211,7 @@
- 本轮核对结论:
- Phase15-A 到 Phase15-H 的主线开发已经在代码层闭环。
- `/clauses/apply` 未单独实现,但条款推荐和改写结果已经通过 `/documents/[id]/edit` 编辑器写入合同正文并生成 `generated_contract_versions`,属于等价闭环。
- `/quality-evals` 未单独实现,当前评测入口集中在 `/prompt-assets`,已能支撑 Prompt 发布前评测门禁;如要做统一质量中心,后续可单独补页面和通用评测表。
- `/quality-evals` 在本轮核对当时尚未单独实现,评测入口集中在 `/prompt-assets`;该缺口已在后续“Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环”中补齐。
- 法规影响分析代码统一使用 `/legal-change-impacts`、`legal_change_events`、`legal_change_impacts` 命名,后续文档应以此为准。
- 本轮验证:
- `$env:PYTHONPATH='backend'; python -m unittest backend.tests.test_legal_document_type_service backend.tests.test_legal_document_types_api backend.tests.test_legal_document_element_schema_service backend.tests.test_legal_document_element_schemas_api backend.tests.test_legal_document_draft_service backend.tests.test_legal_document_drafts_api backend.tests.test_contracts_api backend.tests.test_review_risk_action_service backend.tests.test_review_risk_actions_api backend.tests.test_clause_recommendation_service backend.tests.test_clauses_api backend.tests.test_prompt_asset_service backend.tests.test_prompt_assets_api backend.tests.test_prompt_runtime_service backend.tests.test_legal_change_impact_service backend.tests.test_legal_change_impacts_api backend.tests.test_operation_kpi_service backend.tests.test_operation_kpis_api`
@@ -4223,7 +4223,7 @@
- 剩余边界:
- 当前仍有 Phase15-C、Phase15-F、Phase15-G 相关未提交改动,需要后续统一整理提交。
- 最新代码尚未部署到 `192.168.3.90` 做真实库烟测。
- 专业富文本库、字段级 diff、独立 `/quality-evals`、全量法规向量化、真实微信或支付宝支付、电子签章和小程序均属于后续增强,不是当前法律文书能力吸收主线阻塞。
- 专业富文本库、字段级 diff、全量法规向量化、真实微信或支付宝支付、电子签章和小程序均属于后续增强,不是当前法律文书能力吸收主线阻塞;独立 `/quality-evals` 已在后续闭环中补齐。
## 2026-05-21 Phase15-C/F/G 提交推送与 3.90 真实库烟测闭环
@@ -4270,6 +4270,32 @@
- 远程真实 PostgreSQL 已在 `20260521_0038 (head)`。
- 法规影响 worker 已在测试服务器常驻运行,并通过真实库临时事件消费烟测。
- 剩余边界:
- 本轮没有做 `/quality-evals` 独立质量中心。
- 本轮当时没有做 `/quality-evals` 独立质量中心;该项已在后续“Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环”中补齐。
- 本轮没有做全量法规向量化。
- 本轮没有接入真实微信或支付宝支付。
## 2026-05-21 Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环
- 已按《AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md》补齐文档提到的独立质量评测产品入口。
- 本轮新增前端页面:
- `frontend/src/app/quality-evals/page.tsx`
- 管理员菜单新增“质量评测”,入口位于 `frontend/src/components/layout/app-topbar.tsx`。
- 当前实现口径:
- 不新增重复的质量评测后端模型,继续以 `prompt_assets`、`prompt_asset_versions`、`prompt_asset_evaluation_runs` 作为黄金样本评测真相源。
- `/quality-evals` 负责聚合生成、审查、法规问答、条款推荐、措辞改写、法规影响分析等 Prompt 类型的评测状态。
- 页面支持按能力类型、评测状态、发布受阻筛选。
- 页面展示评测对象、质量门禁、样本通过率、执行方式、当前版本、更新时间和评测历史。
- 页面可直接触发 Prompt 黄金样本评测,并可跳转 `/prompt-assets` 做发布、下线、回滚和运行证据治理。
- 已更新代码对齐检查报告:
- `project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md`
- 旧结论“没有独立 `/quality-evals` 页面”已改为“独立入口已闭环”。
- 本地验证:
- `cd frontend; npx eslint "src/app/quality-evals/page.tsx" "src/components/layout/app-topbar.tsx"`
- `cd frontend; npm run build`
- 构建结果:`Compiled successfully`,静态页面生成 `40/40`,路由清单包含 `/quality-evals`。
- 当前闭环结论:
- 文档提到的“独立 `/quality-evals` 页面”已经完成本地开发闭环。
- Phase15-F 现在形成“Prompt 治理评测真相源 -> `/quality-evals` 独立质量入口 -> 运行评测 -> 查看历史 -> 发布门禁 -> 回到 Prompt 治理”的产品入口闭环。
- 剩余边界:
- 本轮暂未新增通用 `ai_quality_eval_runs/items` 表;该表只在未来要把非 Prompt 质量评测也做成独立后端领域时再补。
- 下一步需要提交推送,并部署到 `192.168.3.90` 后访问 `/quality-evals` 做前端路由烟测。
@@ -15,11 +15,11 @@
当前代码已经覆盖 Phase15-A 到 Phase15-H 的主线开发闭环。也就是说,文书类型入口、动态要素表单、草稿保存恢复、生成接口升级、审查建议采纳、条款推荐与行业惯例、富文本编辑器和 AI 侧边栏、Prompt 治理、黄金样本评测、法规影响扫描、运营 KPI 与商业化增强,均已在代码层形成可运行闭环。
但仍有三类事项需要分开看:
截至本轮独立质量评测中心收口后,仍有三类事项需要分开看:
1. 已开发但待提交或待部署验证:当前工作区存在 Phase15-C、Phase15-F、Phase15-G 的未提交改动,且最新 worker 和语义扫描未在 `192.168.3.90` 做远程部署烟测。
2. 文档建议路径与代码等价实现不同:例如文档建议 `/quality-evals` 和 `/clauses/apply`,当前分别落在 `/prompt-assets` 评测历史和合同编辑器版本链中。
3. 后续增强项:专业富文本库、字段级差异对比、独立质量评测中心、全量法规向量化、真实微信或支付宝支付、电子签章、小程序等不属于当前 Phase15 主线硬阻塞。
1. 本轮新增 `/quality-evals` 独立页面,不新增数据库迁移,继续复用 Prompt 评测运行记录作为质量评测真相源。
2. 文档建议路径与代码仍保留一个等价实现差异:文档建议 `/clauses/apply`,当前落在合同编辑器版本链中;`/quality-evals` 已补成独立产品入口。
3. 后续增强项:专业富文本库、字段级差异对比、通用 `ai_quality_eval_runs/items`、全量法规向量化、真实微信或支付宝支付、电子签章、小程序等不属于当前 Phase15 主线硬阻塞。
## 三、逐阶段对齐表
@@ -39,7 +39,7 @@
| LAW-CLAUSE-002 | 措辞优化 | 支持专业化、简化、偏甲方、偏乙方、均衡等改写模式,返回法律边界和谈判建议 | 已闭环 |
| Phase15-D | 富文本编辑器与 AI 侧边栏 | `/documents/[id]/edit` 支持正文编辑、自动保存、版本历史、条款推荐、措辞改写、相关法条和 Chat 入口 | 主线已闭环 |
| Phase15-E | Prompt 资产治理 | `/prompt-assets` 支持 Prompt 类型、版本、评测、发布、下线、回滚和运行证据 | 已闭环 |
| Phase15-F | 黄金样本与质量评测 | 已有 Prompt 评测运行记录、逐条黄金样本报告、发布前门禁和前端评测历史 | 已闭环 |
| Phase15-F | 黄金样本与质量评测 | 已有 Prompt 评测运行记录、逐条黄金样本报告、发布前门禁、`/prompt-assets` 治理入口和 `/quality-evals` 独立质量评测中心 | 已闭环 |
| Phase15-G | 法规变更影响分析 | 已有法条变更入队、结构化和语义影响扫描、影响项、页面、后台 worker 和 Docker 服务 | 已闭环 |
| Phase15-H | 运营 KPI 与商业化增强 | `/operation-kpis` 已展示用量、文书生成、审查、Chat、模板、条款采纳、试用和转化指标;计费、发票状态、销售跟进已有业务闭环 | 主线已闭环 |
@@ -83,6 +83,7 @@
- 合同编辑器和 AI 侧边栏:`frontend/src/app/documents/[id]/edit/page.tsx`
- 审查建议处理:`frontend/src/app/reviews/[id]/page.tsx`
- Prompt 治理:`frontend/src/app/prompt-assets/page.tsx`
- 质量评测中心:`frontend/src/app/quality-evals/page.tsx`
- 法规影响分析:`frontend/src/app/legal-change-impacts/page.tsx`
- 运营 KPI:`frontend/src/app/operation-kpis/page.tsx`
- 销售跟进:`frontend/src/app/sales-followups/page.tsx`
@@ -108,14 +109,15 @@
### 5.2 `/quality-evals`
文档建议新增独立 `/quality-evals` 页面。当前代码没有这个独立路由,质量评测落在 `/prompt-assets` 中:
文档建议新增独立 `/quality-evals` 页面。本轮已经补齐独立路由,并保持评测数据真相源不重复:
- Prompt 详情页可以运行评测。
- 评测结果回填当前版本。
- 发布按钮受评测状态约束。
- 评测运行历史可以追溯。
- `/quality-evals` 聚合生成、审查、法规问答、条款推荐、措辞改写、法规影响分析等 Prompt 类型的评测状态。
- 页面支持按 Prompt 类型、评测状态和“发布受阻”筛选。
- 页面展示评测对象、质量门禁、样本通过率、执行方式、版本、更新时间和评测历史。
- 页面可直接触发 `POST /api/v1/prompt-assets/{asset_id}/evaluations/run` 重新运行黄金样本评测。
- 页面保留跳转 `/prompt-assets` 的治理入口,发布、下线、回滚和运行证据仍由 Prompt 治理承接。
这对“Prompt 发布前黄金样本评测门禁”已经闭环。但如果要做跨生成、审查、Chat、条款推荐、法规影响的统一质量中心,仍建议后续补独立 `/quality-evals`。
因此,“文档要求独立质量评测产品入口”已经闭环;后续只有在需要把非 Prompt 质量评测也沉淀为独立领域模型时,才需要另补通用 `ai_quality_eval_runs/items` 表。
### 5.3 `regulation-changes`
@@ -131,19 +133,20 @@
### 6.1 待提交和待部署验证
当前工作区存在未提交改动,主要集中在:
Phase15-C、Phase15-F、Phase15-G 已完成提交、推送和 `192.168.3.90` 真实库烟测,详见当前工作进度保存文档。
- Phase15-C 行业惯例推荐。
- Phase15-F 真实黄金样本逐条评测。
- Phase15-G 法规影响语义扫描和后台 worker。
这些代码本地测试已通过,但尚未提交远程 Git,也未在 `192.168.3.90` 重新部署后做真实库烟测。
本轮新增 `/quality-evals` 不涉及 Alembic 迁移或后端服务变更。完成提交后,只需要部署前端并访问 `/quality-evals` 做页面烟测。
### 6.2 独立质量评测中心
当前评测能力已经在 Prompt 治理内闭环,但没有独立 `/quality-evals` 页面,也没有通用 `ai_quality_eval_runs/items` 表承接所有质量评测类型。
独立 `/quality-evals` 页面已经补齐,当前闭环采用“独立页面 + 复用 Prompt 评测真相源”的方式:
这不是 Prompt 发布门禁阻塞项,但属于文档和产品形态的一个剩余增强点。
- 前端入口:`frontend/src/app/quality-evals/page.tsx`。
- 菜单入口:管理员菜单“质量评测”。
- 数据来源:`prompt_assets`、`prompt_asset_versions`、`prompt_asset_evaluation_runs`。
- 执行动作:复用 Prompt 资产评测接口重新运行黄金样本评测。
当前仍未单独新增通用 `ai_quality_eval_runs/items` 表。这个表属于后续“跨 Prompt、跨业务对象的统一质量评测域”增强,不影响 Phase15-F 的产品入口闭环。
### 6.3 专业级富文本增强
@@ -194,7 +197,7 @@ OK
命令:
```powershell
npx eslint "src/app/documents/new/page.tsx" "src/app/documents/new/[type]/page.tsx" "src/app/documents/[id]/edit/page.tsx" "src/app/prompt-assets/page.tsx" "src/app/legal-change-impacts/page.tsx" "src/app/operation-kpis/page.tsx" "src/components/layout/app-topbar.tsx" "src/types/api.ts"
npx eslint "src/app/documents/new/page.tsx" "src/app/documents/new/[type]/page.tsx" "src/app/documents/[id]/edit/page.tsx" "src/app/prompt-assets/page.tsx" "src/app/quality-evals/page.tsx" "src/app/legal-change-impacts/page.tsx" "src/app/operation-kpis/page.tsx" "src/components/layout/app-topbar.tsx" "src/types/api.ts"
```
结果:通过,无错误输出。
@@ -211,7 +214,7 @@ npm run build
```text
Compiled successfully
Generating static pages (39/39)
Generating static pages (40/40)
```
构建路由中已包含:
@@ -220,6 +223,7 @@ Generating static pages (39/39)
- `/documents/new/[type]`
- `/documents/[id]/edit`
- `/prompt-assets`
- `/quality-evals`
- `/legal-change-impacts`
- `/operation-kpis`
- `/sales-followups`
@@ -228,9 +232,9 @@ Generating static pages (39/39)
为了让文档和代码彻底闭合,建议下一步不要继续堆新功能,先按这个顺序收口:
1. 整理并提交当前 Phase15-C、Phase15-F、Phase15-G 的代码和文档改动。
2. 如果本阶段要求“文档路径和产品入口完全一致”,补独立 `/quality-evals` 质量评测中心,把 `/prompt-assets` 中的评测历史聚合过去。
1. 提交并推送本轮 `/quality-evals` 页面和文档更新。
2. 部署前端到 `192.168.3.90`,访问 `/quality-evals` 做页面烟测。
3. 统一文档命名,把法规影响分析的接口名称固定为 `/legal-change-impacts`,避免后续误认为还缺 `/regulation-changes`。
4. 等决定部署时,在 `192.168.3.90` 执行最新 Alembic,并启用 `legal-change-impact-worker` 服务做真实库烟测。
4. 如未来要做更宽的统一质量平台,再补通用 `ai_quality_eval_runs/items` 和非 Prompt 质量评测对象。
按当前代码状态判断,《法律文书能力吸收开发方案》的主线开发没有跑偏,已经基本完成。真正剩下的是提交、部署验证、独立质量中心和若干生产级增强。
按当前代码状态判断,《法律文书能力吸收开发方案》的主线开发没有跑偏,已经基本完成。`/quality-evals` 独立入口补齐后,真正剩下的是若干生产级增强,而不是 Phase15 主线功能缺口。