Initial commit: InternalAuditInterprise
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Env / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Data / models (never commit real audit data or model weights)
|
||||||
|
data/
|
||||||
|
models/
|
||||||
|
*.dump
|
||||||
|
*.parquet
|
||||||
|
minio-data/
|
||||||
|
pgdata/
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
# 0-req-AIAudit · 需求与目标文档
|
||||||
|
|
||||||
|
> 项目:基于本地私有化大模型的电信运营商 AI 全域内审平台(AIAudit)
|
||||||
|
> 版本:v0.1(待评审)
|
||||||
|
> 日期:2026-06
|
||||||
|
> 上游来源:`docs/数据不出域,审计全穿透.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 引言
|
||||||
|
|
||||||
|
### 1.1 背景
|
||||||
|
电信运营商年业务规模达 150 亿级,潜在异常金额约 5000 万级,而传统审计依赖人工抽样,覆盖率仅约 5%,存在三类典型困局:
|
||||||
|
|
||||||
|
- **拆单规避**:大额合同拆分为阈值以下小额合同,规避"三重一大"审批与按金额抽样。
|
||||||
|
- **时序造假**:如"养卡骗补"(脉冲式新增 + 规律性退订)、物联网卡虚假激活等,造假藏在时间序列里,抽样与年度审计难以发现。
|
||||||
|
- **工具乏力**:Excel + 人工方式面对海量单据只能抽样,查不全、查不深。
|
||||||
|
|
||||||
|
核心矛盾:审计数据涉及政企合同、用户隐私、财务凭证,上公有云大模型存在合规风险;不引入 AI 又难以应对全量数据。
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
建设一套**部署在本地机房、数据零出域、覆盖全业务域、可持续进化**的 AI 内审能力体系,实现:
|
||||||
|
|
||||||
|
- **全量穿透**:从抽样审计升级为全量扫描(覆盖率 5% → 100%)。
|
||||||
|
- **数据不出域**:模型—数据—推理—结果全链路内网闭环,数据出域风险归零。
|
||||||
|
- **常态化监控**:从年度快照升级为 7×24 近实时常态化监控。
|
||||||
|
- **能力沉淀**:审计经验固化为可执行规则与机构永久资产,越用越精准。
|
||||||
|
- **独立可信**:审计系统本身独立于被审计业务方,自身全程留痕、分权制衡、可被审计。
|
||||||
|
|
||||||
|
### 1.3 范围说明
|
||||||
|
本文档按**完整蓝图全量编写需求**,覆盖数据治理、四大引擎、八大审计场景、人机协同闭环、误报治理、系统自审计、安全合规与价值度量。具体开发优先级与分期(MVP / 二期 / 三期)在后续 PRD(`1-prd-AIAudit.md`)与任务文档(`2-task-AIAudit.md`)中确定。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 术语表
|
||||||
|
|
||||||
|
| 术语 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 本地私有化 LLM | 部署在本地机房、不依赖外网的大语言模型(如千问 70B / DeepSeek),用于推理、规则生成、报告生成、线索解释。 |
|
||||||
|
| 全量穿透 | 不抽样,对全部业务数据(合同、回款、用户行为等)做关联扫描分析。 |
|
||||||
|
| 风险域 | 审计场景的归类维度,分为收入域、成本域、采购域、资金域、合规域五大类。 |
|
||||||
|
| 审计场景 | 具体的造假/风险模式,如政企拆单、养卡骗补、跨期错配等,本平台覆盖八大场景。 |
|
||||||
|
| 线索(Clue) | AI 扫描产出的疑似异常项,附带证据链与判定理由,是审计员处置的起点。 |
|
||||||
|
| 证据链 | 支撑某条线索成立的关联数据与推理路径(如工商关联、时序聚类、金额分布等)。 |
|
||||||
|
| 审计底稿 | 由系统自动生成、可追溯的审计工作记录文档。 |
|
||||||
|
| 规则进化引擎 | 将审计员用自然语言描述的造假模式,自动转化为可执行规则并经沙箱验证、持续迭代的能力模块。 |
|
||||||
|
| 置信度分级 | 对线索按可信程度分为高/中/低三级,分别对应直接处置/人工复核/归档备查。 |
|
||||||
|
| 误报(假阳性) | AI 判为疑似异常但实际属正常的线索。 |
|
||||||
|
| 审计数据中台(审计数据底座) | 审计专用、与业务系统物理隔离、由审计独立掌控的统一数据底座,逻辑上具备数据中台能力(接入、本体建模、时态建模、统一穿透查询),但不与业务方共享。 |
|
||||||
|
| 数据湖 | 汇聚多源异构业务数据的本地统一存储,是审计数据中台的存储基础。 |
|
||||||
|
| 本体(Ontology) | 对审计域核心实体(客户、合同、号码、IMEI、账户、工单、供应商、结算单等)及其关系的形式化定义。 |
|
||||||
|
| 审计知识图谱 | 依据审计本体,将跨系统实体与关系落地形成的图结构,支撑关联穿透与实控人/关联方识别。 |
|
||||||
|
| 双时态建模 | 同时记录"业务发生时间"与"系统记录时间"的数据建模方式,支持按任意历史时点回放数据状态。 |
|
||||||
|
| 主数据对齐 | 客户、合同、号码、工单、供应商等实体在跨系统间的统一识别与关联,是本体层的落地手段。 |
|
||||||
|
| 数据零出域 | 所有敏感数据、模型与推理过程均不离开本地内网机房。 |
|
||||||
|
| 系统自审计 | 审计平台自身的操作、规则、模型、数据变更全程留痕且可被审计的机制。 |
|
||||||
|
| 三重一大 | 重大事项决策、重要干部任免、重大项目安排、大额资金运作的集体决策制度。 |
|
||||||
|
| BSS/OSS | 业务支撑系统 / 运营支撑系统。 |
|
||||||
|
| IMEI | 国际移动设备识别码,用于标识终端设备。 |
|
||||||
|
| 趸交 | 用户一次性预缴多期费用的缴费方式。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 角色定义
|
||||||
|
|
||||||
|
| 角色 | 说明 | 核心诉求 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 审计员 | 一线内审人员,复核线索、研判定性、决定整改或移交、签字。 | 看得懂线索、查得到证据、处置留得下痕。 |
|
||||||
|
| 审计主管 | 审计部门负责人,分派任务、审批处置结论、查看全局看板。 | 全局掌控、成效可量化、流程合规。 |
|
||||||
|
| 规则管理员 | 配置与维护审计规则、阈值,使用规则进化引擎。 | 自然语言配规则、沙箱验证、版本可控。 |
|
||||||
|
| 系统管理员 | 负责数据接入、模型部署、权限分配、系统运维。 | 接入稳定、权限可控、运行可观测。 |
|
||||||
|
| 系统审计员(独立监督) | 审计"审计系统本身",核查规则/阈值/线索是否被人为放水或拦截。 | 任何改动可追溯、线索不可被删除掩盖。 |
|
||||||
|
| 被审计业务方 | 各业务条线(政企、市场、财务、工程等),是审计对象。 | (非系统用户)系统须与其解耦,保证独立性。 |
|
||||||
|
|
||||||
|
> 独立性原则:本平台是独立的内部审计系统,与被审计业务方解耦;业务方无权配置规则、修改阈值或删除线索。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 功能性需求(EARS 格式)
|
||||||
|
|
||||||
|
> EARS 关键词:WHEN(事件触发)/ IF…THEN(条件)/ WHILE(状态持续)/ WHERE(特定场景或特性)/ THE…SHALL(系统须)。
|
||||||
|
|
||||||
|
### 需求 1:多源异构数据接入
|
||||||
|
|
||||||
|
**用户故事:** 作为系统管理员,我希望平台能接入各业务系统的数据,以便为全量审计提供统一数据底座。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHERE 存在 BSS / OSS / ERP / 财务 / 合同 / 工单 / 信令等数据源,THE 平台 SHALL 提供接口、数据库与文件三类接入适配能力,将数据汇入审计专用数据底座(审计数据中台)。
|
||||||
|
2. WHEN 配置一个新数据源接入任务时,THE 平台 SHALL 支持配置连接方式、字段映射与同步周期,且无需修改源系统。
|
||||||
|
3. THE 平台 SHALL 支持全量初始化导入与增量同步两种模式。
|
||||||
|
4. IF 某数据源接入失败或中断,THEN THE 平台 SHALL 记录失败原因并向系统管理员告警,且不影响其他数据源的接入。
|
||||||
|
5. THE 平台 SHALL 保证所有接入数据仅存储于本地内网,任何接入过程不向外网传输数据。
|
||||||
|
|
||||||
|
### 需求 2:审计数据中台 · 本体层与主数据对齐
|
||||||
|
|
||||||
|
**用户故事:** 作为系统管理员,我希望平台建设一个由审计独立掌控、按审计本体组织实体与关系的专用数据底座,以便穿透分析时主键能对得上、关系能连得通。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 建设审计专用、与业务系统物理隔离的数据底座(审计数据中台),由审计独立掌控,被审计业务方无写入、配置或删除权限。
|
||||||
|
2. THE 平台 SHALL 依据审计本体(Ontology)定义客户、合同、号码、IMEI、账户、工单、供应商、结算单等核心实体及其关系,形成审计知识图谱。
|
||||||
|
3. THE 平台 SHALL 对上述核心实体在跨系统间进行统一识别与关联(主数据对齐),并将关系落地到知识图谱,以支撑隐性实控人、关联方网络、"马甲"供应商等穿透分析。
|
||||||
|
4. WHEN 数据接入数据底座时,THE 平台 SHALL 自动探查缺失、重复、口径不一致问题并执行清洗,且为每个数据源/数据集建立可在管理界面查看的数据质量评分。
|
||||||
|
5. IF 检测到关键字段缺失或实体无法对齐,THEN THE 平台 SHALL 标记该记录并提示人工干预,而非静默丢弃。
|
||||||
|
6. THE 平台 SHALL 对外提供统一的穿透查询与图谱查询服务,作为各引擎与审计场景的共同数据入口。
|
||||||
|
7. THE 平台 SHALL 支持接入真实业务系统数据,同时支持导入脱敏/样例数据用于盲测与演示。
|
||||||
|
|
||||||
|
### 需求 3:审计数据中台 · 时态层与增量同步
|
||||||
|
|
||||||
|
**用户故事:** 作为审计主管,我希望数据底座原生支持时间维度并近实时更新,以便既能识别时序造假,又能把结论回溯到当时的数据状态。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 采用双时态建模(业务发生时间 + 系统记录时间)组织原始数据,支持按任意历史时点回放数据状态。
|
||||||
|
2. THE 平台 SHALL 对关键审计对象(如用户生命周期、回款、话务、佣金发放、资源使用量等)保留时间序列,以支撑时序模式造假识别。
|
||||||
|
3. THE 平台 SHALL 支持按可配置周期执行增量同步。
|
||||||
|
4. WHILE 常态化监控处于开启状态,THE 平台 SHALL 持续接收增量数据并触发相应审计规则的重算。
|
||||||
|
5. THE 平台 SHALL 记录每次同步的时间戳、数据量与数据版本,并保证任一结论可回溯到产生它时的数据版本。
|
||||||
|
|
||||||
|
### 需求 4:本地私有化 LLM 引擎
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望用自然语言与系统交互,以便不写 SQL、不翻 Excel 就能查数和获取线索。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 在本地机房部署私有化大语言模型(如千问 70B / DeepSeek),且模型推理过程不依赖外网。
|
||||||
|
2. WHEN 审计员以自然语言提交查询时,THE LLM 引擎 SHALL 理解意图并返回结构化结果或线索。
|
||||||
|
3. THE LLM 引擎 SHALL 支持异常模式推理、自然语言规则配置、报告自动生成与线索解释四类能力。
|
||||||
|
4. WHERE 涉及电信审计专业领域,THE LLM 引擎 SHALL 基于审计领域语料进行微调以提升专业准确性。
|
||||||
|
5. THE 平台 SHALL 记录模型版本,使任一结论可回溯到产生它的模型状态。
|
||||||
|
|
||||||
|
### 需求 5:全量穿透引擎
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望系统对全部业务数据做关联扫描,以便不再受抽样覆盖率限制。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 全量穿透引擎 SHALL 对全部合同、回款、用户行为等数据执行关联扫描,而非抽样。
|
||||||
|
2. THE 全量穿透引擎 SHALL 直连审计数据中台(数据底座),将数据就地提供给本地 LLM 分析,数据不出域。
|
||||||
|
3. WHEN 一个审计任务执行时,THE 引擎 SHALL 输出本次扫描的覆盖范围与数据量,以证明全量性。
|
||||||
|
4. THE 引擎 SHALL 支持跨系统关联分析(如合同—回款—工商—账户的关联穿透)。
|
||||||
|
|
||||||
|
### 需求 6:规则进化引擎
|
||||||
|
|
||||||
|
**用户故事:** 作为规则管理员,我希望用自然语言描述新的造假模式并自动生成可执行规则,以便把审计经验沉淀为机构资产。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHEN 规则管理员用自然语言描述一种造假/风险模式时,THE 规则进化引擎 SHALL 自动将其转化为可执行规则。
|
||||||
|
2. WHEN 一条新规则生成后,THE 引擎 SHALL 在沙箱环境中用历史数据验证其命中率,并在确认前不投入生产。
|
||||||
|
3. THE 引擎 SHALL 对每条规则保存版本历史,记录创建人、修改人、时间与变更内容。
|
||||||
|
4. WHILE 系统运行,THE 引擎 SHALL 支持基于审计员反馈对规则进行迭代优化。
|
||||||
|
5. THE 平台 SHALL 维护一个可持续增长的本地审计规则库,作为机构永久资产。
|
||||||
|
|
||||||
|
### 需求 7:线索驱动引擎
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望系统主动把高价值线索连同证据链推送给我,以便从"人找数据"转为"数据找人"。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHEN 全量穿透或规则命中产生异常聚类时,THE 线索引擎 SHALL 生成线索并附带证据链与"人话"判定理由。
|
||||||
|
2. THE 线索引擎 SHALL 为每条线索标注所属风险域、审计场景与置信度等级。
|
||||||
|
3. THE 平台 SHALL 将线索推送至对应审计员的工作台/看板。
|
||||||
|
4. THE 线索引擎 SHALL 对线索按价值/风险排序,使审计员可优先处理高价值线索。
|
||||||
|
|
||||||
|
### 需求 8:场景一 · 政企收入全链路穿透
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别政企收入中的拆单规避与虚假回款,以便发现规避审批和长期挂账的异常。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 沿"立项→审批→报价→签约→开票→回款"链路对政企合同做全链路穿透。
|
||||||
|
2. IF 多个合同金额集中分布在审批阈值边缘(如阈值以下),THEN THE 平台 SHALL 识别为疑似拆单并生成线索。
|
||||||
|
3. THE 平台 SHALL 通过工商关联穿透识别隐性实控人(如注册地址、法人亲属、付款账户同源)。
|
||||||
|
4. WHEN 出现批量回款违约或长期尾款挂账时,THE 平台 SHALL 通过回款时序聚类识别异常并生成线索。
|
||||||
|
5. THE 平台 SHALL 支持一键生成《政企客户回款异常专项线索清单》。
|
||||||
|
|
||||||
|
### 需求 9:场景二 · 市场业务真实性(养卡骗补)
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别"骗补后弃养"的周期性造假,以便发现脉冲式新增加规律性退订的套补模式。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 对用户生命周期进行时序模式识别,识别"脉冲式增长 + 规律性衰减"的周期性造假。
|
||||||
|
2. WHEN 某渠道新增用户在固定周期后集中退订时,THE 平台 SHALL 识别为疑似养卡骗补并生成线索。
|
||||||
|
3. THE 平台 SHALL 校验渠道佣金发放与业务质量(如在网时长、通话/流量活跃度)的匹配度。
|
||||||
|
4. THE 平台 SHALL 对沉默/零通话/零流量用户进行批量聚类筛查(含物联网卡虚假激活)。
|
||||||
|
5. THE 平台 SHALL 对项目交付物与收入确认进行交叉验证。
|
||||||
|
|
||||||
|
### 需求 10:场景三 · 收入与成本跨期匹配
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望发现收入确认时点与成本摊销错配的异常分录,以便纠正跨期错配。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 自动勾稽收入确认政策、实际账务与合同条款三者的一致性。
|
||||||
|
2. IF 趸交/预收款被一次性确认收入而对应成本分期摊销,THEN THE 平台 SHALL 识别为确认时点错配并生成线索。
|
||||||
|
3. THE 平台 SHALL 监控设备交付/上架与收入确认之间的时间差。
|
||||||
|
4. WHEN 按使用量计费的合同被提前全额确认收入时,THE 平台 SHALL 识别为异常并生成线索。
|
||||||
|
|
||||||
|
### 需求 11:场景四 · 渠道佣金与代理商套利
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望追踪终端流向与佣金匹配度,以便识别虚假放号、套机套卡与异地窜货套利。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 校验终端 IMEI 与用户绑定的真实性。
|
||||||
|
2. THE 平台 SHALL 校验佣金发放与用户在网时长的匹配度。
|
||||||
|
3. WHEN 终端出现"激活即沉默/流失"或跨省流通时,THE 平台 SHALL 进行 IMEI 级终端流向追踪并生成线索。
|
||||||
|
4. THE 平台 SHALL 对代理商进行业务质量时序衰减分析。
|
||||||
|
|
||||||
|
### 需求 12:场景五 · 网络建设与工程采购
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别围标串标、虚增工程量与虚假巡检,以便保障采购合规。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHEN 同一项目多家投标报价相似度过高或技术方案文件雷同度过高时,THE 平台 SHALL 进行投标关联分析并生成线索。
|
||||||
|
2. THE 平台 SHALL 验证工程量与资源消耗的匹配度。
|
||||||
|
3. THE 平台 SHALL 对巡检 GPS 轨迹与工单记录进行交叉验证,识别照片复用与坐标伪造。
|
||||||
|
4. THE 平台 SHALL 构建供应商画像并识别同一实控人的"马甲"供应商。
|
||||||
|
|
||||||
|
### 需求 13:场景六 · 互联互通与网间结算
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别话务量操纵与短信刷量,以便保障网间结算真实性。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHEN 话务量出现突发峰值或通话时长集中于整数倍时,THE 平台 SHALL 识别为疑似非真人行为并生成线索。
|
||||||
|
2. THE 平台 SHALL 将网间结算数据与网络侧原始信令进行比对。
|
||||||
|
3. THE 平台 SHALL 对 SP/CP 业务量与收入结算进行交叉验证(如短信申报量 vs 实际到达率)。
|
||||||
|
4. THE 平台 SHALL 对国际来话进行真实路由溯源。
|
||||||
|
|
||||||
|
### 需求 14:场景七 · 云业务 / IDC 与新兴业务
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别云资源"空转"确认收入与 IDC 虚租,以便发现资源闲置但确认收入的异常。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 比对云资源实际使用量(如 CPU 利用率、存储占用)与合同计费量的匹配度。
|
||||||
|
2. IF 云资源利用率长期极低但已全额确认收入,THEN THE 平台 SHALL 识别为"空转"并生成线索。
|
||||||
|
3. THE 平台 SHALL 对 IDC 出租率与电力消耗进行勾稽,识别虚租。
|
||||||
|
4. THE 平台 SHALL 对新兴业务客户进行关联方识别与预付模式异常分析。
|
||||||
|
5. THE 平台 SHALL 校验收入确认与交付验收的时序一致性。
|
||||||
|
|
||||||
|
### 需求 15:场景八 · 员工内部舞弊与资源滥用
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望识别内部号码套利、权限滥用与积分套现,以便保障内控有效性。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 对员工权限操作日志进行异常模式识别。
|
||||||
|
2. WHEN 内部测试号产生大量对外流量收入却计入内部成本时,THE 平台 SHALL 识别为用途偏离并生成线索。
|
||||||
|
3. THE 平台 SHALL 追踪积分/电子券流向,识别异常刷积分与套现(如单日发放量异常)。
|
||||||
|
4. THE 平台 SHALL 分析权限与岗位的匹配度,识别越权(如客服岗拥有财务调账权限)。
|
||||||
|
|
||||||
|
### 需求 16:审计域全景与风险分级
|
||||||
|
|
||||||
|
**用户故事:** 作为审计主管,我希望按风险域和热力图查看全局,以便明确监控优先级。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 将所有审计场景归入收入域、成本域、采购域、资金域、合规域五大风险域。
|
||||||
|
2. THE 平台 SHALL 提供风险热力图,以"发生概率 × 金额影响"两维度呈现各场景优先级。
|
||||||
|
3. WHERE 场景为高概率高金额,THE 平台 SHALL 支持将其配置为全量持续监控。
|
||||||
|
4. THE 平台 SHALL 支持按风险域、场景、地市/单位等维度筛选与下钻查看线索。
|
||||||
|
|
||||||
|
### 需求 17:人机协同闭环(线索到销项)
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望从线索到整改销项全流程在线,以便每一步都接得住、留得痕。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. WHEN 一条线索被生成时,THE 平台 SHALL 支持将其分派给指定审计员。
|
||||||
|
2. THE 平台 SHALL 支持审计员对线索进行复核研判、定性分类,并决定整改或移交。
|
||||||
|
3. WHEN 审计员完成研判时,THE 平台 SHALL 自动生成可追溯的审计底稿。
|
||||||
|
4. THE 平台 SHALL 对取证、整改跟踪、销项复核全过程留痕,形成处置闭环。
|
||||||
|
5. THE 平台 SHALL 跟踪每条线索的状态(待研判/研判中/整改中/已销项/已移交等)。
|
||||||
|
|
||||||
|
### 需求 18:误报治理与置信度分级
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望线索按置信度分流并可解释,以便不被海量假阳性淹没。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 将线索按高/中/低三级置信度分流:高置信直接推送处置、中置信人工复核、低置信归档备查。
|
||||||
|
2. THE 平台 SHALL 为每条线索提供证据链与判定理由,不得仅给出无法解释的"黑盒打分"。
|
||||||
|
3. WHEN 审计员标注线索为"误报"或"属实"时,THE 平台 SHALL 记录反馈并据此持续校准阈值与模型。
|
||||||
|
4. THE 平台 SHALL 在运营看板上公开命中率、准确率、线索转化率等指标。
|
||||||
|
|
||||||
|
### 需求 19:独立性与系统自审计
|
||||||
|
|
||||||
|
**用户故事:** 作为系统审计员,我希望审计系统本身经得起审计,以便防止放水与拦截。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 对规则配置、阈值调整全程留痕,任何改动可追溯到操作人、时间与变更内容。
|
||||||
|
2. IF 任何用户尝试删除已生成的线索,THEN THE 平台 SHALL 拒绝删除,并将线索及其处置过程完整保留。
|
||||||
|
3. THE 平台 SHALL 对"配规则、看线索、改阈值、出报告"等关键操作进行分权管理,使其相互制衡。
|
||||||
|
4. THE 平台 SHALL 对模型版本、规则版本、数据版本进行三重留痕,使任一结论可回溯到当时的模型与数据状态。
|
||||||
|
5. THE 平台 SHALL 保证被审计业务方无权配置规则、修改阈值或删除/拦截线索。
|
||||||
|
|
||||||
|
### 需求 20:应用层与自然语言交互
|
||||||
|
|
||||||
|
**用户故事:** 作为审计员,我希望零门槛使用平台,以便专注研判而非操作工具。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 提供自然语言查询、线索看板、智能报告、预警推送四类应用入口。
|
||||||
|
2. WHEN 审计员发起自然语言查询时,THE 平台 SHALL 返回结果且无需用户编写 SQL。
|
||||||
|
3. THE 平台 SHALL 支持一键生成结构化审计报告与专项线索清单。
|
||||||
|
4. WHEN 触发高置信预警时,THE 平台 SHALL 主动向相关审计员推送通知。
|
||||||
|
|
||||||
|
### 需求 21:成效度量与价值测算
|
||||||
|
|
||||||
|
**用户故事:** 作为审计主管,我希望量化平台成效,以便向上汇报价值并指导优化。
|
||||||
|
|
||||||
|
#### 验收标准
|
||||||
|
1. THE 平台 SHALL 统计可挽回收入/止损金额、线索数量、线索转化率等成效指标。
|
||||||
|
2. THE 平台 SHALL 支持历史数据全量重跑,并与既有审计结论进行同台盲测对比。
|
||||||
|
3. WHEN 盲测完成时,THE 平台 SHALL 输出成效报告,呈现此前抽样漏掉而本平台发现的线索。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 非功能性需求
|
||||||
|
|
||||||
|
### 5.1 安全与合规(最高优先级)
|
||||||
|
1. THE 平台 SHALL 保证模型、数据、推理、结果全链路在本地内网闭环,敏感数据一比特不出机房。
|
||||||
|
2. THE 平台 SHALL 满足国资/运营商/等保的合规要求,提供权限分级与不可篡改的操作日志。
|
||||||
|
3. THE 平台 SHALL 对所有用户操作生成不可篡改的审计轨迹(含操作人、时间、对象、动作)。
|
||||||
|
4. THE 平台 SHALL 对敏感数据(政企合同、用户隐私、财务凭证)进行访问控制与脱敏管理。
|
||||||
|
|
||||||
|
### 5.2 性能与时效
|
||||||
|
1. THE 平台 SHALL 支撑 70B 级大模型在本地 GPU 集群(A100 / H100 / 国产 GPU)上的推理。
|
||||||
|
2. WHEN 审计员发起常规自然语言查询时,THE 平台 SHALL 在可接受响应时延内返回结果(目标:秒级,复杂全量任务可异步)。
|
||||||
|
3. THE 平台 SHALL 支持 150 亿级业务规模的全量数据扫描,长耗时任务以异步任务方式执行并反馈进度。
|
||||||
|
|
||||||
|
### 5.3 可用性与易用性
|
||||||
|
1. THE 平台 SHALL 面向审计人员提供零门槛(无需写 SQL/不翻 Excel)的交互方式。
|
||||||
|
2. THE 平台 SHALL 支持 7×24 常态化运行与监控。
|
||||||
|
|
||||||
|
### 5.4 可扩展性
|
||||||
|
1. THE 平台 SHALL 支持新增数据源、新增审计场景与新增规则而无需重构核心架构。
|
||||||
|
2. THE 平台 SHALL 支持模型替换/升级(如更换或升级本地 LLM)。
|
||||||
|
|
||||||
|
### 5.5 可追溯性与可解释性
|
||||||
|
1. THE 平台 SHALL 保证任一线索/结论可回溯到产生它的模型版本、规则版本与数据版本。
|
||||||
|
2. THE 平台 SHALL 保证每条线索均附带可读的证据链与判定理由。
|
||||||
|
|
||||||
|
### 5.6 信创适配
|
||||||
|
1. THE 平台 SHALL 可适配国产 GPU 与信创软硬件环境。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 关键约束与假设
|
||||||
|
|
||||||
|
### 6.1 约束
|
||||||
|
- **数据零出域**:所有数据处理、模型推理必须在本地内网完成,禁止任何敏感数据外传(红线约束)。
|
||||||
|
- **独立性**:本平台为独立内部审计系统,与被审计业务方解耦,业务方不得配置规则、改阈值或删除线索。
|
||||||
|
- **数据底座物理隔离**:审计数据中台为审计专用、与业务系统物理隔离,由审计独立掌控;不做成与业务方共享的全行级中台,以规避独立性风险。
|
||||||
|
- **本地算力依赖**:依赖本地 GPU 集群承载 70B 级模型推理。
|
||||||
|
- **数据接入依赖**:全量审计能力依赖 BSS/OSS/ERP/财务/合同/工单/信令等系统的数据可接入与数据质量。
|
||||||
|
- **数据治理前置**:数据接入与治理是工作量最大、需提前立项的一环,是全量穿透的前提。
|
||||||
|
|
||||||
|
### 6.2 假设
|
||||||
|
- 本地机房具备部署 70B 级模型所需的 GPU 算力与存储资源(或在项目内规划到位)。
|
||||||
|
- 各业务系统可提供数据接口/数据库访问/文件导出之一作为接入方式。
|
||||||
|
- 存在过去 2–3 年的历史审计数据与结论,可用于场景微调与同台盲测。
|
||||||
|
- 初期可使用脱敏/样例数据进行开发、演示与盲测验证,再逐步对接真实生产数据。
|
||||||
|
|
||||||
|
### 6.3 范围边界(本期需求暂不展开,留待 PRD/分期确定)
|
||||||
|
- 具体的开发优先级与分期(MVP / 二期 / 三期)。
|
||||||
|
- 具体技术栈选型(后端框架、前端框架、数据库、向量库、推理框架等)。
|
||||||
|
- 具体的模型选型与微调方案细节。
|
||||||
|
- 与各业务系统对接的具体接口规格。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 需求清单索引
|
||||||
|
|
||||||
|
| 编号 | 需求名称 | 风险域/分类 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| R1 | 多源异构数据接入 | 数据中台 |
|
||||||
|
| R2 | 审计数据中台·本体层与主数据对齐 | 数据中台 |
|
||||||
|
| R3 | 审计数据中台·时态层与增量同步 | 数据中台 |
|
||||||
|
| R4 | 本地私有化 LLM 引擎 | 核心引擎 |
|
||||||
|
| R5 | 全量穿透引擎 | 核心引擎 |
|
||||||
|
| R6 | 规则进化引擎 | 核心引擎 |
|
||||||
|
| R7 | 线索驱动引擎 | 核心引擎 |
|
||||||
|
| R8 | 场景一·政企收入全链路穿透 | 收入域 |
|
||||||
|
| R9 | 场景二·市场业务真实性(养卡骗补) | 成本域 |
|
||||||
|
| R10 | 场景三·收入与成本跨期匹配 | 收入域 |
|
||||||
|
| R11 | 场景四·渠道佣金与代理商套利 | 成本域 |
|
||||||
|
| R12 | 场景五·网络建设与工程采购 | 采购域 |
|
||||||
|
| R13 | 场景六·互联互通与网间结算 | 资金域 |
|
||||||
|
| R14 | 场景七·云业务/IDC与新兴业务 | 收入域 |
|
||||||
|
| R15 | 场景八·员工内部舞弊与资源滥用 | 合规域 |
|
||||||
|
| R16 | 审计域全景与风险分级 | 全局 |
|
||||||
|
| R17 | 人机协同闭环(线索到销项) | 闭环 |
|
||||||
|
| R18 | 误报治理与置信度分级 | 闭环 |
|
||||||
|
| R19 | 独立性与系统自审计 | 合规/制度 |
|
||||||
|
| R20 | 应用层与自然语言交互 | 应用层 |
|
||||||
|
| R21 | 成效度量与价值测算 | 价值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **请检查确认本需求文档。** 确认通过后,我将进入下一阶段,基于本文档生成产品需求文档 `1-prd-AIAudit.md`(含产品定位、成功指标、用户画像与场景、MoSCoW 功能优先级、关键流程、权限矩阵、版本规划、风险等)。如需修改,请直接告诉我要调整的部分。
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# 1-prd-AIAudit · 产品需求文档(PRD)
|
||||||
|
|
||||||
|
> 项目:基于本地私有化大模型的电信运营商 AI 全域内审平台(AIAudit)
|
||||||
|
> 版本:v0.1(待评审)
|
||||||
|
> 日期:2026-06
|
||||||
|
> 上游来源:`0-req-AIAudit.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 产品概述与定位
|
||||||
|
|
||||||
|
### 1.1 一句话定位
|
||||||
|
**AIAudit 是一套建在运营商自己机房、数据零出域、覆盖全业务域、越用越聪明的本地 AI 内审能力体系**——不是一套工具,而是一套可持续进化、归审计独立掌控的审计大脑。
|
||||||
|
|
||||||
|
### 1.2 产品形态
|
||||||
|
- 部署形态:本地私有化部署(内网闭环,数据一比特不出机房)。
|
||||||
|
- 能力构成:审计专用数据中台(底座)+ 四大引擎(LLM / 全量穿透 / 规则进化 / 线索驱动)+ 人机协同闭环(线索到销项)+ 系统自审计(独立可信)。
|
||||||
|
- 交付物:本地 AI 审计平台 + 可进化规则库 + 已验证高价值线索 + 同台盲测成效报告。
|
||||||
|
|
||||||
|
### 1.3 与现有方式的差异化
|
||||||
|
| 维度 | 传统抽样审计 | 公有云 AI 审计 | AIAudit(本地) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 数据范围 | 按金额抽样,查不全 | 全量但数据出域 | 全量且数据不出机房 |
|
||||||
|
| 合规风险 | 低但能力弱 | 能力强但合规风险高 | 私有化、合规可控 |
|
||||||
|
| 响应效率 | Excel 翻表 | 实时但依赖外网 | 内网闭环秒级响应 |
|
||||||
|
| 能力归属 | 经验在人脑 | 能力外部租用 | 本地永久沉淀,越用越聪明 |
|
||||||
|
| 独立性 | 依赖人工 | 数据送人 | 审计独立掌控、自身可审计 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标与成功指标
|
||||||
|
|
||||||
|
### 2.1 业务目标
|
||||||
|
- 把审计覆盖率从约 5% 提升到接近 100%(全量穿透)。
|
||||||
|
- 把审计节奏从年度快照升级为 7×24 常态化监控。
|
||||||
|
- 把审计经验固化为机构永久资产(可进化规则库)。
|
||||||
|
- 数据出域风险归零,满足国资/运营商/等保最严要求。
|
||||||
|
|
||||||
|
### 2.2 成功指标(KPI)
|
||||||
|
| 指标 | 目标(首期/稳态) | 对应需求 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 审计覆盖率 | ≥ 95%(全量扫描) | R5 |
|
||||||
|
| 数据出域事件 | 0 起 | 非功能 5.1、R1 |
|
||||||
|
| 同台盲测命中率 | 复现既有审计结论 + 发现新增真实线索 | R21 |
|
||||||
|
| 线索准确率(高置信) | 随反馈学习持续提升 | R18 |
|
||||||
|
| 线索转化率(线索→属实立案) | 可量化、上看板 | R18、R21 |
|
||||||
|
| 首批线索产出 | 投产首月 200–500 条 | R7 |
|
||||||
|
| 可挽回收入/止损 | 年化数千万级(保守) | R21 |
|
||||||
|
| 查询响应 | 常规查询秒级,全量任务异步反馈进度 | 非功能 5.2 |
|
||||||
|
|
||||||
|
### 2.3 非目标(本产品不做)
|
||||||
|
- 不替代业务系统本身的生产功能。
|
||||||
|
- 不做面向业务方共享的全行级数据中台。
|
||||||
|
- 不做需要数据出域的任何云端推理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 用户画像与核心场景(痛点解法)
|
||||||
|
|
||||||
|
### 3.1 用户画像
|
||||||
|
| 角色 | 画像 | 关键诉求 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 审计员 | 一线内审,业务熟但不写代码 | 看懂线索、查到证据、处置留痕 |
|
||||||
|
| 审计主管 | 部门负责人 | 全局掌控、成效可量化、流程合规 |
|
||||||
|
| 规则管理员 | 资深审计/规则专家 | 自然语言配规则、沙箱验证、版本可控 |
|
||||||
|
| 系统管理员 | IT 运维 | 接入稳定、权限可控、运行可观测 |
|
||||||
|
| 系统审计员 | 独立监督岗 | 改动可追溯、线索不可被删被拦 |
|
||||||
|
|
||||||
|
### 3.2 核心场景与痛点解法
|
||||||
|
| 场景 | 痛点 | AIAudit 解法 | 对应需求 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 政企拆单规避 | 大额拆小额规避三重一大,抽样避开 | 金额阈值边缘分布识别 + 工商关联穿透 + 回款时序聚类 | R8 |
|
||||||
|
| 养卡骗补 | 脉冲新增+规律退订,藏在时序里 | 用户生命周期时序模式识别 + 佣金质量匹配 | R9 |
|
||||||
|
| 收入成本跨期错配 | 趸交一次性确认、成本分摊错配 | 政策/账务/合同三方勾稽 + 时点错配识别 | R10 |
|
||||||
|
| 渠道套利套机 | 虚假放号、套卡、异地窜货 | IMEI 级流向追踪 + 佣金在网时长匹配 | R11 |
|
||||||
|
| 围标串标 | 报价雷同、马甲供应商、虚增工程量 | 投标关联分析 + 巡检轨迹交叉验证 + 供应商画像 | R12 |
|
||||||
|
| 网间结算刷量 | 话务/短信刷量套结算 | 整数时长识别 + 信令比对 + 到达率交叉验证 | R13 |
|
||||||
|
| 云空转/IDC虚租 | 资源闲置却全额确认收入 | 利用率vs计费量比对 + 电力勾稽 + 关联方识别 | R14 |
|
||||||
|
| 内部舞弊 | 内部号套利、越权、积分套现 | 操作日志异常 + 权限岗位匹配 + 积分流向追踪 | R15 |
|
||||||
|
|
||||||
|
> 共性痛点:"数据涉密不能出域 + 海量单据查不过来 + 时序造假抽样抓不到"。共性解法:"本地 LLM + 审计数据中台全量穿透 + 规则进化 + 人机闭环"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 功能清单与优先级(MoSCoW)
|
||||||
|
|
||||||
|
> 优先级:Must(一期 MVP 必须)/ Should(二期)/ Could(三期)/ Won't(暂不做)。映射回 `0-req-AIAudit.md` 需求编号。
|
||||||
|
|
||||||
|
### 4.1 数据中台与底座
|
||||||
|
| 功能 | 优先级 | 需求映射 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 多源异构数据接入(接口/库/文件) | Must | R1 |
|
||||||
|
| 审计专用数据底座(物理隔离、独立掌控) | Must | R2 |
|
||||||
|
| 本体建模与审计知识图谱 | Must | R2 |
|
||||||
|
| 主数据对齐与数据清洗/质量评分 | Must | R2 |
|
||||||
|
| 双时态/时序建模与版本回溯 | Must | R3 |
|
||||||
|
| 增量同步与常态化重算 | Should | R3 |
|
||||||
|
| 统一穿透/图谱查询服务 | Must | R2 |
|
||||||
|
|
||||||
|
### 4.2 核心引擎
|
||||||
|
| 功能 | 优先级 | 需求映射 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 本地私有化 LLM 部署与推理 | Must | R4 |
|
||||||
|
| 自然语言查数(不写 SQL) | Must | R4、R20 |
|
||||||
|
| 全量穿透扫描引擎 | Must | R5 |
|
||||||
|
| 跨系统关联穿透 | Must | R5、R2 |
|
||||||
|
| 规则进化引擎(NL→规则) | Should | R6 |
|
||||||
|
| 规则沙箱验证与版本管理 | Should | R6 |
|
||||||
|
| 线索生成 + 证据链 + 人话解释 | Must | R7 |
|
||||||
|
| 线索价值排序与推送 | Must | R7、R20 |
|
||||||
|
|
||||||
|
### 4.3 审计场景
|
||||||
|
| 功能 | 优先级 | 需求映射 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 场景一 政企收入全链路穿透 | Must | R8 |
|
||||||
|
| 场景二 养卡骗补识别 | Must | R9 |
|
||||||
|
| 场景三 收入成本跨期匹配 | Should | R10 |
|
||||||
|
| 场景四 渠道佣金与套利 | Should | R11 |
|
||||||
|
| 场景五 网络建设与工程采购 | Could | R12 |
|
||||||
|
| 场景六 互联互通与网间结算 | Could | R13 |
|
||||||
|
| 场景七 云业务/IDC | Could | R14 |
|
||||||
|
| 场景八 员工内部舞弊 | Should | R15 |
|
||||||
|
|
||||||
|
> 一期场景优先级建议:选取"高概率×高金额"的政企拆单(R8)与养卡骗补(R9)作为 MVP 跑通,其余按风险热力图分期接入。最终优先级以评审为准。
|
||||||
|
|
||||||
|
### 4.4 闭环、治理与应用
|
||||||
|
| 功能 | 优先级 | 需求映射 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 风险域全景与热力图 | Should | R16 |
|
||||||
|
| 线索分派→研判→定性→整改→销项闭环 | Must | R17 |
|
||||||
|
| 审计底稿自动生成 | Should | R17 |
|
||||||
|
| 置信度三级分流 | Must | R18 |
|
||||||
|
| 误报反馈学习闭环 | Should | R18 |
|
||||||
|
| 运营指标看板(命中率/准确率/转化率) | Should | R18、R21 |
|
||||||
|
| 系统自审计(留痕/分权/三重版本/线索不可删) | Must | R19 |
|
||||||
|
| 线索看板/智能报告/预警推送 | Must | R20 |
|
||||||
|
| 成效度量与同台盲测报告 | Should | R21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键流程
|
||||||
|
|
||||||
|
### 5.1 数据流(从接入到可分析)
|
||||||
|
```
|
||||||
|
源系统(BSS/OSS/ERP/财务/合同/工单/信令)
|
||||||
|
→ 接入适配(接口/库/文件)
|
||||||
|
→ 审计数据中台:清洗/质量评分 → 本体建模/主数据对齐(知识图谱) → 双时态/时序建模
|
||||||
|
→ 统一穿透查询服务
|
||||||
|
→ 全量穿透引擎 + 本地 LLM
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 审计闭环(从线索到销项)
|
||||||
|
```
|
||||||
|
全量扫描/规则命中 → 生成线索+证据链+判定理由(置信度分级)
|
||||||
|
→ 推送/分派审计员 → 复核研判 → 定性分类
|
||||||
|
→ 自动生成审计底稿 → 整改 或 移交
|
||||||
|
→ 销项复核闭环(全程留痕)
|
||||||
|
→ 审计员反馈(误报/属实) → 阈值与模型校准
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 规则进化流
|
||||||
|
```
|
||||||
|
审计员用自然语言描述新造假模式
|
||||||
|
→ LLM 转化为可执行规则
|
||||||
|
→ 沙箱用历史数据验证命中率
|
||||||
|
→ 评审通过 → 投入生产(版本留痕)
|
||||||
|
→ 反馈迭代优化 → 沉淀进规则库
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 角色权限矩阵
|
||||||
|
|
||||||
|
> ✅ 允许 🔶 限本人/受限 ❌ 禁止。体现独立性与分权制衡(R19)。
|
||||||
|
|
||||||
|
| 功能 / 角色 | 审计员 | 审计主管 | 规则管理员 | 系统管理员 | 系统审计员 | 业务方 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 自然语言查询 | ✅ | ✅ | ✅ | 🔶 | ✅ | ❌ |
|
||||||
|
| 查看线索 | 🔶本人 | ✅全部 | ✅ | ❌ | ✅全部 | ❌ |
|
||||||
|
| 研判/定性线索 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| 分派线索 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| 删除线索 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| 配置/修改规则 | ❌ | 🔶审批 | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| 调整阈值 | ❌ | 🔶审批 | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| 出具报告 | ✅ | ✅ | ❌ | ❌ | 🔶审计报告 | ❌ |
|
||||||
|
| 数据接入配置 | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||||
|
| 权限分配 | ❌ | 🔶 | ❌ | ✅ | ❌ | ❌ |
|
||||||
|
| 查看自审计轨迹 | ❌ | 🔶 | ❌ | 🔶 | ✅ | ❌ |
|
||||||
|
| 模型部署/升级 | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
> 关键约束:任何角色均不能删除已生成线索;规则/阈值变动需审批且全程留痕;业务方对系统无任何写权限。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 版本规划
|
||||||
|
|
||||||
|
### 一期 · MVP(约 3 个月,对标蓝图部署节奏)
|
||||||
|
- 第 1 月:本地算力+模型部署;数据接入;搭建审计数据中台(接入+本体+时态最小集)。
|
||||||
|
- 第 2 月:场景微调;政企拆单(R8)+ 养卡骗补(R9)跑通;历史数据全量重跑同台盲测。
|
||||||
|
- 第 3 月:投产;产出首批 200–500 条线索;人机闭环 + 系统自审计上线;规则库首轮进化。
|
||||||
|
- 范围:R1-R5、R7、R8、R9、R17、R18(基础)、R19、R20。
|
||||||
|
|
||||||
|
### 二期 · 能力扩展
|
||||||
|
- 规则进化引擎完整化(R6);新增场景 R10/R11/R15;风险热力图(R16);误报反馈学习(R18);运营看板与盲测报告(R21)。
|
||||||
|
|
||||||
|
### 三期 · 全域覆盖
|
||||||
|
- 接入场景 R12/R13/R14;增量近实时常态化(R3 完整);信创适配深化;规则库规模化沉淀。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 非功能性要求(摘自需求并细化)
|
||||||
|
- **安全合规(红线)**:全链路内网闭环,数据零出域;不可篡改操作日志;敏感数据访问控制与脱敏。(R5.1)
|
||||||
|
- **性能时效**:支撑 70B 级本地推理;常规查询秒级;150 亿级全量扫描异步执行并反馈进度。(R5.2)
|
||||||
|
- **易用性**:审计员零门槛,自然语言交互,无需写 SQL。(R5.3)
|
||||||
|
- **可扩展**:新增数据源/场景/规则不重构核心;模型可替换升级。(R5.4)
|
||||||
|
- **可追溯可解释**:结论可回溯到模型/规则/数据三重版本;线索均附证据链与理由。(R5.5)
|
||||||
|
- **信创适配**:可适配国产 GPU 与信创软硬件。(R5.6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 依赖与风险
|
||||||
|
|
||||||
|
### 9.1 依赖
|
||||||
|
- 本地 GPU 算力(A100/H100/国产 GPU)到位。
|
||||||
|
- 各业务系统可提供接口/库访问/文件导出之一。
|
||||||
|
- 过去 2–3 年历史审计数据与结论可用于微调与盲测。
|
||||||
|
- 初期可用脱敏/样例数据开发与演示。
|
||||||
|
|
||||||
|
### 9.2 风险与应对
|
||||||
|
| 风险 | 影响 | 应对 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 数据治理工作量被低估 | 拖累全量穿透落地 | 数据中台提前立项、独立排期、最小集先行 |
|
||||||
|
| 主数据对不齐 | 关联穿透失效 | 本体先行、对齐失败显式标记人工干预 |
|
||||||
|
| 误报过多 | 审计员被淹没 | 置信度三级分流 + 反馈学习 + 可解释证据链 |
|
||||||
|
| 模型幻觉/误判 | 线索不可信 | 证据链强制、沙箱验证、人工复核闭环 |
|
||||||
|
| 算力不足 | 推理性能不达标 | 异步任务、分级调度、信创适配评估 |
|
||||||
|
| 独立性被破坏 | 放水/拦截 | 分权制衡、线索不可删、三重版本留痕、系统自审计 |
|
||||||
|
| 数据出域 | 合规红线事故 | 内网闭环架构、出域阻断、全链路留痕 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **请检查确认本 PRD。** 确认通过后,我将进入下一阶段,基于本文档生成开发任务文档 `2-task-AIAudit.md`(可勾选任务清单,标注目标、对应需求/PRD、验收标准、依赖与优先级/阶段)。如需修改,请直接告诉我要调整的部分。
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
# 2-task-AIAudit · 开发任务文档
|
||||||
|
|
||||||
|
> 项目:基于本地私有化大模型的电信运营商 AI 全域内审平台(AIAudit)
|
||||||
|
> 版本:v0.1(待评审)
|
||||||
|
> 日期:2026-06
|
||||||
|
> 上游来源:`0-req-AIAudit.md`、`1-prd-AIAudit.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
- 任务以可勾选清单组织:`- [ ]` 未开始 / `- [x]` 已完成 / `- [~]` 进行中。
|
||||||
|
- 编号规则:`P{阶段}.{模块}.{任务}`,子任务再加一级。
|
||||||
|
- 每个任务标注:**目标**、**映射**(需求 R / PRD 功能)、**验收标准(DoD)**、**依赖**。
|
||||||
|
- 阶段:**MVP**(一期)/ **二期** / **三期**。优先级沿用 PRD 的 MoSCoW。
|
||||||
|
- 开发过程中本文档持续更新(勾选、记录变更、补充新任务)。
|
||||||
|
|
||||||
|
### 进度总览
|
||||||
|
| 阶段 | 模块数 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| P0 项目基建与选型 | 4 | 进行中(P0.1 完成,P0.2/P0.3 部分) |
|
||||||
|
| P1 MVP(数据中台+引擎+R8/R9+闭环+自审计+看板+盲测) | 11 | 进行中(P1.2 大部完成,P1.3 部分) |
|
||||||
|
| P2 二期(规则进化+R10/R11/R15+热力图+反馈学习+运营看板) | 6 | 未开始 |
|
||||||
|
| P3 三期(R12/R13/R14+近实时+信创+规则库规模化) | 5 | 未开始 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# P0 · 项目基建与技术选型(MVP 前置)
|
||||||
|
|
||||||
|
## P0.1 技术选型与架构基线
|
||||||
|
> 目标:确定技术栈与总体架构,形成可执行的工程基线。映射:PRD §1.2、非功能全量。依赖:无。
|
||||||
|
|
||||||
|
- [x] P0.1.1 召开技术选型评审,确定后端框架、前端框架、关系库、图数据库、时序/双时态存储、向量库、LLM 推理框架、任务调度组件
|
||||||
|
- 验收:产出《技术选型决策记录(ADR)》,每项含选型理由与备选;信创适配可行性结论
|
||||||
|
- 完成:见 `docs/adr/ADR-0001-tech-stack.md`
|
||||||
|
- [ ] P0.1.2 绘制总体架构图(应用层/引擎层/数据中台层/模型层/算力层/安全自审计层)
|
||||||
|
- 验收:架构图 + 组件职责说明评审通过,明确内网闭环边界与数据零出域边界
|
||||||
|
- [x] P0.1.3 定义模块划分与代码仓库结构(monorepo 或多仓)、分支与发布策略
|
||||||
|
- 验收:仓库初始化完成,README 含目录约定与协作规范
|
||||||
|
- 完成:monorepo 结构(backend/frontend/infra/docs),`README.md`、`.gitignore` 已建
|
||||||
|
- [x] P0.1.4 定义数据零出域的网络与部署约束基线(内网隔离、出域阻断、无外网依赖清单)
|
||||||
|
- 验收:约束清单评审通过,CI 中加入"禁止外网依赖"检查项
|
||||||
|
- 完成:ADR 定义红线;代码层 prod 禁用公网 Provider(`app/config.py`、`app/llm/factory.py`),已有测试覆盖;CI 校验待 P0.3.2
|
||||||
|
|
||||||
|
## P0.2 开发与运行环境
|
||||||
|
> 目标:搭建可复现的开发/测试环境。映射:非功能 5.1/5.2。依赖:P0.1。
|
||||||
|
|
||||||
|
- [ ] P0.2.1 本地化依赖与离线包源(内网制品库/镜像),避免外网拉取
|
||||||
|
- 验收:在隔离网络中可完成依赖安装与构建
|
||||||
|
- 进展:已配置清华镜像源加速开发期安装(`~/.config/pip/pip.conf`);内网制品库待部署
|
||||||
|
- [x] P0.2.2 容器化与编排(开发/测试环境一键拉起)→ 调整为本地安装
|
||||||
|
- 验收:本地 PostgreSQL 16 可用,初始化脚本可建库建扩展
|
||||||
|
- 完成:**弃用 Docker**(已删除 docker-compose/Dockerfile)。本机 Homebrew 安装 PostgreSQL 16.14 + pgvector 0.8 + btree_gist;`infra/postgres/setup_local.sh` 建库建扩展通过。TimescaleDB 因 macOS 编译问题本地跳过(迁移条件执行,生产 Linux 启用),见 ADR-0002
|
||||||
|
- [ ] P0.2.3 GPU 推理环境准备(驱动、推理框架、显存配置)
|
||||||
|
- 验收:可在本地 GPU 上加载一个基线模型并完成一次推理
|
||||||
|
|
||||||
|
## P0.3 工程质量基线
|
||||||
|
> 目标:建立测试、CI、代码规范。映射:阶段 5 通用约束。依赖:P0.1。
|
||||||
|
|
||||||
|
- [x] P0.3.1 单元/集成测试框架与覆盖率门槛
|
||||||
|
- 验收:示例测试可运行,CI 跑测试并产出覆盖率报告
|
||||||
|
- 完成:pytest 接入,7 个测试通过(含数据零出域红线测试);覆盖率门槛与 CI 集成待 P0.3.2
|
||||||
|
- [ ] P0.3.2 CI 流水线(构建+测试+静态检查+无外网依赖校验)
|
||||||
|
- 验收:提交触发流水线,失败可阻断合并
|
||||||
|
- 进展:ruff 已接入并通过(`pyproject.toml`),CI 流水线脚本待编写
|
||||||
|
- [ ] P0.3.3 代码规范、提交规范与 Lint
|
||||||
|
- 验收:Lint 接入 CI,违规阻断
|
||||||
|
|
||||||
|
## P0.4 安全与权限基线(贯穿)
|
||||||
|
> 目标:先立"独立性"地基。映射:R19、非功能 5.1。依赖:P0.1。
|
||||||
|
|
||||||
|
- [ ] P0.4.1 统一鉴权与 RBAC 模型设计(角色:审计员/主管/规则管理员/系统管理员/系统审计员/业务方)
|
||||||
|
- 验收:RBAC 模型评审通过,覆盖 PRD §6 权限矩阵
|
||||||
|
- [ ] P0.4.2 不可篡改操作日志(审计轨迹)基础设施
|
||||||
|
- 验收:任意关键操作落不可篡改日志,含操作人/时间/对象/动作,可查询
|
||||||
|
- [ ] P0.4.3 敏感数据访问控制与脱敏策略
|
||||||
|
- 验收:敏感字段访问受控并可脱敏展示,越权访问被拒并留痕
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# P1 · MVP(一期)
|
||||||
|
|
||||||
|
## P1.1 审计数据中台 · 接入层
|
||||||
|
> 目标:把多源异构数据汇入审计专用底座。映射:R1 / PRD §4.1。依赖:P0。
|
||||||
|
|
||||||
|
- [ ] P1.1.1 接入适配框架(接口/数据库/文件三类),插件式数据源注册
|
||||||
|
- 验收:可注册一个新数据源并完成一次导入,不改源系统
|
||||||
|
- [ ] P1.1.2 数据源接入配置(连接、字段映射、同步周期)管理界面/配置
|
||||||
|
- 验收:通过配置即可接入,无需改代码;配置项可校验
|
||||||
|
- [ ] P1.1.3 全量初始化导入 + 失败告警与隔离
|
||||||
|
- 验收:单源失败记录原因并告警,不影响其他源;可重试
|
||||||
|
- [ ] P1.1.4 接入侧数据零出域校验
|
||||||
|
- 验收:接入过程无任何外网传输,校验项纳入测试
|
||||||
|
- [ ] P1.1.5 样例/脱敏数据集导入(用于开发与盲测)
|
||||||
|
- 验收:可一键导入样例数据,覆盖 R8/R9 所需字段
|
||||||
|
|
||||||
|
## P1.2 审计数据中台 · 本体层与知识图谱
|
||||||
|
> 目标:按审计本体组织实体与关系。映射:R2 / PRD §4.1。依赖:P1.1。
|
||||||
|
|
||||||
|
- [x] P1.2.1 审计本体(Ontology)定义(客户/合同/号码/IMEI/账户/工单/供应商/结算单及关系)
|
||||||
|
- 验收:本体 schema 评审通过,含实体属性与关系定义
|
||||||
|
- 完成:`app/datahub/ontology.py`(12 实体类型 + 12 关系类型 + 本体域约束),见 ADR-0002;单元测试覆盖
|
||||||
|
- [x] P1.2.2 知识图谱存储与建模落地
|
||||||
|
- 验收:实体与关系写入图存储,可做多跳关联查询
|
||||||
|
- 完成:关系表 `entity`/`entity_relationship` + 递归 CTE 多跳穿透(`app/datahub/graph_repo.py`),集成测试验证"实控人识别"
|
||||||
|
- [x] P1.2.3 主数据对齐(跨系统实体统一识别与关联)
|
||||||
|
- 验收:对齐结果可验证(如同一实控人聚合),对齐失败显式标记
|
||||||
|
- 完成:`entity` 以 (类型,业务主键) 幂等归一 + `canonical_id` 归并锚点;穿透识别同一实控人已测
|
||||||
|
- [ ] P1.2.4 数据清洗与质量评分(缺失/重复/口径不一致探查)
|
||||||
|
- 验收:质量评分可在界面查看;关键字段缺失/无法对齐标记人工干预而非丢弃
|
||||||
|
- [x] P1.2.5 统一穿透/图谱查询服务(对引擎与场景提供共同入口)
|
||||||
|
- 验收:提供统一查询 API,支持关联穿透;返回结果可解释来源
|
||||||
|
- 完成:`app/api/datahub.py`(`POST /datahub/penetrate` 多跳穿透 + `GET /datahub/entities/{id}`),返回带最短跳数的关联实体;API 集成测试通过
|
||||||
|
|
||||||
|
## P1.3 审计数据中台 · 时态层
|
||||||
|
> 目标:原生支持时间维度与版本回溯。映射:R3 / PRD §4.1。依赖:P1.1。
|
||||||
|
|
||||||
|
- [x] P1.3.1 双时态建模(业务发生时间 + 系统记录时间)
|
||||||
|
- 验收:可按任意历史时点回放数据状态,回放结果正确
|
||||||
|
- 完成:`BitemporalFact` + `bitemporal_repo.as_of()` 回放;`btree_gist` 排他约束防有效期重叠;集成测试通过
|
||||||
|
- [ ] P1.3.2 关键对象时间序列建模(用户生命周期/回款/话务/佣金/资源使用)
|
||||||
|
- 验收:可按对象取出有序时间序列,供时序分析
|
||||||
|
- 进展:`metric_event` 表已建(生产转 TimescaleDB 超表),时序查询封装待补
|
||||||
|
- [ ] P1.3.3 增量同步与数据版本记录
|
||||||
|
- 验收:每次同步记录时间戳/数据量/数据版本;结论可回溯到数据版本
|
||||||
|
- 进展:`data_version` 表已建并被各表外键引用,同步流程待实现
|
||||||
|
- [ ] P1.3.4 常态化重算触发(增量到达触发相关规则重算)
|
||||||
|
- 验收:增量到达后相关场景结果自动更新
|
||||||
|
|
||||||
|
## P1.4 本地私有化 LLM 引擎
|
||||||
|
> 目标:本地部署模型并支持自然语言能力。映射:R4 / PRD §4.2。依赖:P0.2.3。
|
||||||
|
|
||||||
|
- [ ] P1.4.1 本地模型部署(千问 70B / DeepSeek 之一)与推理服务封装
|
||||||
|
- 验收:内网可用、推理不依赖外网;提供统一推理 API
|
||||||
|
- [ ] P1.4.2 自然语言查数(NL→查询)能力,对接统一穿透查询服务
|
||||||
|
- 验收:审计员自然语言提问返回结构化结果,无需写 SQL
|
||||||
|
- [ ] P1.4.3 异常模式推理、报告生成、线索解释能力接入
|
||||||
|
- 验收:能对给定异常聚类输出"人话"解释与结构化报告
|
||||||
|
- [ ] P1.4.4 模型版本记录与结论可回溯
|
||||||
|
- 验收:每条结论可回溯到模型版本
|
||||||
|
- [ ] P1.4.5 LLM 输出防幻觉约束(强制附证据/可溯源,不可编造数据)
|
||||||
|
- 验收:无证据支撑的结论被拦截或标注低置信
|
||||||
|
|
||||||
|
## P1.5 全量穿透引擎
|
||||||
|
> 目标:全量扫描与跨系统关联穿透。映射:R5 / PRD §4.2。依赖:P1.2、P1.3。
|
||||||
|
|
||||||
|
- [ ] P1.5.1 全量扫描任务框架(异步任务、进度反馈、可中断)
|
||||||
|
- 验收:长耗时全量任务异步执行并反馈进度
|
||||||
|
- [ ] P1.5.2 跨系统关联穿透(合同—回款—工商—账户等)
|
||||||
|
- 验收:可输出关联路径与证据,覆盖 R8 所需穿透
|
||||||
|
- [ ] P1.5.3 扫描覆盖范围与数据量输出(证明全量性)
|
||||||
|
- 验收:任务结束输出覆盖范围与数据量统计
|
||||||
|
- [ ] P1.5.4 数据就地分析、数据不出域
|
||||||
|
- 验收:穿透过程数据不离开内网,校验纳入测试
|
||||||
|
|
||||||
|
## P1.6 线索驱动引擎
|
||||||
|
> 目标:生成线索+证据链+解释并推送。映射:R7、R18(基础) / PRD §4.2。依赖:P1.4、P1.5。
|
||||||
|
|
||||||
|
- [ ] P1.6.1 线索数据模型(风险域/场景/置信度/证据链/判定理由/状态)
|
||||||
|
- 验收:线索结构可承载证据链与状态流转
|
||||||
|
- [ ] P1.6.2 线索生成(由穿透/规则命中产出异常聚类→线索)
|
||||||
|
- 验收:异常聚类自动转为线索并附证据链与理由
|
||||||
|
- [ ] P1.6.3 置信度三级分流(高/中/低)与价值排序
|
||||||
|
- 验收:线索分级正确;高置信优先推送
|
||||||
|
- [ ] P1.6.4 线索推送至审计员工作台
|
||||||
|
- 验收:对应审计员可在工作台收到线索
|
||||||
|
|
||||||
|
## P1.7 场景一 · 政企收入全链路穿透(R8)
|
||||||
|
> 目标:识别拆单规避与虚假回款。映射:R8 / PRD §4.3 Must。依赖:P1.5、P1.6。
|
||||||
|
|
||||||
|
- [ ] P1.7.1 政企合同全链路建模(立项→审批→报价→签约→开票→回款)
|
||||||
|
- 验收:链路数据可端到端串联查询
|
||||||
|
- [ ] P1.7.2 拆单识别(金额阈值边缘分布检测)
|
||||||
|
- 验收:阈值边缘集中分布合同被识别为疑似拆单并生成线索
|
||||||
|
- [ ] P1.7.3 工商关联穿透(隐性实控人:地址/法人亲属/付款账户同源)
|
||||||
|
- 验收:同源关联客户被聚合识别,附证据
|
||||||
|
- [ ] P1.7.4 回款时序聚类(批量违约/长期挂账)
|
||||||
|
- 验收:批量违约模式被识别并生成线索
|
||||||
|
- [ ] P1.7.5 一键生成《政企客户回款异常专项线索清单》
|
||||||
|
- 验收:可导出结构化清单,含证据链
|
||||||
|
|
||||||
|
## P1.8 场景二 · 养卡骗补识别(R9)
|
||||||
|
> 目标:识别脉冲新增+规律退订的周期性造假。映射:R9 / PRD §4.3 Must。依赖:P1.3、P1.6。
|
||||||
|
|
||||||
|
- [ ] P1.8.1 用户生命周期时序模式识别(脉冲式增长+规律性衰减)
|
||||||
|
- 验收:周期性造假模式被识别并生成线索
|
||||||
|
- [ ] P1.8.2 渠道佣金与业务质量匹配(在网时长/通话/流量活跃度)
|
||||||
|
- 验收:佣金与质量不匹配渠道被标记
|
||||||
|
- [ ] P1.8.3 沉默/零通话/零流量用户批量聚类(含物联网卡虚假激活)
|
||||||
|
- 验收:批量沉默用户被聚类识别
|
||||||
|
- [ ] P1.8.4 项目交付物与收入确认交叉验证
|
||||||
|
- 验收:交付与收入不匹配项被识别
|
||||||
|
|
||||||
|
## P1.9 人机协同闭环(R17 基础)
|
||||||
|
> 目标:线索到销项全流程在线留痕。映射:R17 / PRD §4.4 Must。依赖:P1.6。
|
||||||
|
|
||||||
|
- [ ] P1.9.1 线索分派(主管→审计员)
|
||||||
|
- 验收:可分派并通知;分派留痕
|
||||||
|
- [ ] P1.9.2 复核研判与定性分类
|
||||||
|
- 验收:审计员可研判、定性,记录理由
|
||||||
|
- [ ] P1.9.3 审计底稿自动生成(可追溯)
|
||||||
|
- 验收:研判完成自动生成底稿,含证据链与版本信息
|
||||||
|
- [ ] P1.9.4 整改/移交与销项复核闭环、状态机
|
||||||
|
- 验收:线索状态全流程可跟踪,过程留痕
|
||||||
|
|
||||||
|
## P1.10 系统自审计与独立性(R19)
|
||||||
|
> 目标:让审计系统自身经得起审计。映射:R19 / PRD §4.4 Must、§6。依赖:P0.4。
|
||||||
|
|
||||||
|
- [ ] P1.10.1 规则/阈值变更全程留痕(操作人/时间/变更内容)
|
||||||
|
- 验收:任意变更可追溯
|
||||||
|
- [ ] P1.10.2 线索不可删除约束
|
||||||
|
- 验收:任何角色删除线索请求被拒并留痕
|
||||||
|
- [ ] P1.10.3 关键操作分权制衡(配规则/看线索/改阈值/出报告分离)
|
||||||
|
- 验收:越权操作被拒,符合 PRD §6 权限矩阵
|
||||||
|
- [ ] P1.10.4 模型/规则/数据三重版本留痕与回溯
|
||||||
|
- 验收:任一结论可回溯到当时的模型、规则、数据版本
|
||||||
|
|
||||||
|
## P1.11 应用层、看板与盲测验证
|
||||||
|
> 目标:审计员零门槛使用 + 盲测证明价值。映射:R20、R21、R18 / PRD §2.2、§7。依赖:P1.6-P1.10。
|
||||||
|
|
||||||
|
- [ ] P1.11.1 线索看板(按风险域/场景/置信度筛选与下钻)
|
||||||
|
- 验收:看板可筛选下钻,展示证据链
|
||||||
|
- [ ] P1.11.2 自然语言查询入口(前端)
|
||||||
|
- 验收:审计员可自然语言查询并查看结果
|
||||||
|
- [ ] P1.11.3 智能报告与专项清单导出
|
||||||
|
- 验收:可一键生成报告/清单
|
||||||
|
- [ ] P1.11.4 高置信预警推送
|
||||||
|
- 验收:高置信线索触发主动通知
|
||||||
|
- [ ] P1.11.5 历史数据全量重跑 + 同台盲测
|
||||||
|
- 验收:用 2-3 年历史数据重跑,与既有审计结论对比,复现已知线索并发现新增真实线索
|
||||||
|
- [ ] P1.11.6 同台盲测成效报告
|
||||||
|
- 验收:产出成效报告,量化命中率与新增线索价值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# P2 · 二期(能力扩展)
|
||||||
|
|
||||||
|
## P2.1 规则进化引擎完整化(R6)
|
||||||
|
> 目标:NL→规则、沙箱验证、版本管理、迭代。映射:R6 / PRD §4.2 Should。依赖:P1.4、P1.5。
|
||||||
|
|
||||||
|
- [ ] P2.1.1 自然语言→可执行规则转化
|
||||||
|
- 验收:规则管理员用自然语言描述模式即可生成可执行规则
|
||||||
|
- [ ] P2.1.2 规则沙箱(历史数据验证命中率,未确认不投产)
|
||||||
|
- 验收:新规则先沙箱验证,确认前不进生产
|
||||||
|
- [ ] P2.1.3 规则版本管理(创建人/修改人/时间/变更)
|
||||||
|
- 验收:规则版本历史可查可回溯
|
||||||
|
- [ ] P2.1.4 基于反馈的规则迭代优化
|
||||||
|
- 验收:依据审计员反馈可迭代规则
|
||||||
|
- [ ] P2.1.5 规则库(机构永久资产,可持续增长)
|
||||||
|
- 验收:规则可入库、检索、复用
|
||||||
|
|
||||||
|
## P2.2 场景三 · 收入成本跨期匹配(R10)
|
||||||
|
> 目标:识别确认时点错配。映射:R10 / PRD §4.3 Should。依赖:P1.2、P1.3。
|
||||||
|
- [ ] P2.2.1 收入确认政策/账务/合同三方勾稽
|
||||||
|
- [ ] P2.2.2 趸交/预收款一次性确认异常分录识别
|
||||||
|
- [ ] P2.2.3 设备交付与收入确认时间差监控
|
||||||
|
- [ ] P2.2.4 按使用量计费却提前确认收入识别
|
||||||
|
- 验收(本模块):各类时点错配生成线索并附勾稽证据
|
||||||
|
|
||||||
|
## P2.3 场景四 · 渠道佣金与套利(R11)
|
||||||
|
> 目标:终端流向与佣金匹配。映射:R11 / PRD §4.3 Should。依赖:P1.3。
|
||||||
|
- [ ] P2.3.1 IMEI 与用户绑定真实性校验
|
||||||
|
- [ ] P2.3.2 佣金与在网时长匹配度
|
||||||
|
- [ ] P2.3.3 IMEI 级终端流向追踪(激活即沉默/跨省流通)
|
||||||
|
- [ ] P2.3.4 代理商业务质量时序衰减分析
|
||||||
|
- 验收(本模块):套利闭环被识别并生成线索
|
||||||
|
|
||||||
|
## P2.4 场景八 · 员工内部舞弊(R15)
|
||||||
|
> 目标:权限滥用与积分套现识别。映射:R15 / PRD §4.3 Should。依赖:P1.2。
|
||||||
|
- [ ] P2.4.1 员工权限操作日志异常模式识别
|
||||||
|
- [ ] P2.4.2 内部测试号用途偏离识别
|
||||||
|
- [ ] P2.4.3 积分/电子券流向追踪与套现识别
|
||||||
|
- [ ] P2.4.4 权限-岗位匹配度分析(越权识别)
|
||||||
|
- 验收(本模块):内部舞弊模式生成线索并附证据
|
||||||
|
|
||||||
|
## P2.5 风险域全景与热力图(R16)
|
||||||
|
> 目标:全局视图与优先级。映射:R16 / PRD §4.4 Should。依赖:P1.6。
|
||||||
|
- [ ] P2.5.1 五大风险域归类与场景挂载
|
||||||
|
- [ ] P2.5.2 风险热力图(概率×金额)
|
||||||
|
- [ ] P2.5.3 高概率高金额场景配置为全量持续监控
|
||||||
|
- [ ] P2.5.4 多维筛选与下钻(风险域/场景/地市/单位)
|
||||||
|
- 验收(本模块):热力图可视化 + 一眼看出优先级
|
||||||
|
|
||||||
|
## P2.6 误报反馈学习与运营看板(R18、R21)
|
||||||
|
> 目标:准确率随使用上升 + 成效可量化。映射:R18、R21 / PRD §4.4 Should。依赖:P1.6、P1.11。
|
||||||
|
- [ ] P2.6.1 审计员"误报/属实"标注与反馈采集
|
||||||
|
- [ ] P2.6.2 阈值/模型基于反馈持续校准
|
||||||
|
- [ ] P2.6.3 运营看板(命中率/准确率/线索转化率)
|
||||||
|
- [ ] P2.6.4 成效度量(可挽回收入/止损统计)
|
||||||
|
- 验收(本模块):反馈闭环生效,运营指标上看板
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# P3 · 三期(全域覆盖)
|
||||||
|
|
||||||
|
## P3.1 场景五 · 网络建设与工程采购(R12)
|
||||||
|
> 映射:R12 / PRD §4.3 Could。依赖:P1.2、P2.1。
|
||||||
|
- [ ] P3.1.1 投标关联分析(报价相似度/文件雷同度,NLP 比对)
|
||||||
|
- [ ] P3.1.2 工程量与资源消耗匹配验证
|
||||||
|
- [ ] P3.1.3 巡检 GPS 轨迹与工单交叉验证
|
||||||
|
- [ ] P3.1.4 供应商画像与"马甲"识别
|
||||||
|
- 验收(本模块):围标串标/虚增工程量生成线索
|
||||||
|
|
||||||
|
## P3.2 场景六 · 互联互通与网间结算(R13)
|
||||||
|
> 映射:R13 / PRD §4.3 Could。依赖:P1.3。
|
||||||
|
- [ ] P3.2.1 话务量时序异常(突发峰值/整数时长聚集)
|
||||||
|
- [ ] P3.2.2 网间结算与网络侧原始信令比对
|
||||||
|
- [ ] P3.2.3 SP/CP 业务量与收入结算交叉验证
|
||||||
|
- [ ] P3.2.4 国际来话真实路由溯源
|
||||||
|
- 验收(本模块):刷量套利生成线索
|
||||||
|
|
||||||
|
## P3.3 场景七 · 云业务/IDC(R14)
|
||||||
|
> 映射:R14 / PRD §4.3 Could。依赖:P1.2、P1.3。
|
||||||
|
- [ ] P3.3.1 云资源利用率 vs 计费量匹配
|
||||||
|
- [ ] P3.3.2 IDC 出租率与电力消耗勾稽
|
||||||
|
- [ ] P3.3.3 新兴业务客户关联方识别与预付模式异常
|
||||||
|
- [ ] P3.3.4 收入确认与交付验收时序一致性
|
||||||
|
- 验收(本模块):空转/虚租生成线索
|
||||||
|
|
||||||
|
## P3.4 增量近实时与常态化(R3 完整)
|
||||||
|
> 映射:R3 / PRD §7 三期。依赖:P1.3。
|
||||||
|
- [ ] P3.4.1 从周期增量升级为近实时增量
|
||||||
|
- [ ] P3.4.2 常态化 7×24 监控调度与告警
|
||||||
|
- 验收:近实时数据驱动常态化监控稳定运行
|
||||||
|
|
||||||
|
## P3.5 信创适配与规则库规模化
|
||||||
|
> 映射:非功能 5.6、R6 / PRD §7 三期。依赖:P0.1、P2.1。
|
||||||
|
- [ ] P3.5.1 国产 GPU 与信创软硬件适配验证
|
||||||
|
- [ ] P3.5.2 规则库规模化治理(分类/检索/质量/退役)
|
||||||
|
- 验收:信创环境跑通 + 规则库可规模化运营
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变更记录
|
||||||
|
| 日期 | 变更内容 | 责任人 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-06 | 初版创建 | — |
|
||||||
|
| 2026-06 | 弃用 Docker,改用本地 PostgreSQL 16(卸载 pg14,装 pg16+pgvector);数据中台本体/图谱/双时态落地并通过集成测试 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **请检查确认本任务文档。** 确认通过后,将按本文档(建议从 P0 基建开始)推进开发,每完成一组任务进行测试、更新本文档状态并向你汇报。如需调整任务粒度、阶段切分或依赖关系,请直接告诉我。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# AIAudit · 本地私有化大模型电信运营商 AI 全域内审平台
|
||||||
|
|
||||||
|
> 数据不出域,审计全穿透。一套建在本地机房、数据零出域、覆盖全业务域、越用越聪明的 AI 内审能力体系。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
- 需求:[`0-req-AIAudit.md`](./0-req-AIAudit.md)
|
||||||
|
- 产品需求(PRD):[`1-prd-AIAudit.md`](./1-prd-AIAudit.md)
|
||||||
|
- 开发任务:[`2-task-AIAudit.md`](./2-task-AIAudit.md)
|
||||||
|
- 技术选型:[`docs/adr/ADR-0001-tech-stack.md`](./docs/adr/ADR-0001-tech-stack.md)
|
||||||
|
- 方案蓝图:[`docs/数据不出域,审计全穿透.md`](./docs/数据不出域,审计全穿透.md)
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
后端 Python(FastAPI) · 前端 React+TS(Vite) · PostgreSQL(AGE 图谱 / TimescaleDB 时序 / pgvector 向量) · Celery+Redis · MinIO · LLM(开发期公网千问 / 生产本地 vLLM)。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── backend/ # FastAPI 后端(引擎、数据中台、API)
|
||||||
|
├── frontend/ # React 前端(线索看板、自然语言查询)
|
||||||
|
├── infra/ # Docker Compose、自定义镜像、初始化脚本
|
||||||
|
├── docs/ # 蓝图、ADR
|
||||||
|
├── 0-req-AIAudit.md
|
||||||
|
├── 1-prd-AIAudit.md
|
||||||
|
└── 2-task-AIAudit.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据零出域红线
|
||||||
|
- 生产环境禁用任何公网 LLM/外网依赖;公网千问仅用于开发测试且只喂脱敏/样例假数据。
|
||||||
|
- 详见 ADR-0001。
|
||||||
|
|
||||||
|
## 本地开发快速开始
|
||||||
|
```bash
|
||||||
|
# 1. 初始化本地 PostgreSQL 16(已 Homebrew 安装 postgresql@16 + timescaledb + pgvector)
|
||||||
|
bash infra/postgres/setup_local.sh
|
||||||
|
|
||||||
|
# 2. 后端
|
||||||
|
cd backend && python3 -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
alembic upgrade head # 建表
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
|
||||||
|
# 3. 前端
|
||||||
|
cd frontend && npm install && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明:本项目不使用 Docker,开发期直接使用本机 PostgreSQL 16。
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 运行环境:dev | prod
|
||||||
|
# prod 下禁用一切公网 LLM Provider(数据零出域红线)
|
||||||
|
AIAUDIT_ENV=dev
|
||||||
|
|
||||||
|
# 数据库(本地 PostgreSQL 16 / Postgres.app,无密码)
|
||||||
|
DATABASE_URL=postgresql+psycopg://freedak@localhost:5432/aiaudit
|
||||||
|
|
||||||
|
# Redis / Celery
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# MinIO
|
||||||
|
MINIO_ENDPOINT=localhost:9000
|
||||||
|
MINIO_ACCESS_KEY=aiaudit
|
||||||
|
MINIO_SECRET_KEY=aiaudit_dev
|
||||||
|
|
||||||
|
# LLM Provider:dashscope(公网,仅 dev)| vllm(本地,prod)
|
||||||
|
LLM_PROVIDER=dashscope
|
||||||
|
# 公网千问(仅开发测试,且只允许脱敏/样例假数据)
|
||||||
|
DASHSCOPE_API_KEY=
|
||||||
|
DASHSCOPE_MODEL=qwen-plus
|
||||||
|
# 本地 vLLM(生产)
|
||||||
|
VLLM_BASE_URL=http://localhost:8001/v1
|
||||||
|
VLLM_MODEL=qwen2.5-72b-instruct
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = migrations
|
||||||
|
prepend_sys_path = .
|
||||||
|
sqlalchemy.url =
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARNING
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARNING
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""AIAudit 后端应用包。"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""HTTP API 层。"""
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""数据中台统一穿透查询 API(P1.2.5)。
|
||||||
|
|
||||||
|
作为各引擎与审计场景访问知识图谱的共同入口,对上层屏蔽底层是关系表还是图库。
|
||||||
|
对应需求 R2。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.api.schemas import (
|
||||||
|
EntityOut,
|
||||||
|
PenetrateRequest,
|
||||||
|
PenetrateResponse,
|
||||||
|
RelatedEntityOut,
|
||||||
|
)
|
||||||
|
from app.datahub.graph_repo import find_related_entities
|
||||||
|
from app.datahub.models import Entity
|
||||||
|
from app.db import get_session
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/datahub", tags=["datahub"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/entities/{entity_id}", response_model=EntityOut)
|
||||||
|
def get_entity(entity_id: uuid.UUID, session: Session = Depends(get_session)) -> Entity:
|
||||||
|
entity = session.get(Entity, entity_id)
|
||||||
|
if entity is None:
|
||||||
|
raise HTTPException(status_code=404, detail="实体不存在")
|
||||||
|
return entity
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/penetrate", response_model=PenetrateResponse)
|
||||||
|
def penetrate(
|
||||||
|
req: PenetrateRequest, session: Session = Depends(get_session)
|
||||||
|
) -> PenetrateResponse:
|
||||||
|
"""多跳穿透:返回与起点实体连通的关联实体(用于实控人/关联方/马甲识别)。"""
|
||||||
|
start = session.get(Entity, req.start_entity_id)
|
||||||
|
if start is None:
|
||||||
|
raise HTTPException(status_code=404, detail="起点实体不存在")
|
||||||
|
|
||||||
|
related_raw = find_related_entities(session, req.start_entity_id, max_depth=req.max_depth)
|
||||||
|
|
||||||
|
# 批量取出关联实体详情,组装可解释结果
|
||||||
|
id_to_depth = {rid: depth for rid, depth in related_raw}
|
||||||
|
entities = (
|
||||||
|
session.query(Entity).filter(Entity.id.in_(list(id_to_depth.keys()))).all()
|
||||||
|
if id_to_depth
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
related = [
|
||||||
|
RelatedEntityOut(entity=EntityOut.model_validate(e), depth=id_to_depth[e.id])
|
||||||
|
for e in entities
|
||||||
|
]
|
||||||
|
related.sort(key=lambda r: r.depth)
|
||||||
|
|
||||||
|
return PenetrateResponse(
|
||||||
|
start_entity_id=req.start_entity_id,
|
||||||
|
max_depth=req.max_depth,
|
||||||
|
related_count=len(related),
|
||||||
|
related=related,
|
||||||
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""API 数据传输模型(Pydantic)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class EntityOut(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
entity_type: str
|
||||||
|
business_key: str
|
||||||
|
display_name: str | None = None
|
||||||
|
attributes: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedEntityOut(BaseModel):
|
||||||
|
"""穿透命中的关联实体,附最短跳数(证据强度的初步指示)。"""
|
||||||
|
|
||||||
|
entity: EntityOut
|
||||||
|
depth: int
|
||||||
|
|
||||||
|
|
||||||
|
class PenetrateRequest(BaseModel):
|
||||||
|
start_entity_id: uuid.UUID
|
||||||
|
max_depth: int = Field(default=3, ge=1, le=6)
|
||||||
|
|
||||||
|
|
||||||
|
class PenetrateResponse(BaseModel):
|
||||||
|
start_entity_id: uuid.UUID
|
||||||
|
max_depth: int
|
||||||
|
related_count: int
|
||||||
|
related: list[RelatedEntityOut]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""系统自审计模块:不可篡改操作日志、独立性与分权(R19)。"""
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""系统自审计 ORM 模型:不可篡改操作日志(R19)。
|
||||||
|
|
||||||
|
每条日志含哈希链(prev_hash + 内容 → entry_hash),任何篡改都会断链,可检测。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, DateTime, Identity, Index, String
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
|
def _uuid() -> uuid.UUID:
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> dt.datetime:
|
||||||
|
return dt.datetime.now(dt.UTC)
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLog(Base):
|
||||||
|
"""不可篡改审计轨迹。仅追加,不可更新/删除(应用层与制度共同保证)。"""
|
||||||
|
|
||||||
|
__tablename__ = "audit_log"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_audit_actor", "actor"),
|
||||||
|
Index("ix_audit_action", "action"),
|
||||||
|
Index("ix_audit_seq", "seq", unique=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
# 自增序号,构成哈希链顺序
|
||||||
|
seq: Mapped[int] = mapped_column(
|
||||||
|
BigInteger, Identity(always=False), nullable=False, unique=True
|
||||||
|
)
|
||||||
|
actor: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
role: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||||
|
action: Mapped[str] = mapped_column(String(64), nullable=False) # 如 rule.update/clue.assign
|
||||||
|
target_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
target_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||||
|
detail: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
|
|
||||||
|
prev_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
entry_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""RBAC 权限与独立性约束(R19、PRD §6 权限矩阵)。
|
||||||
|
|
||||||
|
核心独立性规则(硬约束):
|
||||||
|
- 任何角色都不能删除线索(DELETE_CLUE 不授予任何角色;数据库触发器再兜底)。
|
||||||
|
- 业务方(business)对系统无任何写权限。
|
||||||
|
- 配规则/改阈值/看线索/出报告分权制衡。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class Role(str, enum.Enum):
|
||||||
|
AUDITOR = "auditor" # 审计员
|
||||||
|
AUDIT_MANAGER = "audit_manager" # 审计主管
|
||||||
|
RULE_ADMIN = "rule_admin" # 规则管理员
|
||||||
|
SYS_ADMIN = "sys_admin" # 系统管理员
|
||||||
|
SYS_AUDITOR = "sys_auditor" # 系统审计员(独立监督)
|
||||||
|
BUSINESS = "business" # 被审计业务方(无写权限)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(str, enum.Enum):
|
||||||
|
QUERY = "query" # 自然语言查询
|
||||||
|
VIEW_CLUE = "view_clue" # 查看线索
|
||||||
|
ADJUDICATE_CLUE = "adjudicate_clue" # 研判/定性线索
|
||||||
|
ASSIGN_CLUE = "assign_clue" # 分派线索
|
||||||
|
DELETE_CLUE = "delete_clue" # 删除线索(禁止授予任何人)
|
||||||
|
CONFIG_RULE = "config_rule" # 配置规则
|
||||||
|
ADJUST_THRESHOLD = "adjust_threshold" # 调整阈值
|
||||||
|
ISSUE_REPORT = "issue_report" # 出具报告
|
||||||
|
DATA_INGEST = "data_ingest" # 数据接入配置
|
||||||
|
VIEW_AUDIT_TRAIL = "view_audit_trail" # 查看自审计轨迹
|
||||||
|
MODEL_DEPLOY = "model_deploy" # 模型部署/升级
|
||||||
|
|
||||||
|
|
||||||
|
# 角色 -> 权限集合。注意:DELETE_CLUE 不出现在任何角色中(线索不可删,R19)。
|
||||||
|
ROLE_PERMISSIONS: dict[Role, set[Permission]] = {
|
||||||
|
Role.AUDITOR: {
|
||||||
|
Permission.QUERY,
|
||||||
|
Permission.VIEW_CLUE,
|
||||||
|
Permission.ADJUDICATE_CLUE,
|
||||||
|
Permission.ISSUE_REPORT,
|
||||||
|
},
|
||||||
|
Role.AUDIT_MANAGER: {
|
||||||
|
Permission.QUERY,
|
||||||
|
Permission.VIEW_CLUE,
|
||||||
|
Permission.ADJUDICATE_CLUE,
|
||||||
|
Permission.ASSIGN_CLUE,
|
||||||
|
Permission.ISSUE_REPORT,
|
||||||
|
},
|
||||||
|
Role.RULE_ADMIN: {
|
||||||
|
Permission.QUERY,
|
||||||
|
Permission.VIEW_CLUE,
|
||||||
|
Permission.CONFIG_RULE,
|
||||||
|
Permission.ADJUST_THRESHOLD,
|
||||||
|
},
|
||||||
|
Role.SYS_ADMIN: {
|
||||||
|
Permission.DATA_INGEST,
|
||||||
|
Permission.MODEL_DEPLOY,
|
||||||
|
},
|
||||||
|
Role.SYS_AUDITOR: {
|
||||||
|
Permission.QUERY,
|
||||||
|
Permission.VIEW_CLUE,
|
||||||
|
Permission.VIEW_AUDIT_TRAIL,
|
||||||
|
Permission.ISSUE_REPORT,
|
||||||
|
},
|
||||||
|
Role.BUSINESS: set(), # 业务方无任何权限
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(role: Role, perm: Permission) -> bool:
|
||||||
|
return perm in ROLE_PERMISSIONS.get(role, set())
|
||||||
|
|
||||||
|
|
||||||
|
def can_delete_clue(role: Role) -> bool:
|
||||||
|
"""线索不可删除——对所有角色恒为 False(独立性硬约束)。"""
|
||||||
|
return False
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""系统自审计服务:写入哈希链审计日志、校验完整性(R19)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.audit.models import AuditLog
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_hash(prev_hash: str | None, payload: dict) -> str:
|
||||||
|
body = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
||||||
|
raw = f"{prev_hash or ''}|{body}"
|
||||||
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def record(
|
||||||
|
session: Session,
|
||||||
|
actor: str,
|
||||||
|
action: str,
|
||||||
|
*,
|
||||||
|
role: str | None = None,
|
||||||
|
target_type: str | None = None,
|
||||||
|
target_id: str | None = None,
|
||||||
|
detail: dict | None = None,
|
||||||
|
) -> AuditLog:
|
||||||
|
"""追加一条审计日志,自动接续哈希链。"""
|
||||||
|
last = session.execute(
|
||||||
|
select(AuditLog).order_by(AuditLog.seq.desc()).limit(1)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
prev_hash = last.entry_hash if last else None
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"actor": actor,
|
||||||
|
"role": role,
|
||||||
|
"action": action,
|
||||||
|
"target_type": target_type,
|
||||||
|
"target_id": target_id,
|
||||||
|
"detail": detail or {},
|
||||||
|
}
|
||||||
|
entry_hash = _compute_hash(prev_hash, payload)
|
||||||
|
|
||||||
|
log = AuditLog(
|
||||||
|
actor=actor,
|
||||||
|
role=role,
|
||||||
|
action=action,
|
||||||
|
target_type=target_type,
|
||||||
|
target_id=target_id,
|
||||||
|
detail=detail or {},
|
||||||
|
prev_hash=prev_hash,
|
||||||
|
entry_hash=entry_hash,
|
||||||
|
)
|
||||||
|
session.add(log)
|
||||||
|
session.flush()
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def verify_chain(session: Session) -> tuple[bool, int | None]:
|
||||||
|
"""校验审计日志哈希链完整性。
|
||||||
|
|
||||||
|
返回 (是否完整, 首个断链的 seq 或 None)。
|
||||||
|
"""
|
||||||
|
rows = session.execute(select(AuditLog).order_by(AuditLog.seq.asc())).scalars().all()
|
||||||
|
prev_hash: str | None = None
|
||||||
|
for row in rows:
|
||||||
|
payload = {
|
||||||
|
"actor": row.actor,
|
||||||
|
"role": row.role,
|
||||||
|
"action": row.action,
|
||||||
|
"target_type": row.target_type,
|
||||||
|
"target_id": row.target_id,
|
||||||
|
"detail": row.detail or {},
|
||||||
|
}
|
||||||
|
expected = _compute_hash(prev_hash, payload)
|
||||||
|
if expected != row.entry_hash or row.prev_hash != prev_hash:
|
||||||
|
return False, row.seq
|
||||||
|
prev_hash = row.entry_hash
|
||||||
|
return True, None
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""线索引擎模块:线索模型、生成、置信度分级、状态流转(人机闭环)。"""
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""线索 ORM 模型。
|
||||||
|
|
||||||
|
对应需求 R7(线索+证据链+解释)、R17(闭环状态)、R18(置信度分级)、R19(线索不可删)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Enum, Float, ForeignKey, Index, String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
|
def _enum_values(enum_cls):
|
||||||
|
"""让 SQLAlchemy 使用枚举的 value(小写)写入 PG 原生 enum,而非 name。"""
|
||||||
|
return [m.value for m in enum_cls]
|
||||||
|
|
||||||
|
|
||||||
|
def _uuid() -> uuid.UUID:
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> dt.datetime:
|
||||||
|
return dt.datetime.now(dt.UTC)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfidenceTier(str, enum.Enum):
|
||||||
|
"""置信度三级分流(R18)。"""
|
||||||
|
|
||||||
|
HIGH = "high" # 高置信:直接推送处置
|
||||||
|
MEDIUM = "medium" # 中置信:人工复核
|
||||||
|
LOW = "low" # 低置信:归档备查
|
||||||
|
|
||||||
|
|
||||||
|
class ClueStatus(str, enum.Enum):
|
||||||
|
"""线索闭环状态机(R17)。"""
|
||||||
|
|
||||||
|
NEW = "new" # 新生成
|
||||||
|
ASSIGNED = "assigned" # 已分派
|
||||||
|
REVIEWING = "reviewing" # 研判中
|
||||||
|
CONFIRMED = "confirmed" # 已定性属实
|
||||||
|
DISMISSED = "dismissed" # 已定性误报
|
||||||
|
RECTIFYING = "rectifying" # 整改中
|
||||||
|
TRANSFERRED = "transferred" # 已移交
|
||||||
|
CLOSED = "closed" # 已销项闭环
|
||||||
|
|
||||||
|
|
||||||
|
class Clue(Base):
|
||||||
|
"""审计线索。线索一经生成不可物理删除(R19),失效通过状态表达。"""
|
||||||
|
|
||||||
|
__tablename__ = "clue"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_clue_status", "status"),
|
||||||
|
Index("ix_clue_scenario", "scenario_code"),
|
||||||
|
Index("ix_clue_assignee", "assignee"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||||
|
risk_domain: Mapped[str] = mapped_column(String(32), nullable=False) # 收入/成本/采购/资金/合规
|
||||||
|
scenario_code: Mapped[str] = mapped_column(String(32), nullable=False) # 如 R8/R9
|
||||||
|
confidence: Mapped[ConfidenceTier] = mapped_column(
|
||||||
|
Enum(ConfidenceTier, name="confidence_tier", values_callable=_enum_values),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
score: Mapped[float] = mapped_column(Float, default=0.0) # 0-1 风险评分
|
||||||
|
status: Mapped[ClueStatus] = mapped_column(
|
||||||
|
Enum(ClueStatus, name="clue_status", values_callable=_enum_values),
|
||||||
|
default=ClueStatus.NEW,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
# 人话解释(判定理由)与证据链
|
||||||
|
rationale: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
evidence: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
# 涉及的主体(金额、实体 id 列表等)
|
||||||
|
subjects: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
amount_involved: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
assignee: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
# 误报/属实反馈(R18 反馈学习)
|
||||||
|
feedback: Mapped[str | None] = mapped_column(String(16), nullable=True) # confirmed/false_positive
|
||||||
|
|
||||||
|
# 可追溯:产生该线索时的模型/规则/数据版本(R19 三重留痕)
|
||||||
|
model_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
rule_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
data_version_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
|
||||||
|
|
||||||
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
|
updated_at: Mapped[dt.datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=_now, onupdate=_now
|
||||||
|
)
|
||||||
|
|
||||||
|
history: Mapped[list[ClueStatusHistory]] = relationship(
|
||||||
|
back_populates="clue", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClueStatusHistory(Base):
|
||||||
|
"""线索状态流转留痕(R17/R19)。"""
|
||||||
|
|
||||||
|
__tablename__ = "clue_status_history"
|
||||||
|
__table_args__ = (Index("ix_csh_clue", "clue_id"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
clue_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("clue.id"), nullable=False
|
||||||
|
)
|
||||||
|
from_status: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||||
|
to_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
actor: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
|
|
||||||
|
clue: Mapped[Clue] = relationship(back_populates="history")
|
||||||
|
|
||||||
|
|
||||||
|
class WorkingPaper(Base):
|
||||||
|
"""审计底稿(R17):研判完成自动生成,可追溯。"""
|
||||||
|
|
||||||
|
__tablename__ = "working_paper"
|
||||||
|
__table_args__ = (Index("ix_wp_clue", "clue_id"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
clue_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("clue.id"), nullable=False
|
||||||
|
)
|
||||||
|
content: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
conclusion: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||||
|
author: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
snapshot: Mapped[dict] = mapped_column(JSONB, default=dict) # 证据/版本快照
|
||||||
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"""线索服务:生成、置信度分级、状态流转、底稿生成、反馈。
|
||||||
|
|
||||||
|
对应 R7 / R17 / R18 / R19。所有状态变更写入历史并记自审计日志(线索不可删)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.audit import service as audit
|
||||||
|
from app.clues.models import (
|
||||||
|
Clue,
|
||||||
|
ClueStatus,
|
||||||
|
ClueStatusHistory,
|
||||||
|
ConfidenceTier,
|
||||||
|
WorkingPaper,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 允许的状态流转(R17 闭环)
|
||||||
|
_ALLOWED_TRANSITIONS: dict[ClueStatus, set[ClueStatus]] = {
|
||||||
|
ClueStatus.NEW: {ClueStatus.ASSIGNED, ClueStatus.REVIEWING},
|
||||||
|
ClueStatus.ASSIGNED: {ClueStatus.REVIEWING},
|
||||||
|
ClueStatus.REVIEWING: {ClueStatus.CONFIRMED, ClueStatus.DISMISSED},
|
||||||
|
ClueStatus.CONFIRMED: {ClueStatus.RECTIFYING, ClueStatus.TRANSFERRED},
|
||||||
|
ClueStatus.DISMISSED: {ClueStatus.CLOSED},
|
||||||
|
ClueStatus.RECTIFYING: {ClueStatus.CLOSED},
|
||||||
|
ClueStatus.TRANSFERRED: {ClueStatus.CLOSED},
|
||||||
|
ClueStatus.CLOSED: set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IllegalTransitionError(ValueError):
|
||||||
|
"""非法的线索状态流转。"""
|
||||||
|
|
||||||
|
|
||||||
|
def score_to_tier(score: float) -> ConfidenceTier:
|
||||||
|
"""风险评分映射到置信度三级(R18)。"""
|
||||||
|
if score >= 0.8:
|
||||||
|
return ConfidenceTier.HIGH
|
||||||
|
if score >= 0.5:
|
||||||
|
return ConfidenceTier.MEDIUM
|
||||||
|
return ConfidenceTier.LOW
|
||||||
|
|
||||||
|
|
||||||
|
def create_clue(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
title: str,
|
||||||
|
risk_domain: str,
|
||||||
|
scenario_code: str,
|
||||||
|
score: float,
|
||||||
|
rationale: str,
|
||||||
|
evidence: dict,
|
||||||
|
subjects: dict | None = None,
|
||||||
|
amount_involved: float | None = None,
|
||||||
|
model_version: str | None = None,
|
||||||
|
rule_version: str | None = None,
|
||||||
|
data_version_id: uuid.UUID | None = None,
|
||||||
|
actor: str = "system",
|
||||||
|
) -> Clue:
|
||||||
|
"""生成一条线索,自动按评分分级,并记录创建留痕。"""
|
||||||
|
clue = Clue(
|
||||||
|
title=title,
|
||||||
|
risk_domain=risk_domain,
|
||||||
|
scenario_code=scenario_code,
|
||||||
|
confidence=score_to_tier(score),
|
||||||
|
score=score,
|
||||||
|
status=ClueStatus.NEW,
|
||||||
|
rationale=rationale,
|
||||||
|
evidence=evidence,
|
||||||
|
subjects=subjects or {},
|
||||||
|
amount_involved=amount_involved,
|
||||||
|
model_version=model_version,
|
||||||
|
rule_version=rule_version,
|
||||||
|
data_version_id=data_version_id,
|
||||||
|
)
|
||||||
|
session.add(clue)
|
||||||
|
session.flush()
|
||||||
|
_add_history(session, clue, None, ClueStatus.NEW, actor, "线索生成")
|
||||||
|
audit.record(
|
||||||
|
session, actor, "create_clue",
|
||||||
|
target_type="clue", target_id=str(clue.id),
|
||||||
|
detail={"scenario": scenario_code, "score": score, "confidence": clue.confidence.value},
|
||||||
|
)
|
||||||
|
return clue
|
||||||
|
|
||||||
|
|
||||||
|
def _add_history(
|
||||||
|
session: Session,
|
||||||
|
clue: Clue,
|
||||||
|
from_status: ClueStatus | None,
|
||||||
|
to_status: ClueStatus,
|
||||||
|
actor: str,
|
||||||
|
note: str | None,
|
||||||
|
) -> None:
|
||||||
|
session.add(
|
||||||
|
ClueStatusHistory(
|
||||||
|
clue_id=clue.id,
|
||||||
|
from_status=from_status.value if from_status else None,
|
||||||
|
to_status=to_status.value,
|
||||||
|
actor=actor,
|
||||||
|
note=note,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def transition(
|
||||||
|
session: Session, clue: Clue, to_status: ClueStatus, actor: str, note: str | None = None
|
||||||
|
) -> Clue:
|
||||||
|
"""执行状态流转,校验合法性并留痕。"""
|
||||||
|
if to_status not in _ALLOWED_TRANSITIONS.get(clue.status, set()):
|
||||||
|
raise IllegalTransitionError(
|
||||||
|
f"线索状态不能从 {clue.status.value} 流转到 {to_status.value}"
|
||||||
|
)
|
||||||
|
from_status = clue.status
|
||||||
|
clue.status = to_status
|
||||||
|
session.flush()
|
||||||
|
_add_history(session, clue, from_status, to_status, actor, note)
|
||||||
|
audit.record(
|
||||||
|
session, actor, "transition_clue",
|
||||||
|
target_type="clue", target_id=str(clue.id),
|
||||||
|
detail={"from": from_status.value, "to": to_status.value, "note": note},
|
||||||
|
)
|
||||||
|
return clue
|
||||||
|
|
||||||
|
|
||||||
|
def assign(session: Session, clue: Clue, assignee: str, actor: str) -> Clue:
|
||||||
|
clue.assignee = assignee
|
||||||
|
session.flush()
|
||||||
|
if clue.status == ClueStatus.NEW:
|
||||||
|
transition(session, clue, ClueStatus.ASSIGNED, actor, f"分派给 {assignee}")
|
||||||
|
audit.record(session, actor, "assign_clue", target_type="clue", target_id=str(clue.id), detail={"assignee": assignee})
|
||||||
|
return clue
|
||||||
|
|
||||||
|
|
||||||
|
def adjudicate(
|
||||||
|
session: Session, clue: Clue, confirmed: bool, actor: str, note: str | None = None
|
||||||
|
) -> WorkingPaper:
|
||||||
|
"""研判定性:确认属实或误报,自动生成审计底稿并记录反馈(R17/R18)。"""
|
||||||
|
if clue.status not in (ClueStatus.ASSIGNED, ClueStatus.REVIEWING, ClueStatus.NEW):
|
||||||
|
# 允许从 NEW/ASSIGNED 直接进入研判
|
||||||
|
pass
|
||||||
|
if clue.status != ClueStatus.REVIEWING:
|
||||||
|
# 先进入研判中
|
||||||
|
target = ClueStatus.REVIEWING
|
||||||
|
if target in _ALLOWED_TRANSITIONS.get(clue.status, set()):
|
||||||
|
transition(session, clue, ClueStatus.REVIEWING, actor, "进入研判")
|
||||||
|
|
||||||
|
to = ClueStatus.CONFIRMED if confirmed else ClueStatus.DISMISSED
|
||||||
|
transition(session, clue, to, actor, note)
|
||||||
|
clue.feedback = "confirmed" if confirmed else "false_positive"
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
paper = WorkingPaper(
|
||||||
|
clue_id=clue.id,
|
||||||
|
content=note or "",
|
||||||
|
conclusion=to.value,
|
||||||
|
author=actor,
|
||||||
|
snapshot={
|
||||||
|
"evidence": clue.evidence,
|
||||||
|
"rationale": clue.rationale,
|
||||||
|
"score": clue.score,
|
||||||
|
"model_version": clue.model_version,
|
||||||
|
"rule_version": clue.rule_version,
|
||||||
|
"data_version_id": str(clue.data_version_id) if clue.data_version_id else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.add(paper)
|
||||||
|
session.flush()
|
||||||
|
audit.record(
|
||||||
|
session, actor, "create_working_paper",
|
||||||
|
target_type="working_paper", target_id=str(paper.id),
|
||||||
|
detail={"clue_id": str(clue.id), "conclusion": to.value},
|
||||||
|
)
|
||||||
|
return paper
|
||||||
|
|
||||||
|
|
||||||
|
def list_clues(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
status: ClueStatus | None = None,
|
||||||
|
scenario_code: str | None = None,
|
||||||
|
confidence: ConfidenceTier | None = None,
|
||||||
|
) -> list[Clue]:
|
||||||
|
q = session.query(Clue)
|
||||||
|
if status:
|
||||||
|
q = q.filter(Clue.status == status)
|
||||||
|
if scenario_code:
|
||||||
|
q = q.filter(Clue.scenario_code == scenario_code)
|
||||||
|
if confidence:
|
||||||
|
q = q.filter(Clue.confidence == confidence)
|
||||||
|
return q.order_by(Clue.score.desc()).all()
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""应用配置。
|
||||||
|
|
||||||
|
通过环境变量加载,区分 dev / prod 运行环境。
|
||||||
|
prod 环境强制执行"数据零出域"红线:禁用任何公网 LLM Provider。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class AppEnv(str, Enum):
|
||||||
|
dev = "dev"
|
||||||
|
prod = "prod"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMProviderName(str, Enum):
|
||||||
|
dashscope = "dashscope" # 公网千问,仅 dev
|
||||||
|
vllm = "vllm" # 本地,prod
|
||||||
|
|
||||||
|
|
||||||
|
# 被认定为"公网/出域"的 Provider,prod 下禁止使用
|
||||||
|
EGRESS_PROVIDERS: frozenset[LLMProviderName] = frozenset({LLMProviderName.dashscope})
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="",
|
||||||
|
env_file=".env",
|
||||||
|
extra="ignore",
|
||||||
|
case_sensitive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
aiaudit_env: AppEnv = AppEnv.dev
|
||||||
|
|
||||||
|
database_url: str = "postgresql+psycopg://freedak@localhost:5432/aiaudit"
|
||||||
|
redis_url: str = "redis://localhost:6379/0"
|
||||||
|
|
||||||
|
llm_provider: LLMProviderName = LLMProviderName.dashscope
|
||||||
|
dashscope_api_key: str = ""
|
||||||
|
dashscope_model: str = "qwen-plus"
|
||||||
|
vllm_base_url: str = "http://localhost:8001/v1"
|
||||||
|
vllm_model: str = "qwen2.5-72b-instruct"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_prod(self) -> bool:
|
||||||
|
return self.aiaudit_env == AppEnv.prod
|
||||||
|
|
||||||
|
def validate_egress_policy(self) -> None:
|
||||||
|
"""数据零出域红线校验:prod 环境禁用公网 Provider。
|
||||||
|
|
||||||
|
在应用启动时调用;违反则抛出异常阻断启动。
|
||||||
|
"""
|
||||||
|
if self.is_prod and self.llm_provider in EGRESS_PROVIDERS:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"数据零出域红线违规:prod 环境禁止使用公网 LLM Provider "
|
||||||
|
f"'{self.llm_provider.value}'。请改用本地 Provider(如 vllm)。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_settings: Settings | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
global _settings
|
||||||
|
if _settings is None:
|
||||||
|
_settings = Settings()
|
||||||
|
return _settings
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""审计数据中台模块:本体/知识图谱、双时态、时序、数据版本。"""
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""双时态事实仓储:写入与"按历史时点回放"查询。
|
||||||
|
|
||||||
|
对应需求 R3 / ADR-0002:
|
||||||
|
- 业务有效期 valid_from/valid_to(应用时间)
|
||||||
|
- 系统记录期 system_from/system_to(事务时间)
|
||||||
|
回放 = 给定 (as_of_valid, as_of_system) 在两条时间线上各取"包含该时点"的记录。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import or_
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.datahub.models import BitemporalFact
|
||||||
|
|
||||||
|
|
||||||
|
def record_fact(
|
||||||
|
session: Session,
|
||||||
|
entity_id: uuid.UUID,
|
||||||
|
attr_name: str,
|
||||||
|
attr_value: dict,
|
||||||
|
valid_from: dt.datetime,
|
||||||
|
valid_to: dt.datetime | None = None,
|
||||||
|
data_version_id: uuid.UUID | None = None,
|
||||||
|
) -> BitemporalFact:
|
||||||
|
"""记录一条双时态事实(system_from 自动取当前事务时间)。"""
|
||||||
|
fact = BitemporalFact(
|
||||||
|
entity_id=entity_id,
|
||||||
|
attr_name=attr_name,
|
||||||
|
attr_value=attr_value,
|
||||||
|
valid_from=valid_from,
|
||||||
|
valid_to=valid_to,
|
||||||
|
data_version_id=data_version_id,
|
||||||
|
)
|
||||||
|
session.add(fact)
|
||||||
|
session.flush()
|
||||||
|
return fact
|
||||||
|
|
||||||
|
|
||||||
|
def as_of(
|
||||||
|
session: Session,
|
||||||
|
entity_id: uuid.UUID,
|
||||||
|
attr_name: str,
|
||||||
|
as_of_valid: dt.datetime,
|
||||||
|
as_of_system: dt.datetime | None = None,
|
||||||
|
) -> BitemporalFact | None:
|
||||||
|
"""回放:返回在给定业务时点且按给定系统时点可见的事实。
|
||||||
|
|
||||||
|
- 业务时间线:valid_from <= as_of_valid < valid_to(或为空表示至今)
|
||||||
|
- 系统时间线:system_from <= as_of_system < system_to(或为空表示当前可见)
|
||||||
|
"""
|
||||||
|
as_of_system = as_of_system or dt.datetime.now(dt.UTC)
|
||||||
|
|
||||||
|
q = (
|
||||||
|
session.query(BitemporalFact)
|
||||||
|
.filter(BitemporalFact.entity_id == entity_id)
|
||||||
|
.filter(BitemporalFact.attr_name == attr_name)
|
||||||
|
.filter(BitemporalFact.valid_from <= as_of_valid)
|
||||||
|
.filter(
|
||||||
|
or_(BitemporalFact.valid_to.is_(None), BitemporalFact.valid_to > as_of_valid)
|
||||||
|
)
|
||||||
|
.filter(BitemporalFact.system_from <= as_of_system)
|
||||||
|
.filter(
|
||||||
|
or_(
|
||||||
|
BitemporalFact.system_to.is_(None),
|
||||||
|
BitemporalFact.system_to > as_of_system,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(BitemporalFact.system_from.desc())
|
||||||
|
)
|
||||||
|
return q.first()
|
||||||
|
|
||||||
|
|
||||||
|
def close_fact(
|
||||||
|
session: Session, fact: BitemporalFact, system_to: dt.datetime | None = None
|
||||||
|
) -> None:
|
||||||
|
"""逻辑关闭一条事实的系统可见期(用于更正/失效,而非物理删除)。"""
|
||||||
|
fact.system_to = system_to or dt.datetime.now(dt.UTC)
|
||||||
|
session.add(fact)
|
||||||
|
session.flush()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""数据中台 schema 初始化。
|
||||||
|
|
||||||
|
MVP 阶段以 SQLAlchemy metadata 建表(后续可迁移到 Alembic)。
|
||||||
|
扩展按可用性可选启用:
|
||||||
|
- btree_gist / vector:若可用则创建。
|
||||||
|
- timescaledb:若可用则把 metric_event 转为超表;不可用则保持普通表(带时间索引)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
|
from app.datahub import models # noqa: F401 确保模型注册到 metadata
|
||||||
|
from app.db import Base, get_engine
|
||||||
|
|
||||||
|
|
||||||
|
def _extension_available(engine: Engine, name: str) -> bool:
|
||||||
|
with engine.connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
text("SELECT 1 FROM pg_available_extensions WHERE name = :n"), {"n": name}
|
||||||
|
).first()
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
def init_extensions(engine: Engine) -> dict[str, bool]:
|
||||||
|
"""按可用性创建扩展,返回各扩展启用状态。"""
|
||||||
|
status: dict[str, bool] = {}
|
||||||
|
for ext in ("btree_gist", "vector", "timescaledb"):
|
||||||
|
available = _extension_available(engine, ext)
|
||||||
|
status[ext] = available
|
||||||
|
if available:
|
||||||
|
with engine.begin() as conn:
|
||||||
|
conn.execute(text(f"CREATE EXTENSION IF NOT EXISTS {ext}"))
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def create_schema(engine: Engine | None = None) -> dict[str, bool]:
|
||||||
|
"""创建数据中台全部表,并按需启用时序超表。返回扩展状态。"""
|
||||||
|
engine = engine or get_engine()
|
||||||
|
status = init_extensions(engine)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# 若 TimescaleDB 可用,将时序事件表转为超表(幂等)
|
||||||
|
if status.get("timescaledb"):
|
||||||
|
with engine.begin() as conn:
|
||||||
|
conn.execute(
|
||||||
|
text(
|
||||||
|
"SELECT create_hypertable('metric_event', 'event_time', "
|
||||||
|
"if_not_exists => TRUE, migrate_data => TRUE)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
st = create_schema()
|
||||||
|
print("数据中台 schema 初始化完成。扩展状态:", st)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""知识图谱仓储:实体/关系写入与多跳穿透(递归 CTE)。
|
||||||
|
|
||||||
|
对应需求 R2:支撑隐性实控人、关联方网络、"马甲"供应商等穿透分析。
|
||||||
|
统一穿透查询服务(P1.2.5)在此之上封装对外 API,对上层屏蔽底层是关系表还是图库。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.datahub.models import Entity, EntityRelationship
|
||||||
|
from app.datahub.ontology import EntityType, RelationshipType, is_valid_relationship
|
||||||
|
|
||||||
|
|
||||||
|
class OntologyViolationError(ValueError):
|
||||||
|
"""关系不符合本体约束。"""
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_entity(
|
||||||
|
session: Session,
|
||||||
|
entity_type: EntityType,
|
||||||
|
business_key: str,
|
||||||
|
display_name: str | None = None,
|
||||||
|
attributes: dict | None = None,
|
||||||
|
data_version_id: uuid.UUID | None = None,
|
||||||
|
) -> Entity:
|
||||||
|
"""按 (类型, 业务主键) 幂等写入实体(主数据对齐的归一锚点)。"""
|
||||||
|
existing = (
|
||||||
|
session.query(Entity)
|
||||||
|
.filter(Entity.entity_type == entity_type.value, Entity.business_key == business_key)
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
if existing is not None:
|
||||||
|
if display_name is not None:
|
||||||
|
existing.display_name = display_name
|
||||||
|
if attributes:
|
||||||
|
existing.attributes = {**(existing.attributes or {}), **attributes}
|
||||||
|
return existing
|
||||||
|
|
||||||
|
entity = Entity(
|
||||||
|
entity_type=entity_type.value,
|
||||||
|
business_key=business_key,
|
||||||
|
display_name=display_name,
|
||||||
|
attributes=attributes or {},
|
||||||
|
data_version_id=data_version_id,
|
||||||
|
)
|
||||||
|
session.add(entity)
|
||||||
|
session.flush()
|
||||||
|
return entity
|
||||||
|
|
||||||
|
|
||||||
|
def add_relationship(
|
||||||
|
session: Session,
|
||||||
|
rel_type: RelationshipType,
|
||||||
|
source: Entity,
|
||||||
|
target: Entity,
|
||||||
|
attributes: dict | None = None,
|
||||||
|
data_version_id: uuid.UUID | None = None,
|
||||||
|
) -> EntityRelationship:
|
||||||
|
"""新增一条关系边,写入前校验本体约束。"""
|
||||||
|
src_type = EntityType(source.entity_type)
|
||||||
|
tgt_type = EntityType(target.entity_type)
|
||||||
|
if not is_valid_relationship(rel_type, src_type, tgt_type):
|
||||||
|
raise OntologyViolationError(
|
||||||
|
f"关系 {rel_type.value} 不允许从 {src_type.value} 指向 {tgt_type.value}"
|
||||||
|
)
|
||||||
|
rel = EntityRelationship(
|
||||||
|
rel_type=rel_type.value,
|
||||||
|
source_id=source.id,
|
||||||
|
target_id=target.id,
|
||||||
|
attributes=attributes or {},
|
||||||
|
data_version_id=data_version_id,
|
||||||
|
)
|
||||||
|
session.add(rel)
|
||||||
|
session.flush()
|
||||||
|
return rel
|
||||||
|
|
||||||
|
|
||||||
|
# 多跳穿透:以无向方式遍历关系边,返回与起点在 max_depth 跳内连通的实体集合。
|
||||||
|
# 用于"疑似同一实控人/关联方网络"识别。
|
||||||
|
_TRAVERSE_SQL = text(
|
||||||
|
"""
|
||||||
|
WITH RECURSIVE reachable(entity_id, depth, path) AS (
|
||||||
|
SELECT :start_id, 0, ARRAY[:start_id]
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
CASE WHEN r.source_id = rc.entity_id THEN r.target_id ELSE r.source_id END,
|
||||||
|
rc.depth + 1,
|
||||||
|
rc.path || CASE WHEN r.source_id = rc.entity_id THEN r.target_id ELSE r.source_id END
|
||||||
|
FROM reachable rc
|
||||||
|
JOIN entity_relationship r
|
||||||
|
ON (r.source_id = rc.entity_id OR r.target_id = rc.entity_id)
|
||||||
|
WHERE rc.depth < :max_depth
|
||||||
|
AND NOT (
|
||||||
|
CASE WHEN r.source_id = rc.entity_id THEN r.target_id ELSE r.source_id END
|
||||||
|
= ANY(rc.path)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT DISTINCT entity_id, MIN(depth) AS depth
|
||||||
|
FROM reachable
|
||||||
|
WHERE entity_id <> :start_id
|
||||||
|
GROUP BY entity_id
|
||||||
|
ORDER BY depth;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_related_entities(
|
||||||
|
session: Session, start_id: uuid.UUID, max_depth: int = 3
|
||||||
|
) -> list[tuple[uuid.UUID, int]]:
|
||||||
|
"""返回与起点实体在 max_depth 跳内连通的实体 (id, 最短跳数) 列表。"""
|
||||||
|
rows = session.execute(
|
||||||
|
_TRAVERSE_SQL, {"start_id": start_id, "max_depth": max_depth}
|
||||||
|
).all()
|
||||||
|
return [(r[0], r[1]) for r in rows]
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"""审计数据中台 ORM 模型。
|
||||||
|
|
||||||
|
涵盖:数据版本、本体实体、知识图谱关系边、双时态属性、时序事件。
|
||||||
|
对应需求 R2 / R3,建模决策见 ADR-0002。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime,
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
Index,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
|
def _uuid() -> uuid.UUID:
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
|
class DataVersion(Base):
|
||||||
|
"""数据版本登记:每批接入数据的来源/批次/时间/行数,支撑结论可追溯(R3)。"""
|
||||||
|
|
||||||
|
__tablename__ = "data_version"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
source_system: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
batch_label: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
row_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
ingested_at: Mapped[dt.datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=lambda: dt.datetime.now(dt.UTC)
|
||||||
|
)
|
||||||
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(Base):
|
||||||
|
"""本体实体节点(知识图谱顶点)。
|
||||||
|
|
||||||
|
business_key 是源系统中的业务主键,用于主数据对齐(同一实体跨系统归一)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "entity"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("entity_type", "business_key", name="uq_entity_type_bizkey"),
|
||||||
|
Index("ix_entity_type", "entity_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
entity_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
business_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
display_name: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||||
|
attributes: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
|
||||||
|
# 主数据对齐:被归并到的"金主"实体(同一实控人/同一主体)。NULL 表示自身即主实体。
|
||||||
|
canonical_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("entity.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
data_version_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("data_version.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EntityRelationship(Base):
|
||||||
|
"""知识图谱关系边(有向)。多跳穿透用递归 CTE 遍历本表。"""
|
||||||
|
|
||||||
|
__tablename__ = "entity_relationship"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_rel_source", "source_id"),
|
||||||
|
Index("ix_rel_target", "target_id"),
|
||||||
|
Index("ix_rel_type", "rel_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
rel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
source_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("entity.id"), nullable=False
|
||||||
|
)
|
||||||
|
target_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("entity.id"), nullable=False
|
||||||
|
)
|
||||||
|
attributes: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
|
||||||
|
data_version_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("data_version.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
source: Mapped[Entity] = relationship(foreign_keys=[source_id])
|
||||||
|
target: Mapped[Entity] = relationship(foreign_keys=[target_id])
|
||||||
|
|
||||||
|
|
||||||
|
class BitemporalFact(Base):
|
||||||
|
"""双时态事实:实体的某个属性/状态随时间变化的记录。
|
||||||
|
|
||||||
|
- 业务有效期 valid_from/valid_to(应用时间)
|
||||||
|
- 系统记录期 system_from/system_to(事务时间)
|
||||||
|
回放历史 = 给定 (as_of_valid, as_of_system) 过滤两条时间线(见 repository)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "bitemporal_fact"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_btf_entity_attr", "entity_id", "attr_name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
entity_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("entity.id"), nullable=False
|
||||||
|
)
|
||||||
|
attr_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
attr_value: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
|
||||||
|
valid_from: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
|
valid_to: Mapped[dt.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
system_from: Mapped[dt.datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=lambda: dt.datetime.now(dt.UTC)
|
||||||
|
)
|
||||||
|
system_to: Mapped[dt.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
data_version_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("data_version.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricEvent(Base):
|
||||||
|
"""时序事件:行为/指标类数据(用户生命周期、回款、话务、佣金、资源使用)。
|
||||||
|
|
||||||
|
部署后通过 TimescaleDB create_hypertable('metric_event', 'event_time') 转为超表。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "metric_event"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_metric_subject_time", "subject_type", "subject_key", "event_time"),
|
||||||
|
Index("ix_metric_name_time", "metric_name", "event_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
|
||||||
|
event_time: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||||
|
subject_type: Mapped[str] = mapped_column(String(32), nullable=False) # 如 msisdn/channel
|
||||||
|
subject_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
metric_name: Mapped[str] = mapped_column(String(64), nullable=False) # 如 traffic_mb/commission
|
||||||
|
metric_value: Mapped[float] = mapped_column(Float, default=0.0)
|
||||||
|
attributes: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
|
||||||
|
data_version_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("data_version.id"), nullable=True
|
||||||
|
)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""审计本体(Ontology)定义。
|
||||||
|
|
||||||
|
定义电信内审域的核心实体类型与关系类型,作为知识图谱与主数据对齐的基准。
|
||||||
|
对应需求 R2。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class EntityType(str, Enum):
|
||||||
|
"""审计本体核心实体类型。"""
|
||||||
|
|
||||||
|
CUSTOMER = "customer" # 客户(含政企)
|
||||||
|
CONTRACT = "contract" # 合同
|
||||||
|
MSISDN = "msisdn" # 号码
|
||||||
|
IMEI = "imei" # 终端设备
|
||||||
|
ACCOUNT = "account" # 账户(付款/收款)
|
||||||
|
WORK_ORDER = "work_order" # 工单
|
||||||
|
SUPPLIER = "supplier" # 供应商
|
||||||
|
SETTLEMENT = "settlement" # 结算单
|
||||||
|
EMPLOYEE = "employee" # 员工
|
||||||
|
CHANNEL = "channel" # 渠道/代理商
|
||||||
|
LEGAL_PERSON = "legal_person" # 法人/自然人
|
||||||
|
ADDRESS = "address" # 地址
|
||||||
|
|
||||||
|
|
||||||
|
class RelationshipType(str, Enum):
|
||||||
|
"""审计本体核心关系类型(有向)。"""
|
||||||
|
|
||||||
|
SIGNED = "signed" # 客户 —签约→ 合同
|
||||||
|
PAID_BY = "paid_by" # 合同 —回款账户→ 账户
|
||||||
|
OWNS_ACCOUNT = "owns_account" # 客户/供应商 —拥有→ 账户
|
||||||
|
REGISTERED_AT = "registered_at" # 客户/供应商 —注册地址→ 地址
|
||||||
|
LEGAL_REP_OF = "legal_rep_of" # 法人 —法定代表人→ 客户/供应商
|
||||||
|
RELATED_TO = "related_to" # 法人 —亲属/关联→ 法人
|
||||||
|
HOLDS_MSISDN = "holds_msisdn" # 客户 —持有→ 号码
|
||||||
|
BOUND_DEVICE = "bound_device" # 号码 —绑定→ IMEI
|
||||||
|
BELONGS_TO_CHANNEL = "belongs_to_channel" # 号码/合同 —归属→ 渠道
|
||||||
|
SUPPLIES = "supplies" # 供应商 —供货→ 合同/工单
|
||||||
|
HANDLED_BY = "handled_by" # 工单 —处理人→ 员工
|
||||||
|
SETTLES = "settles" # 结算单 —结算→ 合同
|
||||||
|
|
||||||
|
|
||||||
|
# 关系的合法 (源实体类型, 目标实体类型) 约束,用于校验图谱写入
|
||||||
|
RELATIONSHIP_DOMAIN: dict[RelationshipType, tuple[set[EntityType], set[EntityType]]] = {
|
||||||
|
RelationshipType.SIGNED: ({EntityType.CUSTOMER}, {EntityType.CONTRACT}),
|
||||||
|
RelationshipType.PAID_BY: ({EntityType.CONTRACT}, {EntityType.ACCOUNT}),
|
||||||
|
RelationshipType.OWNS_ACCOUNT: (
|
||||||
|
{EntityType.CUSTOMER, EntityType.SUPPLIER, EntityType.LEGAL_PERSON},
|
||||||
|
{EntityType.ACCOUNT},
|
||||||
|
),
|
||||||
|
RelationshipType.REGISTERED_AT: (
|
||||||
|
{EntityType.CUSTOMER, EntityType.SUPPLIER},
|
||||||
|
{EntityType.ADDRESS},
|
||||||
|
),
|
||||||
|
RelationshipType.LEGAL_REP_OF: (
|
||||||
|
{EntityType.LEGAL_PERSON},
|
||||||
|
{EntityType.CUSTOMER, EntityType.SUPPLIER},
|
||||||
|
),
|
||||||
|
RelationshipType.RELATED_TO: ({EntityType.LEGAL_PERSON}, {EntityType.LEGAL_PERSON}),
|
||||||
|
RelationshipType.HOLDS_MSISDN: ({EntityType.CUSTOMER}, {EntityType.MSISDN}),
|
||||||
|
RelationshipType.BOUND_DEVICE: ({EntityType.MSISDN}, {EntityType.IMEI}),
|
||||||
|
RelationshipType.BELONGS_TO_CHANNEL: (
|
||||||
|
{EntityType.MSISDN, EntityType.CONTRACT},
|
||||||
|
{EntityType.CHANNEL},
|
||||||
|
),
|
||||||
|
RelationshipType.SUPPLIES: (
|
||||||
|
{EntityType.SUPPLIER},
|
||||||
|
{EntityType.CONTRACT, EntityType.WORK_ORDER},
|
||||||
|
),
|
||||||
|
RelationshipType.HANDLED_BY: ({EntityType.WORK_ORDER}, {EntityType.EMPLOYEE}),
|
||||||
|
RelationshipType.SETTLES: ({EntityType.SETTLEMENT}, {EntityType.CONTRACT}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_relationship(
|
||||||
|
rel: RelationshipType, source: EntityType, target: EntityType
|
||||||
|
) -> bool:
|
||||||
|
"""校验一条关系的源/目标实体类型是否符合本体约束。"""
|
||||||
|
domain = RELATIONSHIP_DOMAIN.get(rel)
|
||||||
|
if domain is None:
|
||||||
|
return False
|
||||||
|
sources, targets = domain
|
||||||
|
return source in sources and target in targets
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""数据库引擎与会话管理。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""所有 ORM 模型的基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
_engine = None
|
||||||
|
_SessionLocal: sessionmaker[Session] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
global _engine
|
||||||
|
if _engine is None:
|
||||||
|
settings = get_settings()
|
||||||
|
_engine = create_engine(settings.database_url, pool_pre_ping=True, future=True)
|
||||||
|
return _engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_sessionmaker() -> sessionmaker[Session]:
|
||||||
|
global _SessionLocal
|
||||||
|
if _SessionLocal is None:
|
||||||
|
_SessionLocal = sessionmaker(bind=get_engine(), expire_on_commit=False)
|
||||||
|
return _SessionLocal
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> Iterator[Session]:
|
||||||
|
"""FastAPI 依赖注入用的会话生成器。"""
|
||||||
|
sm = get_sessionmaker()
|
||||||
|
with sm() as session:
|
||||||
|
yield session
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""LLM Provider 抽象层。
|
||||||
|
|
||||||
|
通过统一接口隔离 LLM 实现,使开发期可用公网千问、生产期无缝切换本地 vLLM。
|
||||||
|
强约束:"数据零出域"红线由 provider 工厂在 prod 环境拦截公网 Provider。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.llm.base import ChatMessage, LLMProvider, LLMResponse
|
||||||
|
from app.llm.factory import get_llm_provider
|
||||||
|
|
||||||
|
__all__ = ["ChatMessage", "LLMProvider", "LLMResponse", "get_llm_provider"]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""LLM Provider 抽象接口与数据模型。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChatMessage:
|
||||||
|
role: str # "system" | "user" | "assistant"
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMResponse:
|
||||||
|
content: str
|
||||||
|
model: str
|
||||||
|
provider: str
|
||||||
|
# 是否经过出域(公网)通道,便于审计轨迹记录
|
||||||
|
egress: bool = False
|
||||||
|
raw: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class LLMProvider(abc.ABC):
|
||||||
|
"""所有 LLM 实现的统一接口。
|
||||||
|
|
||||||
|
业务代码只依赖本接口;切换公网/本地仅改配置,不改调用方。
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: provider 名称
|
||||||
|
name: str = "base"
|
||||||
|
#: 是否走公网(出域)。prod 环境禁止 egress=True 的 provider。
|
||||||
|
egress: bool = False
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||||
|
"""同步对话补全。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def health(self) -> bool:
|
||||||
|
"""探活:provider 是否可用。"""
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""LLM Provider 工厂:按配置创建 provider,并执行数据零出域红线校验。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.config import EGRESS_PROVIDERS, LLMProviderName, Settings, get_settings
|
||||||
|
from app.llm.base import LLMProvider
|
||||||
|
from app.llm.providers import DashScopeProvider, VllmProvider
|
||||||
|
|
||||||
|
|
||||||
|
class EgressPolicyError(RuntimeError):
|
||||||
|
"""数据零出域红线违规。"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_provider(settings: Settings | None = None) -> LLMProvider:
|
||||||
|
settings = settings or get_settings()
|
||||||
|
|
||||||
|
# 红线:prod 环境禁止公网 provider
|
||||||
|
if settings.is_prod and settings.llm_provider in EGRESS_PROVIDERS:
|
||||||
|
raise EgressPolicyError(
|
||||||
|
f"数据零出域红线违规:prod 环境禁止使用公网 LLM Provider "
|
||||||
|
f"'{settings.llm_provider.value}'。"
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.llm_provider == LLMProviderName.dashscope:
|
||||||
|
return DashScopeProvider(
|
||||||
|
api_key=settings.dashscope_api_key, model=settings.dashscope_model
|
||||||
|
)
|
||||||
|
if settings.llm_provider == LLMProviderName.vllm:
|
||||||
|
return VllmProvider(base_url=settings.vllm_base_url, model=settings.vllm_model)
|
||||||
|
|
||||||
|
raise ValueError(f"未知的 LLM Provider: {settings.llm_provider}")
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""具体 LLM Provider 实现:DashScope(公网千问,仅 dev)、vLLM(本地,prod)。
|
||||||
|
|
||||||
|
两者均走 OpenAI 兼容的 /chat/completions 协议。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.llm.base import ChatMessage, LLMProvider, LLMResponse
|
||||||
|
|
||||||
|
|
||||||
|
class DashScopeProvider(LLMProvider):
|
||||||
|
"""公网千问(DashScope,OpenAI 兼容模式)。仅限开发测试,且只允许脱敏/样例假数据。"""
|
||||||
|
|
||||||
|
name = "dashscope"
|
||||||
|
egress = True # 走公网,出域
|
||||||
|
|
||||||
|
_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, model: str, timeout: float = 30.0) -> None:
|
||||||
|
self._api_key = api_key
|
||||||
|
self._model = model
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||||
|
payload = {
|
||||||
|
"model": self._model,
|
||||||
|
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||||
|
with httpx.Client(timeout=self._timeout) as client:
|
||||||
|
resp = client.post(
|
||||||
|
f"{self._BASE_URL}/chat/completions", json=payload, headers=headers
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
content = data["choices"][0]["message"]["content"]
|
||||||
|
return LLMResponse(
|
||||||
|
content=content, model=self._model, provider=self.name, egress=True, raw=data
|
||||||
|
)
|
||||||
|
|
||||||
|
def health(self) -> bool:
|
||||||
|
return bool(self._api_key)
|
||||||
|
|
||||||
|
|
||||||
|
class VllmProvider(LLMProvider):
|
||||||
|
"""本地 vLLM(OpenAI 兼容)。生产使用,数据不出域。"""
|
||||||
|
|
||||||
|
name = "vllm"
|
||||||
|
egress = False
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, model: str, timeout: float = 60.0) -> None:
|
||||||
|
self._base_url = base_url.rstrip("/")
|
||||||
|
self._model = model
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||||
|
payload = {
|
||||||
|
"model": self._model,
|
||||||
|
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
with httpx.Client(timeout=self._timeout) as client:
|
||||||
|
resp = client.post(f"{self._base_url}/chat/completions", json=payload)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
content = data["choices"][0]["message"]["content"]
|
||||||
|
return LLMResponse(
|
||||||
|
content=content, model=self._model, provider=self.name, egress=False, raw=data
|
||||||
|
)
|
||||||
|
|
||||||
|
def health(self) -> bool:
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=5.0) as client:
|
||||||
|
resp = client.get(f"{self._base_url}/models")
|
||||||
|
return resp.status_code == 200
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""AIAudit FastAPI 应用入口。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from app import __version__
|
||||||
|
from app.api.datahub import router as datahub_router
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# 启动时执行数据零出域红线校验,违规则阻断启动
|
||||||
|
settings = get_settings()
|
||||||
|
settings.validate_egress_policy()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="AIAudit · 本地 AI 内审平台",
|
||||||
|
version=__version__,
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(datahub_router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health() -> dict:
|
||||||
|
"""存活探针。"""
|
||||||
|
return {"status": "ok", "version": __version__}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health/config")
|
||||||
|
def health_config() -> dict:
|
||||||
|
"""配置/合规探针:暴露环境与 LLM provider 出域状态(不含密钥)。"""
|
||||||
|
settings = get_settings()
|
||||||
|
return {
|
||||||
|
"env": settings.aiaudit_env.value,
|
||||||
|
"llm_provider": settings.llm_provider.value,
|
||||||
|
"egress_blocked_in_prod": settings.is_prod,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 数据库迁移(Alembic)
|
||||||
|
|
||||||
|
- 生成迁移:`alembic revision --autogenerate -m "描述"`
|
||||||
|
- 应用迁移:`alembic upgrade head`
|
||||||
|
- 回滚一步:`alembic downgrade -1`
|
||||||
|
|
||||||
|
模型定义见 `app/datahub/models.py`;连接串取自应用配置(`DATABASE_URL`)。
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Alembic 迁移环境。
|
||||||
|
|
||||||
|
从应用配置读取数据库 URL,并以 app.db.Base 的元数据作为 autogenerate 目标。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
|
||||||
|
from app.audit import models as audit_models # noqa: F401,E402
|
||||||
|
from app.clues import models as clue_models # noqa: F401,E402
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
# 导入模型以注册到 Base.metadata
|
||||||
|
from app.datahub import models # noqa: F401,E402
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# 用应用配置覆盖 sqlalchemy.url
|
||||||
|
config.set_main_option("sqlalchemy.url", get_settings().database_url)
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""初始化数据中台表:数据版本 / 实体 / 关系 / 双时态事实 / 时序事件
|
||||||
|
|
||||||
|
Revision ID: 0001_init_datahub
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-06
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision: str = "0001_init_datahub"
|
||||||
|
down_revision: str | None = None
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# data_version
|
||||||
|
op.create_table(
|
||||||
|
"data_version",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("source_system", sa.String(64), nullable=False),
|
||||||
|
sa.Column("batch_label", sa.String(128), nullable=False),
|
||||||
|
sa.Column("row_count", sa.Integer(), nullable=False, server_default="0"),
|
||||||
|
sa.Column("ingested_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("note", sa.Text(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# entity
|
||||||
|
op.create_table(
|
||||||
|
"entity",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("entity_type", sa.String(32), nullable=False),
|
||||||
|
sa.Column("business_key", sa.String(128), nullable=False),
|
||||||
|
sa.Column("display_name", sa.String(256), nullable=True),
|
||||||
|
sa.Column("attributes", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("canonical_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column("data_version_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["canonical_id"], ["entity.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["data_version_id"], ["data_version.id"]),
|
||||||
|
sa.UniqueConstraint("entity_type", "business_key", name="uq_entity_type_bizkey"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_entity_type", "entity", ["entity_type"])
|
||||||
|
|
||||||
|
# entity_relationship
|
||||||
|
op.create_table(
|
||||||
|
"entity_relationship",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("rel_type", sa.String(32), nullable=False),
|
||||||
|
sa.Column("source_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("target_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("attributes", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("data_version_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["source_id"], ["entity.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["target_id"], ["entity.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["data_version_id"], ["data_version.id"]),
|
||||||
|
)
|
||||||
|
op.create_index("ix_rel_source", "entity_relationship", ["source_id"])
|
||||||
|
op.create_index("ix_rel_target", "entity_relationship", ["target_id"])
|
||||||
|
op.create_index("ix_rel_type", "entity_relationship", ["rel_type"])
|
||||||
|
|
||||||
|
# bitemporal_fact
|
||||||
|
op.create_table(
|
||||||
|
"bitemporal_fact",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("attr_name", sa.String(64), nullable=False),
|
||||||
|
sa.Column("attr_value", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("system_from", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("system_to", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("data_version_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["data_version_id"], ["data_version.id"]),
|
||||||
|
)
|
||||||
|
op.create_index("ix_btf_entity_attr", "bitemporal_fact", ["entity_id", "attr_name"])
|
||||||
|
|
||||||
|
# metric_event(时序)
|
||||||
|
op.create_table(
|
||||||
|
"metric_event",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("event_time", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("subject_type", sa.String(32), nullable=False),
|
||||||
|
sa.Column("subject_key", sa.String(128), nullable=False),
|
||||||
|
sa.Column("metric_name", sa.String(64), nullable=False),
|
||||||
|
sa.Column("metric_value", sa.Float(), nullable=False, server_default="0"),
|
||||||
|
sa.Column("attributes", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("data_version_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
# 超表主键需包含分区列 event_time
|
||||||
|
sa.PrimaryKeyConstraint("id", "event_time"),
|
||||||
|
sa.ForeignKeyConstraint(["data_version_id"], ["data_version.id"]),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_metric_subject_time",
|
||||||
|
"metric_event",
|
||||||
|
["subject_type", "subject_key", "event_time"],
|
||||||
|
)
|
||||||
|
op.create_index("ix_metric_name_time", "metric_event", ["metric_name", "event_time"])
|
||||||
|
|
||||||
|
# 转为 TimescaleDB 超表(若扩展不存在则跳过,便于无 timescaledb 环境运行测试)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN
|
||||||
|
PERFORM create_hypertable('metric_event', 'event_time', if_not_exists => TRUE);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 双时态排他约束:同一实体同一属性,业务有效期不重叠(需 btree_gist)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'btree_gist') THEN
|
||||||
|
ALTER TABLE bitemporal_fact
|
||||||
|
ADD CONSTRAINT ex_btf_no_overlap
|
||||||
|
EXCLUDE USING gist (
|
||||||
|
entity_id WITH =,
|
||||||
|
attr_name WITH =,
|
||||||
|
tstzrange(valid_from, valid_to) WITH &&
|
||||||
|
) WHERE (system_to IS NULL);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("metric_event")
|
||||||
|
op.drop_table("bitemporal_fact")
|
||||||
|
op.drop_table("entity_relationship")
|
||||||
|
op.drop_index("ix_entity_type", table_name="entity")
|
||||||
|
op.drop_table("entity")
|
||||||
|
op.drop_table("data_version")
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""线索引擎与系统自审计表:clue / clue_status_history / working_paper / audit_log
|
||||||
|
|
||||||
|
Revision ID: 0002_clues_audit
|
||||||
|
Revises: 0001_init_datahub
|
||||||
|
Create Date: 2026-06
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision: str = "0002_clues_audit"
|
||||||
|
down_revision: str | None = "0001_init_datahub"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
confidence_tier = postgresql.ENUM(
|
||||||
|
"high", "medium", "low", name="confidence_tier", create_type=False
|
||||||
|
)
|
||||||
|
clue_status = postgresql.ENUM(
|
||||||
|
"new", "assigned", "reviewing", "confirmed", "dismissed",
|
||||||
|
"rectifying", "transferred", "closed", name="clue_status", create_type=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
confidence_tier.create(bind, checkfirst=True)
|
||||||
|
clue_status.create(bind, checkfirst=True)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"clue",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("title", sa.String(256), nullable=False),
|
||||||
|
sa.Column("risk_domain", sa.String(32), nullable=False),
|
||||||
|
sa.Column("scenario_code", sa.String(32), nullable=False),
|
||||||
|
sa.Column("confidence", confidence_tier, nullable=False),
|
||||||
|
sa.Column("score", sa.Float(), nullable=False, server_default="0"),
|
||||||
|
sa.Column("status", clue_status, nullable=False, server_default="new"),
|
||||||
|
sa.Column("rationale", sa.Text(), nullable=False, server_default=""),
|
||||||
|
sa.Column("evidence", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("subjects", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("amount_involved", sa.Float(), nullable=True),
|
||||||
|
sa.Column("assignee", sa.String(64), nullable=True),
|
||||||
|
sa.Column("feedback", sa.String(16), nullable=True),
|
||||||
|
sa.Column("model_version", sa.String(64), nullable=True),
|
||||||
|
sa.Column("rule_version", sa.String(64), nullable=True),
|
||||||
|
sa.Column("data_version_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
)
|
||||||
|
op.create_index("ix_clue_status", "clue", ["status"])
|
||||||
|
op.create_index("ix_clue_scenario", "clue", ["scenario_code"])
|
||||||
|
op.create_index("ix_clue_assignee", "clue", ["assignee"])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"clue_status_history",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("clue_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("from_status", sa.String(16), nullable=True),
|
||||||
|
sa.Column("to_status", sa.String(16), nullable=False),
|
||||||
|
sa.Column("actor", sa.String(64), nullable=False),
|
||||||
|
sa.Column("note", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["clue_id"], ["clue.id"]),
|
||||||
|
)
|
||||||
|
op.create_index("ix_csh_clue", "clue_status_history", ["clue_id"])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"working_paper",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("clue_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column("content", sa.Text(), nullable=False, server_default=""),
|
||||||
|
sa.Column("conclusion", sa.String(32), nullable=True),
|
||||||
|
sa.Column("author", sa.String(64), nullable=False),
|
||||||
|
sa.Column("snapshot", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["clue_id"], ["clue.id"]),
|
||||||
|
)
|
||||||
|
op.create_index("ix_wp_clue", "working_paper", ["clue_id"])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"audit_log",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("seq", sa.BigInteger(), sa.Identity(always=False), nullable=False),
|
||||||
|
sa.Column("actor", sa.String(64), nullable=False),
|
||||||
|
sa.Column("role", sa.String(32), nullable=True),
|
||||||
|
sa.Column("action", sa.String(64), nullable=False),
|
||||||
|
sa.Column("target_type", sa.String(64), nullable=True),
|
||||||
|
sa.Column("target_id", sa.String(128), nullable=True),
|
||||||
|
sa.Column("detail", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("prev_hash", sa.String(64), nullable=True),
|
||||||
|
sa.Column("entry_hash", sa.String(64), nullable=False),
|
||||||
|
)
|
||||||
|
op.create_index("ix_audit_actor", "audit_log", ["actor"])
|
||||||
|
op.create_index("ix_audit_action", "audit_log", ["action"])
|
||||||
|
op.create_index("ix_audit_seq", "audit_log", ["seq"], unique=True)
|
||||||
|
|
||||||
|
# R19:禁止物理删除线索与审计日志(数据库级触发器兜底)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION forbid_delete() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
RAISE EXCEPTION '禁止删除:% 表受 R19 不可删除约束保护', TG_TABLE_NAME;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"CREATE TRIGGER trg_clue_no_delete BEFORE DELETE ON clue "
|
||||||
|
"FOR EACH ROW EXECUTE FUNCTION forbid_delete();"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"CREATE TRIGGER trg_audit_no_delete BEFORE DELETE ON audit_log "
|
||||||
|
"FOR EACH ROW EXECUTE FUNCTION forbid_delete();"
|
||||||
|
)
|
||||||
|
# 审计日志禁止更新(仅追加)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION forbid_update() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
RAISE EXCEPTION '禁止更新:% 表为仅追加日志', TG_TABLE_NAME;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"CREATE TRIGGER trg_audit_no_update BEFORE UPDATE ON audit_log "
|
||||||
|
"FOR EACH ROW EXECUTE FUNCTION forbid_update();"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS trg_audit_no_update ON audit_log;")
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS trg_audit_no_delete ON audit_log;")
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS trg_clue_no_delete ON clue;")
|
||||||
|
op.drop_table("audit_log")
|
||||||
|
op.drop_table("working_paper")
|
||||||
|
op.drop_table("clue_status_history")
|
||||||
|
op.drop_table("clue")
|
||||||
|
clue_status.drop(op.get_bind(), checkfirst=True)
|
||||||
|
confidence_tier.drop(op.get_bind(), checkfirst=True)
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS forbid_update();")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS forbid_delete();")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[project]
|
||||||
|
name = "aiaudit-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "AIAudit 本地 AI 内审平台后端"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py311"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "W", "UP", "B"]
|
||||||
|
# B008:FastAPI 依赖注入 Depends() 作为默认值是官方推荐用法
|
||||||
|
ignore = ["B008"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.11"
|
||||||
|
ignore_missing_imports = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
pytest==8.3.4
|
||||||
|
pytest-asyncio==0.25.0
|
||||||
|
ruff==0.8.4
|
||||||
|
mypy==1.14.0
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn[standard]==0.34.0
|
||||||
|
pydantic==2.10.4
|
||||||
|
pydantic-settings==2.7.1
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
psycopg[binary]==3.2.3
|
||||||
|
alembic==1.14.0
|
||||||
|
celery==5.4.0
|
||||||
|
redis==5.2.1
|
||||||
|
httpx==0.28.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""集成测试 fixture:连接本地 PostgreSQL 16,按事务隔离并回滚。
|
||||||
|
|
||||||
|
需要可连接的数据库(DATABASE_URL)。无法连接时跳过整组集成测试。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
|
from app.db import get_engine
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def db_available() -> bool:
|
||||||
|
try:
|
||||||
|
with get_engine().connect() as conn:
|
||||||
|
conn.execute(text("SELECT 1"))
|
||||||
|
return True
|
||||||
|
except OperationalError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def session(db_available):
|
||||||
|
if not db_available:
|
||||||
|
pytest.skip("数据库不可用,跳过集成测试")
|
||||||
|
engine = get_engine()
|
||||||
|
connection = engine.connect()
|
||||||
|
trans = connection.begin()
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
sess = Session(bind=connection)
|
||||||
|
try:
|
||||||
|
yield sess
|
||||||
|
finally:
|
||||||
|
sess.close()
|
||||||
|
if trans.is_active:
|
||||||
|
trans.rollback()
|
||||||
|
connection.close()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""双时态集成测试(需 PostgreSQL)。
|
||||||
|
|
||||||
|
验证 R3:按历史业务时点回放属性值,以及双时态排他约束防止有效期重叠。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from app.datahub import bitemporal_repo as btr
|
||||||
|
from app.datahub.graph_repo import upsert_entity
|
||||||
|
from app.datahub.ontology import EntityType
|
||||||
|
|
||||||
|
|
||||||
|
def test_bitemporal_replay(session):
|
||||||
|
"""不同业务时点回放出不同的属性值。"""
|
||||||
|
cust = upsert_entity(session, EntityType.CUSTOMER, "CUST_BT", "丁公司")
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
t1 = dt.datetime(2025, 1, 1, tzinfo=dt.UTC)
|
||||||
|
t2 = dt.datetime(2025, 6, 1, tzinfo=dt.UTC)
|
||||||
|
|
||||||
|
btr.record_fact(session, cust.id, "credit_level", {"v": "A"}, valid_from=t1, valid_to=t2)
|
||||||
|
btr.record_fact(session, cust.id, "credit_level", {"v": "C"}, valid_from=t2)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
early = btr.as_of(session, cust.id, "credit_level", dt.datetime(2025, 3, 1, tzinfo=dt.UTC))
|
||||||
|
late = btr.as_of(session, cust.id, "credit_level", dt.datetime(2025, 9, 1, tzinfo=dt.UTC))
|
||||||
|
assert early is not None and early.attr_value["v"] == "A"
|
||||||
|
assert late is not None and late.attr_value["v"] == "C"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bitemporal_exclusion_constraint(session):
|
||||||
|
"""同一实体同一属性的业务有效期重叠应被排他约束拒绝。"""
|
||||||
|
cust = upsert_entity(session, EntityType.CUSTOMER, "CUST_EX", "戊公司")
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
t1 = dt.datetime(2025, 1, 1, tzinfo=dt.UTC)
|
||||||
|
t3 = dt.datetime(2025, 12, 1, tzinfo=dt.UTC)
|
||||||
|
t2 = dt.datetime(2025, 6, 1, tzinfo=dt.UTC)
|
||||||
|
|
||||||
|
btr.record_fact(session, cust.id, "status", {"v": "active"}, valid_from=t1, valid_to=t3)
|
||||||
|
session.flush()
|
||||||
|
# 与上一条 [t1,t3) 重叠:record_fact 内部 flush 时即触发排他约束
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
btr.record_fact(session, cust.id, "status", {"v": "frozen"}, valid_from=t2, valid_to=None)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""线索闭环 + 系统自审计集成测试(需 PostgreSQL)。
|
||||||
|
|
||||||
|
覆盖 R7/R17/R18/R19:线索生成与分级、状态流转、底稿、审计哈希链、线索不可删。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.exc import InternalError, ProgrammingError
|
||||||
|
|
||||||
|
from app.audit import service as audit
|
||||||
|
from app.clues import service as clue_svc
|
||||||
|
from app.clues.models import ClueStatus, ConfidenceTier
|
||||||
|
|
||||||
|
|
||||||
|
def _new_clue(session, score=0.9):
|
||||||
|
return clue_svc.create_clue(
|
||||||
|
session,
|
||||||
|
title="疑似政企拆单",
|
||||||
|
risk_domain="收入",
|
||||||
|
scenario_code="R8",
|
||||||
|
score=score,
|
||||||
|
rationale="8 个客户金额集中在审批阈值边缘,且法人关联同一实控人",
|
||||||
|
evidence={"contracts": 8, "threshold": 1000000},
|
||||||
|
amount_involved=4800000,
|
||||||
|
actor="system",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_to_confidence_tier():
|
||||||
|
assert clue_svc.score_to_tier(0.9) == ConfidenceTier.HIGH
|
||||||
|
assert clue_svc.score_to_tier(0.6) == ConfidenceTier.MEDIUM
|
||||||
|
assert clue_svc.score_to_tier(0.2) == ConfidenceTier.LOW
|
||||||
|
|
||||||
|
|
||||||
|
def test_clue_full_lifecycle(session):
|
||||||
|
clue = _new_clue(session)
|
||||||
|
assert clue.confidence == ConfidenceTier.HIGH
|
||||||
|
assert clue.status == ClueStatus.NEW
|
||||||
|
|
||||||
|
clue_svc.assign(session, clue, assignee="auditor_zhang", actor="manager_li")
|
||||||
|
assert clue.status == ClueStatus.ASSIGNED
|
||||||
|
assert clue.assignee == "auditor_zhang"
|
||||||
|
|
||||||
|
paper = clue_svc.adjudicate(session, clue, confirmed=True, actor="auditor_zhang", note="属实,移交")
|
||||||
|
assert clue.status == ClueStatus.CONFIRMED
|
||||||
|
assert clue.feedback == "confirmed"
|
||||||
|
assert paper.conclusion == "confirmed"
|
||||||
|
assert paper.snapshot["score"] == 0.9
|
||||||
|
|
||||||
|
# 继续闭环:确认 -> 移交 -> 销项
|
||||||
|
clue_svc.transition(session, clue, ClueStatus.TRANSFERRED, actor="manager_li")
|
||||||
|
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="manager_li")
|
||||||
|
assert clue.status == ClueStatus.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
def test_illegal_transition_rejected(session):
|
||||||
|
clue = _new_clue(session)
|
||||||
|
with pytest.raises(clue_svc.IllegalTransitionError):
|
||||||
|
# NEW 不能直接到 CLOSED
|
||||||
|
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="x")
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_hash_chain_integrity(session):
|
||||||
|
_new_clue(session)
|
||||||
|
clue = _new_clue(session)
|
||||||
|
clue_svc.assign(session, clue, "auditor_zhang", "manager_li")
|
||||||
|
ok, broken = audit.verify_chain(session)
|
||||||
|
assert ok is True
|
||||||
|
assert broken is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_clue_cannot_be_deleted(session):
|
||||||
|
"""R19:数据库触发器禁止物理删除线索。"""
|
||||||
|
clue = _new_clue(session)
|
||||||
|
session.flush()
|
||||||
|
with pytest.raises((InternalError, ProgrammingError)):
|
||||||
|
session.execute(text("DELETE FROM clue WHERE id = :i"), {"i": clue.id})
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_clues_filters(session):
|
||||||
|
_new_clue(session, score=0.9)
|
||||||
|
_new_clue(session, score=0.3)
|
||||||
|
highs = clue_svc.list_clues(session, confidence=ConfidenceTier.HIGH)
|
||||||
|
assert all(c.confidence == ConfidenceTier.HIGH for c in highs)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""数据中台穿透 API 集成测试(需 PostgreSQL)。
|
||||||
|
|
||||||
|
通过 TestClient 调用 /datahub/penetrate,验证统一穿透查询服务端到端可用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.datahub.graph_repo import add_relationship, upsert_entity
|
||||||
|
from app.datahub.ontology import EntityType, RelationshipType
|
||||||
|
from app.db import get_session
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(session):
|
||||||
|
# 用集成测试的事务化 session 覆盖应用依赖,保证测试数据回滚
|
||||||
|
app.dependency_overrides[get_session] = lambda: session
|
||||||
|
try:
|
||||||
|
yield TestClient(app)
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.pop(get_session, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_penetrate_endpoint_detects_related(client, session):
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
controller = upsert_entity(session, EntityType.LEGAL_PERSON, f"CTRL-{suffix}", "实控人")
|
||||||
|
cust = upsert_entity(session, EntityType.CUSTOMER, f"CUST-{suffix}", "政企客户")
|
||||||
|
rep = upsert_entity(session, EntityType.LEGAL_PERSON, f"REP-{suffix}", "法人")
|
||||||
|
add_relationship(session, RelationshipType.LEGAL_REP_OF, rep, cust)
|
||||||
|
add_relationship(session, RelationshipType.RELATED_TO, rep, controller)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/datahub/penetrate",
|
||||||
|
json={"start_entity_id": str(controller.id), "max_depth": 3},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
related_ids = {r["entity"]["id"] for r in body["related"]}
|
||||||
|
assert str(cust.id) in related_ids
|
||||||
|
assert body["related_count"] >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_penetrate_unknown_entity_404(client):
|
||||||
|
resp = client.post(
|
||||||
|
"/datahub/penetrate",
|
||||||
|
json={"start_entity_id": str(uuid.uuid4()), "max_depth": 2},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_entity_endpoint(client, session):
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
e = upsert_entity(session, EntityType.SUPPLIER, f"SUP-{suffix}", "供应商甲")
|
||||||
|
session.flush()
|
||||||
|
resp = client.get(f"/datahub/entities/{e.id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["business_key"] == f"SUP-{suffix}"
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""知识图谱穿透集成测试(需 PostgreSQL)。
|
||||||
|
|
||||||
|
验证 R2 关键能力:通过关系边的多跳穿透识别"疑似同一实控人",
|
||||||
|
以及本体约束对非法关系的拒绝。对应场景一(政企拆单+隐性实控人,R8)的图谱基础。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.datahub.graph_repo import (
|
||||||
|
OntologyViolationError,
|
||||||
|
add_relationship,
|
||||||
|
find_related_entities,
|
||||||
|
upsert_entity,
|
||||||
|
)
|
||||||
|
from app.datahub.ontology import EntityType, RelationshipType
|
||||||
|
|
||||||
|
|
||||||
|
def test_upsert_entity_is_idempotent(session):
|
||||||
|
e1 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲")
|
||||||
|
e2 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲")
|
||||||
|
assert e1.id == e2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_ontology_violation_rejected(session):
|
||||||
|
contract = upsert_entity(session, EntityType.CONTRACT, "C-1")
|
||||||
|
customer = upsert_entity(session, EntityType.CUSTOMER, "CUST-2")
|
||||||
|
# 合同 —签约→ 客户 方向非法
|
||||||
|
with pytest.raises(OntologyViolationError):
|
||||||
|
add_relationship(session, RelationshipType.SIGNED, contract, customer)
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_shared_controller_across_customers(session):
|
||||||
|
"""模拟"8 个客户疑似同一实控人":多个客户经法人关联到同一实控自然人。
|
||||||
|
|
||||||
|
构图:每个客户 <-法定代表人- 各自法人;各法人 -关联-> 同一实控人。
|
||||||
|
从实控人出发,应能穿透到全部客户。
|
||||||
|
"""
|
||||||
|
controller = upsert_entity(session, EntityType.LEGAL_PERSON, "PER-CTRL", "实控人")
|
||||||
|
|
||||||
|
customers = []
|
||||||
|
for i in range(8):
|
||||||
|
cust = upsert_entity(session, EntityType.CUSTOMER, f"CUST-{i}", f"政企客户{i}")
|
||||||
|
rep = upsert_entity(session, EntityType.LEGAL_PERSON, f"PER-{i}", f"法人{i}")
|
||||||
|
# 法人 —法定代表人→ 客户
|
||||||
|
add_relationship(session, RelationshipType.LEGAL_REP_OF, rep, cust)
|
||||||
|
# 法人 —关联(亲属/实控)→ 实控人
|
||||||
|
add_relationship(session, RelationshipType.RELATED_TO, rep, controller)
|
||||||
|
customers.append(cust)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
related = find_related_entities(session, controller.id, max_depth=3)
|
||||||
|
related_ids = {rid for rid, _ in related}
|
||||||
|
|
||||||
|
# 从实控人 3 跳内应能穿透到全部 8 个客户
|
||||||
|
for cust in customers:
|
||||||
|
assert cust.id in related_ids, f"未穿透到 {cust.business_key}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_traversal_respects_max_depth(session):
|
||||||
|
a = upsert_entity(session, EntityType.LEGAL_PERSON, "A")
|
||||||
|
b = upsert_entity(session, EntityType.LEGAL_PERSON, "B")
|
||||||
|
c = upsert_entity(session, EntityType.CUSTOMER, "C")
|
||||||
|
add_relationship(session, RelationshipType.RELATED_TO, a, b)
|
||||||
|
add_relationship(session, RelationshipType.LEGAL_REP_OF, b, c)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# depth=1:从 A 只能到 B,到不了 C
|
||||||
|
ids_d1 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=1)}
|
||||||
|
assert b.id in ids_d1
|
||||||
|
assert c.id not in ids_d1
|
||||||
|
|
||||||
|
# depth=2:能到 C
|
||||||
|
ids_d2 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=2)}
|
||||||
|
assert c.id in ids_d2
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""数据零出域红线测试:prod 环境必须禁用公网 LLM Provider。"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.config import AppEnv, LLMProviderName, Settings
|
||||||
|
from app.llm.factory import EgressPolicyError, get_llm_provider
|
||||||
|
|
||||||
|
|
||||||
|
def _settings(env: AppEnv, provider: LLMProviderName) -> Settings:
|
||||||
|
return Settings(aiaudit_env=env, llm_provider=provider, dashscope_api_key="x")
|
||||||
|
|
||||||
|
|
||||||
|
def test_prod_blocks_public_dashscope():
|
||||||
|
s = _settings(AppEnv.prod, LLMProviderName.dashscope)
|
||||||
|
with pytest.raises(EgressPolicyError):
|
||||||
|
get_llm_provider(s)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prod_allows_local_vllm():
|
||||||
|
s = _settings(AppEnv.prod, LLMProviderName.vllm)
|
||||||
|
provider = get_llm_provider(s)
|
||||||
|
assert provider.name == "vllm"
|
||||||
|
assert provider.egress is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_dev_allows_dashscope():
|
||||||
|
s = _settings(AppEnv.dev, LLMProviderName.dashscope)
|
||||||
|
provider = get_llm_provider(s)
|
||||||
|
assert provider.name == "dashscope"
|
||||||
|
assert provider.egress is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_egress_policy_raises_in_prod():
|
||||||
|
s = _settings(AppEnv.prod, LLMProviderName.dashscope)
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
s.validate_egress_policy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_egress_policy_ok_in_dev():
|
||||||
|
s = _settings(AppEnv.dev, LLMProviderName.dashscope)
|
||||||
|
# dev 下不应抛出
|
||||||
|
s.validate_egress_policy()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""健康检查端点测试。"""
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_ok():
|
||||||
|
resp = client.get("/health")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_config():
|
||||||
|
resp = client.get("/health/config")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert "env" in body
|
||||||
|
assert "llm_provider" in body
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""审计本体约束测试(无需数据库)。"""
|
||||||
|
|
||||||
|
from app.datahub.ontology import EntityType, RelationshipType, is_valid_relationship
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_signed_relationship():
|
||||||
|
assert is_valid_relationship(
|
||||||
|
RelationshipType.SIGNED, EntityType.CUSTOMER, EntityType.CONTRACT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_signed_direction():
|
||||||
|
# 合同不能"签约"客户(方向反了)
|
||||||
|
assert not is_valid_relationship(
|
||||||
|
RelationshipType.SIGNED, EntityType.CONTRACT, EntityType.CUSTOMER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legal_rep_relationship():
|
||||||
|
assert is_valid_relationship(
|
||||||
|
RelationshipType.LEGAL_REP_OF, EntityType.LEGAL_PERSON, EntityType.SUPPLIER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_related_to_between_legal_persons():
|
||||||
|
# 实控人关联识别的基础:法人之间的亲属/关联关系
|
||||||
|
assert is_valid_relationship(
|
||||||
|
RelationshipType.RELATED_TO, EntityType.LEGAL_PERSON, EntityType.LEGAL_PERSON
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_relationship_wrong_target():
|
||||||
|
assert not is_valid_relationship(
|
||||||
|
RelationshipType.HOLDS_MSISDN, EntityType.CUSTOMER, EntityType.CONTRACT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_relationship_types_have_domain():
|
||||||
|
from app.datahub.ontology import RELATIONSHIP_DOMAIN
|
||||||
|
|
||||||
|
for rel in RelationshipType:
|
||||||
|
assert rel in RELATIONSHIP_DOMAIN, f"关系 {rel} 缺少本体域定义"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# ADR-0001 · 技术选型决策记录
|
||||||
|
|
||||||
|
> 项目:AIAudit(本地私有化大模型电信运营商 AI 全域内审平台)
|
||||||
|
> 状态:已接受(MVP 基线)
|
||||||
|
> 日期:2026-06
|
||||||
|
> 关联:`0-req-AIAudit.md`、`1-prd-AIAudit.md`、`2-task-AIAudit.md`(任务 P0.1)
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
平台需在本地内网部署、数据零出域,具备本体/知识图谱、双时态/时序、本地 LLM 推理、向量检索等能力,并需信创适配。技术选型需在"能力完整"与"组件最少、出域面最小、便于信创"之间取得平衡。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
| 层 | 选型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 后端 | Python 3.12 + FastAPI | 贴近 LLM/数据/ML 生态,异步任务友好 |
|
||||||
|
| 前端 | React + TypeScript + Vite | 看板/下钻交互成熟 |
|
||||||
|
| 主存储 | PostgreSQL 16 | 一库多能,降低组件数与出域面 |
|
||||||
|
| 知识图谱 | PostgreSQL + Apache AGE | 免独立图库,信创友好,满足 MVP 多跳穿透 |
|
||||||
|
| 双时态/时序 | PostgreSQL 时态列 + TimescaleDB | 双时态回放 + 时间序列一体 |
|
||||||
|
| 向量检索 | pgvector | 与主库同栈,免独立向量库 |
|
||||||
|
| 任务调度 | Celery + Redis | 全量扫描异步任务、进度反馈 |
|
||||||
|
| 文件/对象 | MinIO(本地 S3) | 凭证/底稿存储,不出域 |
|
||||||
|
| LLM 推理 | Provider 抽象:开发期 DashScope 公网千问;生产 vLLM + 本地 70B | 见下"LLM 抽象与红线" |
|
||||||
|
| 部署 | 本地 Homebrew 安装(开发)→ 生产内网裸机/信创环境 | 不使用 Docker;开发直接用本机 PostgreSQL 16 + 本地服务 |
|
||||||
|
|
||||||
|
## LLM 抽象与数据零出域红线(关键约束)
|
||||||
|
- LLM 通过统一 `LLMProvider` 接口接入,至少实现两种:`DashScopeProvider`(公网千问,**仅开发/测试**)、`VllmProvider`(本地,生产)。
|
||||||
|
- **红线:公网 Provider 只允许处理脱敏/样例假数据,严禁传入任何真实审计数据。** 通过配置开关 + 环境标识(dev/prod)强约束;prod 环境禁用任何公网 Provider。
|
||||||
|
- 切换 Provider 仅改配置,不改业务代码。
|
||||||
|
|
||||||
|
## 本机环境结论(开发机)
|
||||||
|
- Mac mini · Apple M4 · 16GB · macOS 26.5.1(ARM64);磁盘可用 ~170GB。
|
||||||
|
- 开发 MVP 够用(样例数据 + 公网千问 API + Docker 组件)。
|
||||||
|
- **不能本地运行 70B**;生产推理需独立 GPU 服务器(A100/H100/国产 GPU)跑 vLLM。
|
||||||
|
|
||||||
|
## 备选与未选原因
|
||||||
|
- 独立 Neo4j 图库:能力更强但增加组件与信创/授权负担,MVP 暂不引入;图谱压力增大时再评估。
|
||||||
|
- 独立时序库 / 独立向量库:同理,先用 PG 一体化,后续按压力拆分。
|
||||||
|
- 后端 Java Spring Boot:企业集成习惯好,但 LLM/数据/ML 生态以 Python 为主,会多一层;若团队为 Java 班底可改为"Java 主服务 + Python 分析/推理服务"。
|
||||||
|
|
||||||
|
## 影响
|
||||||
|
- 开发环境:本机 Homebrew 安装 PostgreSQL 16 + TimescaleDB + pgvector,不使用 Docker(已移除 docker-compose 与自定义镜像);初始化脚本见 `infra/postgres/setup_local.sh`。
|
||||||
|
- 生产部署需规划独立 GPU 推理节点(任务 P3.5 信创适配同步评估)。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ADR-0002 · 数据中台建模决策(本体 / 双时态 / 时序 / 图谱)
|
||||||
|
|
||||||
|
> 项目:AIAudit 状态:已接受(MVP) 日期:2026-06
|
||||||
|
> 关联:`0-req-AIAudit.md`(R2、R3)、任务 P1.2 / P1.3
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
审计数据中台需同时满足:①按审计本体组织实体与关系(知识图谱穿透);②双时态建模(业务时间+系统时间,可回放历史);③时间序列(时序造假识别)。需在能力与可靠性/组件数之间平衡。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
### 1. 本体与知识图谱:关系表 + 递归 CTE(MVP)
|
||||||
|
- 用 `entity`(实体)+ `entity_relationship`(关系边)两张通用表承载审计本体,实体类型与关系类型由 `ontology_entity_type` / `ontology_relationship_type` 字典定义。
|
||||||
|
- 多跳穿透(如实控人识别)用 PostgreSQL **递归 CTE** 实现。
|
||||||
|
- 不在 MVP 引入 Apache AGE,规避源码编译的构建脆弱性;后续多跳压力增大再评估迁移到 AGE/Neo4j。
|
||||||
|
|
||||||
|
### 2. 双时态建模
|
||||||
|
- 关键审计对象采用双时态:
|
||||||
|
- 业务有效期:`valid_from` / `valid_to`(应用时间)。
|
||||||
|
- 系统记录期:`system_from` / `system_to`(事务时间)。
|
||||||
|
- 用 `tstzrange` + `btree_gist` 排他约束防止同一实体业务有效期重叠。
|
||||||
|
- "按任意历史时点回放"= 给定 `(as_of_valid, as_of_system)` 过滤两条时间线。
|
||||||
|
|
||||||
|
### 3. 时间序列
|
||||||
|
- 行为/指标类数据(用户生命周期事件、回款、话务、佣金、资源使用)写入 `metric_event` 等表。
|
||||||
|
- 生产环境(Linux)用 TimescaleDB `create_hypertable` 转为超表,按时间分区/压缩。
|
||||||
|
- **本地开发(macOS)**:因 TimescaleDB 在 macOS 上 Homebrew 编译不稳定,本地跳过该扩展,`metric_event` 作为普通索引表使用;超表转换在迁移中条件执行(扩展存在才转),**不影响功能**,仅少了规模优化。
|
||||||
|
|
||||||
|
### 4. 数据版本与可追溯
|
||||||
|
- 每批数据落地登记 `data_version`(来源、批次、时间、行数),业务记录引用 `data_version_id`,使任一结论可回溯到当时数据版本(R3)。
|
||||||
|
|
||||||
|
## 影响
|
||||||
|
- MVP 仅依赖 TimescaleDB + pgvector + btree_gist,镜像可靠(`timescaledb-ha:pg16` 内置)。
|
||||||
|
- 图能力以关系建模实现,接口层(统一穿透查询服务)对上层屏蔽底层是关系还是图库,便于将来替换。
|
||||||
@@ -0,0 +1,779 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""精美版:本地LLM审计方案 PPTX,带流程图/热力矩阵/架构分层/卡片排版。"""
|
||||||
|
from pptx import Presentation
|
||||||
|
from pptx.util import Inches, Pt, Emu
|
||||||
|
from pptx.dml.color import RGBColor
|
||||||
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||||
|
from pptx.enum.shapes import MSO_SHAPE
|
||||||
|
from pptx.oxml.ns import qn
|
||||||
|
import copy
|
||||||
|
|
||||||
|
# ---------- 主题色 ----------
|
||||||
|
BG = RGBColor(0x0A, 0x16, 0x2E) # 主深蓝背景
|
||||||
|
BG2 = RGBColor(0x0E, 0x20, 0x42) # 次背景
|
||||||
|
CARD = RGBColor(0x14, 0x2B, 0x52) # 卡片
|
||||||
|
CARD2 = RGBColor(0x1B, 0x37, 0x66) # 卡片亮
|
||||||
|
CYAN = RGBColor(0x2D, 0xE0, 0xD0) # 主青
|
||||||
|
CYAN_D = RGBColor(0x16, 0x9B, 0x97)
|
||||||
|
BLUE = RGBColor(0x3B, 0x82, 0xF6) # 蓝
|
||||||
|
PURPLE = RGBColor(0x8B, 0x7CF if False else 0x7C, 0xF6)
|
||||||
|
GOLD = RGBColor(0xF5, 0xB7, 0x42) # 金
|
||||||
|
RED = RGBColor(0xEF, 0x5A, 0x5A) # 红(高风险)
|
||||||
|
ORANGE = RGBColor(0xF2, 0x8B, 0x3C)
|
||||||
|
GREEN = RGBColor(0x35, 0xC7, 0x59)
|
||||||
|
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
||||||
|
LIGHT = RGBColor(0xCB, 0xD8, 0xEC) # 正文浅
|
||||||
|
MUTE = RGBColor(0x8A, 0x9C, 0xB8) # 弱化
|
||||||
|
|
||||||
|
FONT = "PingFang SC"
|
||||||
|
FONT_B = "PingFang SC"
|
||||||
|
|
||||||
|
SW, SH = Inches(13.333), Inches(7.5)
|
||||||
|
prs = Presentation()
|
||||||
|
prs.slide_width, prs.slide_height = SW, SH
|
||||||
|
BLANK = prs.slide_layouts[6]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 底层工具 ----------
|
||||||
|
def _set_grad(shape, c1, c2, angle=90):
|
||||||
|
"""给 shape 设置线性渐变填充。"""
|
||||||
|
sp = shape.fill._xPr # spPr
|
||||||
|
# 移除已有填充
|
||||||
|
for tag in ('a:noFill','a:solidFill','a:gradFill','a:blipFill','a:pattFill','a:grpFill'):
|
||||||
|
for e in sp.findall(qn(tag)):
|
||||||
|
sp.remove(e)
|
||||||
|
grad = sp.makeelement(qn('a:gradFill'), {})
|
||||||
|
lst = grad.makeelement(qn('a:gsLst'), {})
|
||||||
|
for pos, col in ((0, c1), (100000, c2)):
|
||||||
|
gs = grad.makeelement(qn('a:gs'), {'pos': str(pos if pos else int(pos*1000))})
|
||||||
|
if pos == 0: gs.set('pos','0')
|
||||||
|
else: gs.set('pos','100000')
|
||||||
|
clr = gs.makeelement(qn('a:srgbClr'), {'val': '%02X%02X%02X' % (col[0],col[1],col[2])})
|
||||||
|
gs.append(clr); lst.append(gs)
|
||||||
|
grad.append(lst)
|
||||||
|
lin = grad.makeelement(qn('a:lin'), {'ang': str(int(angle*60000)), 'scaled':'1'})
|
||||||
|
grad.append(lin)
|
||||||
|
# 插入到 ln 之前
|
||||||
|
ln = sp.find(qn('a:ln'))
|
||||||
|
if ln is not None: sp.insert(list(sp).index(ln), grad)
|
||||||
|
else: sp.append(grad)
|
||||||
|
|
||||||
|
|
||||||
|
def bg_gradient(slide, c1=BG, c2=BG2):
|
||||||
|
r = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SW, SH)
|
||||||
|
r.line.fill.background(); r.shadow.inherit = False
|
||||||
|
r.fill.solid(); r.fill.fore_color.rgb = c1
|
||||||
|
_set_grad(r, c1, c2, angle=120)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def rrect(slide, x, y, w, h, color=None, grad=None, line=None, lw=1, radius=0.08):
|
||||||
|
sp = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h)
|
||||||
|
sp.shadow.inherit = False
|
||||||
|
try:
|
||||||
|
sp.adjustments[0] = radius
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if grad:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = grad[0]
|
||||||
|
_set_grad(sp, grad[0], grad[1], angle=90)
|
||||||
|
elif color is not None:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = color
|
||||||
|
else:
|
||||||
|
sp.fill.background()
|
||||||
|
if line is not None:
|
||||||
|
sp.line.color.rgb = line; sp.line.width = Pt(lw)
|
||||||
|
else:
|
||||||
|
sp.line.fill.background()
|
||||||
|
return sp
|
||||||
|
|
||||||
|
|
||||||
|
def rect(slide, x, y, w, h, color=None, grad=None, line=None, lw=1):
|
||||||
|
sp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
|
||||||
|
sp.shadow.inherit = False
|
||||||
|
if grad:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = grad[0]; _set_grad(sp, grad[0], grad[1])
|
||||||
|
elif color is not None:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = color
|
||||||
|
else:
|
||||||
|
sp.fill.background()
|
||||||
|
if line is not None:
|
||||||
|
sp.line.color.rgb = line; sp.line.width = Pt(lw)
|
||||||
|
else:
|
||||||
|
sp.line.fill.background()
|
||||||
|
return sp
|
||||||
|
|
||||||
|
|
||||||
|
def circle(slide, x, y, d, color=None, grad=None, line=None, lw=1.5):
|
||||||
|
sp = slide.shapes.add_shape(MSO_SHAPE.OVAL, x, y, d, d)
|
||||||
|
sp.shadow.inherit = False
|
||||||
|
if grad:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = grad[0]; _set_grad(sp, grad[0], grad[1])
|
||||||
|
elif color is not None:
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = color
|
||||||
|
else:
|
||||||
|
sp.fill.background()
|
||||||
|
if line is not None:
|
||||||
|
sp.line.color.rgb = line; sp.line.width = Pt(lw)
|
||||||
|
else:
|
||||||
|
sp.line.fill.background()
|
||||||
|
return sp
|
||||||
|
|
||||||
|
|
||||||
|
def chevron(slide, x, y, w, h, color):
|
||||||
|
sp = slide.shapes.add_shape(MSO_SHAPE.CHEVRON, x, y, w, h)
|
||||||
|
sp.shadow.inherit = False
|
||||||
|
sp.fill.solid(); sp.fill.fore_color.rgb = color
|
||||||
|
sp.line.fill.background()
|
||||||
|
return sp
|
||||||
|
|
||||||
|
|
||||||
|
def shape_text(shape, runs, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, wrap=True):
|
||||||
|
tf = shape.text_frame; tf.word_wrap = wrap; tf.vertical_anchor = anchor
|
||||||
|
tf.margin_left = Pt(4); tf.margin_right = Pt(4)
|
||||||
|
tf.margin_top = Pt(2); tf.margin_bottom = Pt(2)
|
||||||
|
for i, para in enumerate(runs):
|
||||||
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||||
|
p.alignment = align; p.space_after = Pt(2); p.space_before = Pt(0)
|
||||||
|
for (t, sz, c, b) in para:
|
||||||
|
r = p.add_run(); r.text = t
|
||||||
|
r.font.size = Pt(sz); r.font.color.rgb = c; r.font.bold = b; r.font.name = FONT
|
||||||
|
|
||||||
|
|
||||||
|
def txt(slide, x, y, w, h, runs, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP,
|
||||||
|
wrap=True, sa=6, line_spacing=None):
|
||||||
|
tb = slide.shapes.add_textbox(x, y, w, h); tf = tb.text_frame
|
||||||
|
tf.word_wrap = wrap; tf.vertical_anchor = anchor
|
||||||
|
for i, para in enumerate(runs):
|
||||||
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||||
|
p.alignment = align; p.space_after = Pt(sa); p.space_before = Pt(0)
|
||||||
|
if line_spacing: p.line_spacing = line_spacing
|
||||||
|
for (t, sz, c, b) in para:
|
||||||
|
r = p.add_run(); r.text = t
|
||||||
|
r.font.size = Pt(sz); r.font.color.rgb = c; r.font.bold = b; r.font.name = FONT
|
||||||
|
return tb
|
||||||
|
|
||||||
|
|
||||||
|
def header(slide, kicker, title):
|
||||||
|
rect(slide, Inches(0.7), Inches(0.62), Inches(0.14), Inches(0.62), grad=(CYAN, BLUE))
|
||||||
|
txt(slide, Inches(0.98), Inches(0.5), Inches(11.5), Inches(0.4),
|
||||||
|
[[(kicker, 13, CYAN, True)]], sa=0)
|
||||||
|
txt(slide, Inches(0.95), Inches(0.82), Inches(11.6), Inches(0.7),
|
||||||
|
[[(title, 29, WHITE, True)]], sa=0)
|
||||||
|
|
||||||
|
|
||||||
|
def footer(slide, page):
|
||||||
|
rect(slide, 0, Inches(7.18), SW, Inches(0.32), color=BG2)
|
||||||
|
txt(slide, Inches(0.7), Inches(7.16), Inches(6), Inches(0.32),
|
||||||
|
[[("数据不出域 · 审计全穿透", 9.5, MUTE, False)]], anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(slide, Inches(11.6), Inches(7.16), Inches(1.0), Inches(0.32),
|
||||||
|
[[(f"{page:02d} / 23", 10, CYAN, True)]], align=PP_ALIGN.RIGHT,
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
|
||||||
|
|
||||||
|
def new(kicker=None, title=None, page=None):
|
||||||
|
s = prs.slides.add_slide(BLANK)
|
||||||
|
bg_gradient(s)
|
||||||
|
# 右上角装饰圆
|
||||||
|
circle(s, Inches(11.6), Inches(-1.0), Inches(3.2), color=BG2)
|
||||||
|
circle(s, Inches(12.4), Inches(-0.4), Inches(1.6), color=CARD)
|
||||||
|
if kicker is not None:
|
||||||
|
header(s, kicker, title)
|
||||||
|
if page is not None:
|
||||||
|
footer(s, page)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 1 封面 ==============
|
||||||
|
def cover():
|
||||||
|
s = prs.slides.add_slide(BLANK)
|
||||||
|
bg_gradient(s, RGBColor(0x07,0x10,0x24), RGBColor(0x10,0x29,0x52))
|
||||||
|
# 几何装饰
|
||||||
|
circle(s, Inches(9.8), Inches(-1.6), Inches(5.2), color=RGBColor(0x10,0x24,0x48))
|
||||||
|
circle(s, Inches(11.2), Inches(0.2), Inches(2.8), grad=(CYAN_D, BG))
|
||||||
|
for i, d in enumerate([Inches(0.16)]*3):
|
||||||
|
circle(s, Inches(10.2+ i*0.5), Inches(4.6 + i*0.35), d, color=CYAN)
|
||||||
|
rect(s, 0, 0, Inches(0.22), SH, grad=(CYAN, BLUE))
|
||||||
|
txt(s, Inches(0.9), Inches(1.7), Inches(3), Inches(0.5),
|
||||||
|
[[("AI · 全域内审", 15, CYAN, True)]], sa=0)
|
||||||
|
rect(s, Inches(0.95), Inches(2.2), Inches(2.4), Pt(4), color=CYAN)
|
||||||
|
txt(s, Inches(0.88), Inches(2.45), Inches(10.5), Inches(1.7),
|
||||||
|
[[("数据不出域", 56, WHITE, True)], [("审计全穿透", 56, CYAN, True)]], sa=4)
|
||||||
|
txt(s, Inches(0.95), Inches(4.95), Inches(10.5), Inches(1.2),
|
||||||
|
[[("基于本地私有化大模型的电信运营商 AI 全域内审体系", 20, WHITE, True)],
|
||||||
|
[("不是一套工具,而是一套建在自己机房里、越用越聪明的审计能力体系", 14.5, LIGHT, False)]], sa=8)
|
||||||
|
rect(s, Inches(0.95), Inches(6.35), Inches(0.5), Pt(3), color=CYAN)
|
||||||
|
txt(s, Inches(0.95), Inches(6.5), Inches(6), Inches(0.4),
|
||||||
|
[[("2026 年 6 月", 13, MUTE, False)]], sa=0)
|
||||||
|
cover()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 2 痛点:三个数字 + 三类困局 ==============
|
||||||
|
def pain():
|
||||||
|
s = new("现状 · 困局", "为什么传统审计“查不过来”?", 2)
|
||||||
|
# 三个大数字卡片
|
||||||
|
stats = [("150亿", "年业务规模", BLUE), ("5000万", "潜在异常金额", GOLD), ("5%", "传统抽样覆盖率", RED)]
|
||||||
|
x = Inches(0.95); w = Inches(3.62); gap = Inches(0.27)
|
||||||
|
for i,(num, lab, col) in enumerate(stats):
|
||||||
|
cx = x + i*(w+gap)
|
||||||
|
c = rrect(s, cx, Inches(1.75), w, Inches(1.55), color=CARD, radius=0.1)
|
||||||
|
rect(s, cx, Inches(1.75), Inches(0.1), Inches(1.55), color=col)
|
||||||
|
txt(s, cx+Inches(0.25), Inches(1.92), w-Inches(0.3), Inches(0.85),
|
||||||
|
[[(num, 38, col, True)]], sa=0)
|
||||||
|
txt(s, cx+Inches(0.27), Inches(2.75), w-Inches(0.3), Inches(0.4),
|
||||||
|
[[(lab, 14, LIGHT, False)]], sa=0)
|
||||||
|
# 三类困局
|
||||||
|
cases = [
|
||||||
|
("拆单规避", "8 个客户各签 600 万 ICT 项目全拆成 80 万以下,三重一大抽样完美避开。"),
|
||||||
|
("稳定的定,稳定的退", "每月新增 6000 人订彩铃,3 个月后首月用户全退订,渠道已领佣金、骗补后弃养。"),
|
||||||
|
("Excel 干不过来", "海量单据只能抽样,查不全查不深,5000 万异常如针落大海。"),
|
||||||
|
]
|
||||||
|
y = Inches(3.55)
|
||||||
|
for i,(t, d) in enumerate(cases):
|
||||||
|
cy = y + i*Inches(0.78)
|
||||||
|
rrect(s, Inches(0.95), cy, Inches(11.55), Inches(0.66), color=CARD if i%2 else CARD2, radius=0.12)
|
||||||
|
circle(s, Inches(1.12), cy+Inches(0.13), Inches(0.4), grad=(CYAN, BLUE))
|
||||||
|
txt(s, Inches(1.12), cy+Inches(0.13), Inches(0.4), Inches(0.4),
|
||||||
|
[[(str(i+1), 16, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(1.7), cy, Inches(2.4), Inches(0.66),
|
||||||
|
[[(t, 14.5, CYAN, True)]], anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(4.0), cy, Inches(8.3), Inches(0.66),
|
||||||
|
[[(d, 12.5, LIGHT, False)]], anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(0.95), Inches(6.05), Inches(11.55), Inches(0.5),
|
||||||
|
[[("核心矛盾:", 13, GOLD, True),
|
||||||
|
("数据涉政企合同/用户隐私/财务凭证,上公有云=裸奔;不上 AI 又干不过来。", 13, WHITE, False)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
pain()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 通用表格 ==============
|
||||||
|
def style_table(table, headers, rows, col_widths, total_w, top, left=Inches(0.95),
|
||||||
|
hrow=Inches(0.52), rrow=Inches(0.62), fs=12, hfs=13):
|
||||||
|
n = len(rows)+1
|
||||||
|
for i,w in enumerate(col_widths):
|
||||||
|
table.columns[i].width = Inches(w)
|
||||||
|
table.rows[0].height = hrow
|
||||||
|
for i in range(1, n): table.rows[i].height = rrow
|
||||||
|
# remove default style banding via first row formatting
|
||||||
|
for j,h in enumerate(headers):
|
||||||
|
c = table.cell(0,j); c.fill.solid(); c.fill.fore_color.rgb = CYAN_D
|
||||||
|
c.vertical_anchor = MSO_ANCHOR.MIDDLE
|
||||||
|
p = c.text_frame.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
|
||||||
|
c.text_frame.word_wrap = True
|
||||||
|
r = p.add_run(); r.text = h; r.font.size = Pt(hfs); r.font.bold = True
|
||||||
|
r.font.color.rgb = WHITE; r.font.name = FONT
|
||||||
|
for i,row in enumerate(rows, start=1):
|
||||||
|
for j,val in enumerate(row):
|
||||||
|
c = table.cell(i,j); c.fill.solid()
|
||||||
|
c.fill.fore_color.rgb = CARD if i%2 else CARD2
|
||||||
|
c.vertical_anchor = MSO_ANCHOR.MIDDLE
|
||||||
|
c.text_frame.word_wrap = True
|
||||||
|
p = c.text_frame.paragraphs[0]
|
||||||
|
p.alignment = PP_ALIGN.CENTER if j==0 else PP_ALIGN.LEFT
|
||||||
|
r = p.add_run(); r.text = val; r.font.size = Pt(fs)
|
||||||
|
r.font.bold = (j==0)
|
||||||
|
r.font.color.rgb = CYAN if j==0 else LIGHT
|
||||||
|
r.font.name = FONT
|
||||||
|
|
||||||
|
|
||||||
|
def table_slide(page, kicker, title, headers, rows, note=None, col_widths=None,
|
||||||
|
fs=12, hfs=13):
|
||||||
|
s = new(kicker, title, page)
|
||||||
|
total = sum(col_widths)
|
||||||
|
top = Inches(1.85)
|
||||||
|
shp = s.shapes.add_table(len(rows)+1, len(headers), Inches(0.95), top,
|
||||||
|
Inches(total), Inches(0.5)+Inches(0.6)*len(rows))
|
||||||
|
# 去掉自带样式
|
||||||
|
tbl = shp.table
|
||||||
|
style_table(tbl, headers, rows, col_widths, total, top, fs=fs, hfs=hfs)
|
||||||
|
if note:
|
||||||
|
ny = top + Inches(0.5)+Inches(0.62)*len(rows) + Inches(0.3)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.7), color=CARD2, radius=0.18)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.7), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.7),
|
||||||
|
[[("▶ ", 13, GOLD, True), (note, 13, WHITE, True)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 3 三方对比 ==============
|
||||||
|
table_slide(3, "破局 · 定位", "本地 LLM 让“安全”和“智能”不再二选一",
|
||||||
|
["对比维度", "传统抽样审计", "公有云 AI 审计", "本地 LLM 审计(我们)"],
|
||||||
|
[
|
||||||
|
["数据范围", "按金额抽样,查不全", "全量扫描,但数据出域", "全量扫描,数据不出机房"],
|
||||||
|
["规则能力", "规则写死,反向规避", "模型强,但合规风险高", "模型私有化,合规可控"],
|
||||||
|
["响应效率", "Excel 翻表,效率低", "实时预警,依赖外网", "内网闭环,秒级响应"],
|
||||||
|
["交互模式", "人找数据", "数据找人,但数据送人", "数据找人,数据原地不动"],
|
||||||
|
["能力归属", "经验在人脑,人走经验走", "能力在外部,租用即失", "能力沉淀本地,越用越聪明"],
|
||||||
|
],
|
||||||
|
note="把千问 70B / DeepSeek 装进本地机房,让 AI 在数据旁边干活,而不是把数据送给 AI。",
|
||||||
|
col_widths=[2.0, 3.1, 3.2, 3.25])
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 4 审计域全景 + 风险热力矩阵 ==============
|
||||||
|
def domain_heat():
|
||||||
|
s = new("方法论 · 框架", "审计域全景图 + 风险分级模型", 4)
|
||||||
|
# 左:五大风险域
|
||||||
|
domains = [
|
||||||
|
("收入域", "政企穿透·跨期匹配·云空转", CYAN),
|
||||||
|
("成本域", "渠道佣金·终端补贴·摊销", BLUE),
|
||||||
|
("采购域", "网络建设·工程·围标串标", PURPLE),
|
||||||
|
("资金域", "回款挂账·网间结算·流向", GOLD),
|
||||||
|
("合规域", "员工舞弊·权限·积分套现", GREEN),
|
||||||
|
]
|
||||||
|
txt(s, Inches(0.95), Inches(1.75), Inches(6), Inches(0.4),
|
||||||
|
[[("五大风险域 · 全覆盖", 15, WHITE, True)]], sa=0)
|
||||||
|
y = Inches(2.25)
|
||||||
|
for i,(t,d,col) in enumerate(domains):
|
||||||
|
cy = y + i*Inches(0.82)
|
||||||
|
rrect(s, Inches(0.95), cy, Inches(6.0), Inches(0.68), color=CARD, radius=0.14)
|
||||||
|
rect(s, Inches(0.95), cy, Inches(0.12), Inches(0.68), color=col)
|
||||||
|
circle(s, Inches(1.2), cy+Inches(0.14), Inches(0.4), color=col)
|
||||||
|
txt(s, Inches(1.55), cy, Inches(1.5), Inches(0.68),
|
||||||
|
[[(t, 15, WHITE, True)]], anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(3.0), cy, Inches(3.85), Inches(0.68),
|
||||||
|
[[(d, 11.5, LIGHT, False)]], anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
# 右:风险热力矩阵 3x3
|
||||||
|
txt(s, Inches(7.4), Inches(1.75), Inches(5), Inches(0.4),
|
||||||
|
[[("风险热力矩阵 · 有优先级", 15, WHITE, True)]], sa=0)
|
||||||
|
gx, gy = Inches(8.35), Inches(2.35)
|
||||||
|
cell = Inches(1.25)
|
||||||
|
# 颜色矩阵 [行=金额影响 高->低][列=概率 低->高]
|
||||||
|
heat = [
|
||||||
|
[ORANGE, RED, RED],
|
||||||
|
[GOLD, ORANGE, RED],
|
||||||
|
[GREEN, GOLD, ORANGE],
|
||||||
|
]
|
||||||
|
labels = [
|
||||||
|
["", "", "优先\n全量监控"],
|
||||||
|
["重点\n定向穿透", "", ""],
|
||||||
|
["", "", "批量\n聚类筛查"],
|
||||||
|
]
|
||||||
|
for r_ in range(3):
|
||||||
|
for c_ in range(3):
|
||||||
|
cx = gx + c_*cell; cyy = gy + r_*cell
|
||||||
|
rrect(s, cx, cyy, cell-Inches(0.08), cell-Inches(0.08),
|
||||||
|
color=heat[r_][c_], radius=0.12)
|
||||||
|
if labels[r_][c_]:
|
||||||
|
lines = [[(seg, 9.5, WHITE, True)] for seg in labels[r_][c_].split("\n")]
|
||||||
|
txt(s, cx, cyy, cell-Inches(0.08), cell-Inches(0.08),
|
||||||
|
lines, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
# 轴标签
|
||||||
|
txt(s, gx-Inches(0.05), gy-Inches(0.05), cell*3, Inches(0.3), [], sa=0)
|
||||||
|
txt(s, Inches(7.55), gy, Inches(0.75), cell*3,
|
||||||
|
[[("金\n额\n影\n响", 11, CYAN, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, gx, gy+cell*3-Inches(0.02), cell*3, Inches(0.35),
|
||||||
|
[[("发生概率 低 → 高", 11, CYAN, True)]], align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
# 底注
|
||||||
|
ny = Inches(6.35)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.62), color=CARD2, radius=0.2)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.62), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.62),
|
||||||
|
[[("▶ ", 13, GOLD, True),
|
||||||
|
("不是工具集合,而是有体系、有优先级的全域审计框架。", 13, WHITE, True)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
domain_heat()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 5 四大引擎(2x2卡片)==============
|
||||||
|
def engines():
|
||||||
|
s = new("能力 · 底座", "“本地 AI 审计大脑”四大核心引擎", 5)
|
||||||
|
cards = [
|
||||||
|
("01", "本地私有化 LLM 引擎", "模型本地化部署,数据绝不出域;推理、规则配置、报告生成、线索解释。", CYAN),
|
||||||
|
("02", "全量穿透引擎", "直连 BSS/OSS/ERP/财务,不抽样,对所有合同、回款、行为做关联扫描。", BLUE),
|
||||||
|
("03", "规则进化引擎(护城河)", "自然语言描述新造假→自动转规则→沙箱验证→把顾问经验固化为机构资产。", GOLD),
|
||||||
|
("04", "线索驱动引擎", "对异常聚类做人话解释,输出附证据链的高价值线索,直推审计员桌面。", GREEN),
|
||||||
|
]
|
||||||
|
W, H = Inches(5.7), Inches(2.18)
|
||||||
|
gx, gy = Inches(0.95), Inches(1.85)
|
||||||
|
gapx, gapy = Inches(0.18), Inches(0.22)
|
||||||
|
for i,(no,t,d,col) in enumerate(cards):
|
||||||
|
r,c = divmod(i,2)
|
||||||
|
x = gx + c*(W+gapx); y = gy + r*(H+gapy)
|
||||||
|
rrect(s, x, y, W, H, color=CARD, radius=0.07)
|
||||||
|
rect(s, x, y, Inches(0.14), H, color=col)
|
||||||
|
circle(s, x+Inches(0.35), y+Inches(0.32), Inches(0.85), grad=(col, BG2))
|
||||||
|
txt(s, x+Inches(0.35), y+Inches(0.32), Inches(0.85), Inches(0.85),
|
||||||
|
[[(no, 26, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, x+Inches(1.4), y+Inches(0.35), W-Inches(1.6), Inches(0.6),
|
||||||
|
[[(t, 17, WHITE, True)]], sa=0)
|
||||||
|
txt(s, x+Inches(1.4), y+Inches(1.0), W-Inches(1.65), Inches(1.05),
|
||||||
|
[[(d, 12.5, LIGHT, False)]], sa=0, line_spacing=1.15)
|
||||||
|
engines()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 6-13 场景页 ==============
|
||||||
|
def scene(page, no, name, color, blocks):
|
||||||
|
s = prs.slides.add_slide(BLANK); bg_gradient(s)
|
||||||
|
# 左侧色带
|
||||||
|
rect(s, 0, 0, Inches(3.0), SH, color=BG2)
|
||||||
|
rect(s, Inches(3.0), 0, Inches(0.06), SH, color=color)
|
||||||
|
circle(s, Inches(0.55), Inches(2.0), Inches(1.9), grad=(color, BG2))
|
||||||
|
txt(s, Inches(0.55), Inches(2.05), Inches(1.9), Inches(1.9),
|
||||||
|
[[(no, 60, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(0.3), Inches(0.55), Inches(2.4), Inches(0.4),
|
||||||
|
[[("场景", 14, color, True)]], align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
txt(s, Inches(0.25), Inches(4.15), Inches(2.55), Inches(1.6),
|
||||||
|
[[(seg, 19, WHITE, True)] for seg in name.split("\n")],
|
||||||
|
align=PP_ALIGN.CENTER, sa=2)
|
||||||
|
# 右侧内容卡片
|
||||||
|
y = Inches(0.7)
|
||||||
|
icons = {"经典案例":"●","扩展案例":"●","AI 审计点":"◆","本地 LLM 能力":"▲","业务链路":"►"}
|
||||||
|
for label, text in blocks:
|
||||||
|
h = Inches(1.18) if len(text) > 46 else Inches(0.95)
|
||||||
|
rrect(s, Inches(3.3), y, Inches(9.55), h, color=CARD if "案例" not in label or "扩展" in label else CARD2, radius=0.08)
|
||||||
|
# 标签条
|
||||||
|
lab_col = color if label in ("AI 审计点","本地 LLM 能力","业务链路") else CYAN
|
||||||
|
txt(s, Inches(3.55), y+Inches(0.12), Inches(9.1), Inches(0.36),
|
||||||
|
[[(label, 13.5, lab_col, True)]], sa=0)
|
||||||
|
txt(s, Inches(3.55), y+Inches(0.46), Inches(9.1), h-Inches(0.5),
|
||||||
|
[[(text, 12.5, LIGHT, False)]], sa=0, line_spacing=1.12)
|
||||||
|
y = y + h + Inches(0.12)
|
||||||
|
footer(s, page)
|
||||||
|
|
||||||
|
scenes = [
|
||||||
|
("01","政企收入\n全链路穿透", CYAN, [
|
||||||
|
("业务链路","立项→审批→报价→签约→开票→回款,全链路穿透。"),
|
||||||
|
("经典案例(拆单规避+虚假回款)","8 个客户各签 600 万拆成 79-99 万规避审批,尾款 500 万长期挂账;注册地址同楼、法人为同一人亲属、付款账户同一实控企业。"),
|
||||||
|
("AI 审计点","合同金额阈值边缘聚集;工商关联穿透识别隐性实控人;回款时序聚类识别批量违约。"),
|
||||||
|
("本地 LLM 能力","自然语言查数、关联推理、一键生成《政企客户回款异常专项线索清单》。"),
|
||||||
|
]),
|
||||||
|
("02","市场业务\n真实性", BLUE, [
|
||||||
|
("经典案例(稳定的定,稳定的退)","每月新增 6000 人订彩铃,3 个月后首月用户全退订,骗补后弃养;号码集中乡镇、通话记录为零。"),
|
||||||
|
("扩展案例(物联网卡虚假激活)","批量开通 10 万张卡称智慧停车,激活后零流量,按激活量领每台 50 元补贴,半年后集体沉默。"),
|
||||||
|
("AI 审计点","用户生命周期时序识别;佣金与业务质量匹配;沉默/零通话用户聚类;交付物与收入交叉验证。"),
|
||||||
|
("本地 LLM 能力","识别脉冲式增长+规律性衰减的周期性造假,自动提炼为新规则。"),
|
||||||
|
]),
|
||||||
|
("03","收入与成本\n跨期匹配", PURPLE, [
|
||||||
|
("经典案例(趸交收入一次性确认)","24 个月套餐送手表,收入应分 24 月却因趸交一把确认,手表成本却摊 24 月,确认时点严重错配。"),
|
||||||
|
("扩展案例(提前确认)","云项目约定按用量计费,财务却在设备上架当月全额确认,客户前 6 月几乎零使用。"),
|
||||||
|
("AI 审计点","自动勾稽确认政策 vs 账务 vs 合同;识别一次性确认异常分录;成本摊销与收入跨期匹配。"),
|
||||||
|
("本地 LLM 能力","跨系统自动勾稽,识别收入成本确认时点错配的异常分录模式。"),
|
||||||
|
]),
|
||||||
|
("04","渠道佣金与\n代理商套利", GOLD, [
|
||||||
|
("经典案例(虚假放号+套机套卡)","批量买老人机插 5G 卡激活后丢弃,领 5G 迁转佣金每台 200 元+补贴 300 元,次月用户全流失。"),
|
||||||
|
("扩展案例(异地窜货套利)","从邻省低价采购同款机,本省以新用户入网名义领高额补贴,手机回流二级市场。"),
|
||||||
|
("AI 审计点","IMEI 与用户绑定真实性;佣金与在网时长匹配;终端流向追踪;代理商质量时序衰减。"),
|
||||||
|
("本地 LLM 能力","IMEI 级终端流向追踪,识别激活-沉默-流失套利闭环。"),
|
||||||
|
]),
|
||||||
|
("05","网络建设与\n工程采购", GREEN, [
|
||||||
|
("经典案例(围标串标+虚增工程量)","3 家投标报价差异不足 1%、方案雷同,中标后同一班组施工,签证单同一笔迹不同日期批量签字。"),
|
||||||
|
("扩展案例(虚假巡检)","系统显示月巡检 2000 次,GPS 比对实际只到 300 站,其余照片复用+坐标伪造。"),
|
||||||
|
("AI 审计点","投标报价相似度与文件雷同度;工程量与资源消耗匹配;巡检轨迹与工单交叉;马甲供应商识别。"),
|
||||||
|
("本地 LLM 能力","NLP 比对投标雷同度,GPS 轨迹与工单交叉验证,识别马甲供应商。"),
|
||||||
|
]),
|
||||||
|
("06","互联互通与\n网间结算", CYAN, [
|
||||||
|
("经典案例(话务量操纵)","与境外合谋虚假国际来话刷量,主叫为虚商号段,时长均为 30/60 秒整数倍,明显非真人。"),
|
||||||
|
("扩展案例(短信网关刷量)","SP 伪造记录申报成功发送 10 亿条按 0.05 元/条结算,实际到达率不足 10%。"),
|
||||||
|
("AI 审计点","话务量时序异常与整数时长聚集;结算数据与原始信令比对;SP/CP 业务量与结算交叉验证。"),
|
||||||
|
("本地 LLM 能力","识别整数倍通话时长等非人类行为,信令级原始数据比对。"),
|
||||||
|
]),
|
||||||
|
("07","云业务/IDC\n与新兴业务", BLUE, [
|
||||||
|
("经典案例(云资源空转)","政企客户签 3 年云服务年付 100 万,CPU 利用率长期<5%、存储近空,却全额确认收入,实控人为领导亲属。"),
|
||||||
|
("扩展案例(IDC 机柜虚租)","宣称出租率 90%,实际大量机柜无设备、电费为零,收入来自关联方预付租金。"),
|
||||||
|
("AI 审计点","资源使用量 vs 计费量匹配;出租率与电力消耗勾稽;关联方与预付异常;确认与验收时序一致性。"),
|
||||||
|
("本地 LLM 能力","资源利用率与计费量自动比对,关联方网络挖掘,识别空转收入。"),
|
||||||
|
]),
|
||||||
|
("08","员工内部舞弊\n与资源滥用", PURPLE, [
|
||||||
|
("经典案例(内部号码套利)","员工用权限批量开员工测试号对外出租免流套餐,流量收入全计入内部成本未确认收入。"),
|
||||||
|
("扩展案例(积分套现)","勾结外部商户虚构消费批量刷积分兑换礼品卡变现,某商户单日发放量超正常 100 倍。"),
|
||||||
|
("AI 审计点","权限操作日志异常模式;测试号实际用途偏离;积分流向追踪;权限与岗位匹配度。"),
|
||||||
|
("本地 LLM 能力","操作日志异常挖掘,权限-岗位匹配分析,积分流向网络追踪。"),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
pg = 6
|
||||||
|
for no,name,col,blocks in scenes:
|
||||||
|
scene(pg, no, name, col, blocks); pg += 1
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 14 数据治理 ==============
|
||||||
|
def governance():
|
||||||
|
s = new("工程 · 地基", "数据接入与治理层(全量穿透的前提)", 14)
|
||||||
|
items = [
|
||||||
|
("多源异构接入","适配 BSS/OSS/ERP/财务/合同/工单/信令各系统接口、数据库、文件,统一汇入本地数据湖。", CYAN),
|
||||||
|
("主数据对齐","客户、合同、号码、工单、供应商跨系统实体统一,解决主键对不上。", BLUE),
|
||||||
|
("数据质量探查与清洗","缺失、重复、口径不一自动探查清洗,建立质量评分。", PURPLE),
|
||||||
|
("增量同步与时效","从年度快照升级为近实时增量,支撑常态化监控。", GOLD),
|
||||||
|
]
|
||||||
|
W, H = Inches(5.7), Inches(1.55)
|
||||||
|
gx,gy = Inches(0.95), Inches(1.85)
|
||||||
|
for i,(t,d,col) in enumerate(items):
|
||||||
|
r,c = divmod(i,2)
|
||||||
|
x = gx + c*(W+Inches(0.18)); y = gy + r*(H+Inches(0.2))
|
||||||
|
rrect(s, x, y, W, H, color=CARD, radius=0.08)
|
||||||
|
rect(s, x, y, Inches(0.12), H, color=col)
|
||||||
|
circle(s, x+Inches(0.32), y+Inches(0.3), Inches(0.55), color=col)
|
||||||
|
txt(s, x+Inches(0.32), y+Inches(0.3), Inches(0.55), Inches(0.55),
|
||||||
|
[[(str(i+1), 18, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, x+Inches(1.05), y+Inches(0.2), W-Inches(1.2), Inches(0.45),
|
||||||
|
[[(t, 15.5, WHITE, True)]], sa=0)
|
||||||
|
txt(s, x+Inches(1.05), y+Inches(0.66), W-Inches(1.25), Inches(0.8),
|
||||||
|
[[(d, 12, LIGHT, False)]], sa=0, line_spacing=1.12)
|
||||||
|
ny = Inches(5.5)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.95), color=CARD2, radius=0.12)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.95), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.95),
|
||||||
|
[[("我们把脏活写进方案、承担下来", 14.5, GOLD, True)],
|
||||||
|
[("数据治理是这套体系工作量最大、最该提前立项的一环,而非回避。", 12.5, WHITE, False)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=3)
|
||||||
|
governance()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 15 人机协同闭环(流程图)==============
|
||||||
|
def closed_loop():
|
||||||
|
s = new("闭环 · 价值", "人机协同闭环:线索之后才是价值", 15)
|
||||||
|
steps = ["AI 全量扫描","生成线索+证据链","审计员复核研判","自动生成底稿","定性 / 整改","复核销项闭环"]
|
||||||
|
cols = [CYAN, BLUE, PURPLE, BLUE, GOLD, GREEN]
|
||||||
|
n = len(steps)
|
||||||
|
x0 = Inches(0.85); y = Inches(2.1); w = Inches(1.95); h = Inches(0.95); ov = Inches(0.32)
|
||||||
|
step_w = (Inches(12.5) - w) / (n-1)
|
||||||
|
for i,(t,col) in enumerate(zip(steps, cols)):
|
||||||
|
x = x0 + step_w*i
|
||||||
|
ch = chevron(s, x, y, w+ov, h, col)
|
||||||
|
shape_text(ch, [[(seg, 12.5, WHITE, True)] for seg in t.split(" ")] if " " in t else [[(t,12.5,WHITE,True)]],
|
||||||
|
align=PP_ALIGN.CENTER)
|
||||||
|
# 返回箭头示意(闭环)
|
||||||
|
txt(s, Inches(0.85), Inches(3.05), Inches(12), Inches(0.4),
|
||||||
|
[[("◄──────────────── 规则进化反哺,越用越聪明 ────────────────►", 12, MUTE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
# 三栏角色
|
||||||
|
roles = [
|
||||||
|
("AI 侧","出线索、附证据链、给判定理由、自动生成可追溯底稿。", CYAN),
|
||||||
|
("审计员侧","复核研判、定性、决定整改或移交、最终签字。", BLUE),
|
||||||
|
("闭环管理","线索分派、取证留痕、整改跟踪、销项复核全流程在线。", GOLD),
|
||||||
|
]
|
||||||
|
W = Inches(3.7); gx = Inches(0.95); y2 = Inches(3.75)
|
||||||
|
for i,(t,d,col) in enumerate(roles):
|
||||||
|
x = gx + i*(W+Inches(0.22))
|
||||||
|
rrect(s, x, y2, W, Inches(1.85), color=CARD, radius=0.08)
|
||||||
|
rect(s, x, y2, W, Inches(0.5), color=col)
|
||||||
|
txt(s, x, y2, W, Inches(0.5), [[(t, 15, WHITE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, x+Inches(0.25), y2+Inches(0.65), W-Inches(0.5), Inches(1.1),
|
||||||
|
[[(d, 12.5, LIGHT, False)]], sa=0, line_spacing=1.18)
|
||||||
|
ny = Inches(5.95)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.62), color=CARD2, radius=0.2)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.62), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.62),
|
||||||
|
[[("▶ ", 13, GOLD, True),
|
||||||
|
("从“发现工具”升级为“办案平台”——每一步都接得住、留得痕。", 13, WHITE, True)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
closed_loop()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 16 误报治理 ==============
|
||||||
|
def fp_control():
|
||||||
|
s = new("可信 · 落地", "误报治理与置信度分级(专业 = 诚实)", 16)
|
||||||
|
# 左:三级置信漏斗
|
||||||
|
txt(s, Inches(0.95), Inches(1.8), Inches(6), Inches(0.4),
|
||||||
|
[[("三级置信分流", 15, WHITE, True)]], sa=0)
|
||||||
|
tiers = [("高置信","直接推送处置", GREEN, 6.0),
|
||||||
|
("中置信","人工复核研判", GOLD, 4.6),
|
||||||
|
("低置信","归档备查", MUTE, 3.2)]
|
||||||
|
y = Inches(2.35)
|
||||||
|
for t,d,col,w in tiers:
|
||||||
|
ww = Inches(w)
|
||||||
|
x = Inches(0.95) + (Inches(6.0)-ww)/2
|
||||||
|
rrect(s, x, y, ww, Inches(0.85), color=col, radius=0.22)
|
||||||
|
shape = txt(s, x, y, ww, Inches(0.85),
|
||||||
|
[[(t, 15, WHITE, True)],[(d, 12, WHITE, False)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=1)
|
||||||
|
y = y + Inches(1.0)
|
||||||
|
# 右:要点卡片
|
||||||
|
pts = [
|
||||||
|
("每条线索可解释","附证据链 + 判定理由,拒绝黑盒打分。", CYAN),
|
||||||
|
("反馈学习闭环","审计员标注误报/属实,系统持续校准阈值,准确率随使用上升。", BLUE),
|
||||||
|
("公开运营指标","命中率、准确率、线索转化率上看板,成效可量化可追溯。", GOLD),
|
||||||
|
]
|
||||||
|
x = Inches(7.4); yy = Inches(2.35)
|
||||||
|
for t,d,col in pts:
|
||||||
|
rrect(s, x, yy, Inches(5.1), Inches(0.85), color=CARD, radius=0.1)
|
||||||
|
rect(s, x, yy, Inches(0.1), Inches(0.85), color=col)
|
||||||
|
txt(s, x+Inches(0.3), yy+Inches(0.1), Inches(4.7), Inches(0.32),
|
||||||
|
[[(t, 13.5, CYAN, True)]], sa=0)
|
||||||
|
txt(s, x+Inches(0.3), yy+Inches(0.42), Inches(4.7), Inches(0.4),
|
||||||
|
[[(d, 11.5, LIGHT, False)]], sa=0, line_spacing=1.05)
|
||||||
|
yy = yy + Inches(1.0)
|
||||||
|
ny = Inches(6.05)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.7), color=CARD2, radius=0.18)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.7), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.7),
|
||||||
|
[[("▶ ", 13, GOLD, True),
|
||||||
|
("主动交代精准度反而显专业——藏着不说,才是最大的风险。", 13, WHITE, True)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
fp_control()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 17 平台架构(分层)==============
|
||||||
|
def architecture():
|
||||||
|
s = new("架构 · 全栈", "本地私有化 LLM 审计平台架构", 17)
|
||||||
|
layers = [
|
||||||
|
("应用层","自然语言查询 · 线索看板 · 智能报告 · 预警推送 —— 审计人员零门槛使用", CYAN),
|
||||||
|
("引擎层","全量穿透引擎 + 规则进化引擎 + 线索生成引擎 —— LLM 驱动三大引擎", BLUE),
|
||||||
|
("数据层","本地数据湖(BSS/OSS/ERP/财务/合同/工单/信令)—— 直连内网,零出域", PURPLE),
|
||||||
|
("模型层","千问 70B / DeepSeek / 自研行业模型 —— 审计领域微调,懂电信业务", GOLD),
|
||||||
|
("算力层","本地 A100 / H100 / 国产 GPU 集群 —— 承载 70B 级推理,信创可适配", GREEN),
|
||||||
|
]
|
||||||
|
y = Inches(1.8); h = Inches(0.82); lw = Inches(9.9)
|
||||||
|
for i,(t,d,col) in enumerate(layers):
|
||||||
|
rrect(s, Inches(0.95), y, lw, h, color=CARD, radius=0.06)
|
||||||
|
rrect(s, Inches(0.95), y, Inches(1.7), h, color=col, radius=0.06)
|
||||||
|
txt(s, Inches(0.95), y, Inches(1.7), h, [[(t, 15, WHITE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(2.85), y, lw-Inches(2.0), h, [[(d, 12.5, LIGHT, False)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
y = y + h + Inches(0.12)
|
||||||
|
# 右侧贯穿条:安全合规与自审计
|
||||||
|
rrect(s, Inches(11.05), Inches(1.8), Inches(1.45), h*5+Inches(0.48), grad=(CYAN_D, BLUE), radius=0.08)
|
||||||
|
txt(s, Inches(11.05), Inches(1.8), Inches(1.45), h*5+Inches(0.48),
|
||||||
|
[[("安", 17, WHITE, True)],[("全", 17, WHITE, True)],[("合", 17, WHITE, True)],
|
||||||
|
[("规", 17, WHITE, True)],[("·", 14, WHITE, True)],[("自", 17, WHITE, True)],
|
||||||
|
[("审", 17, WHITE, True)],[("计", 17, WHITE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=1)
|
||||||
|
ny = Inches(6.6)
|
||||||
|
txt(s, Inches(0.95), ny, Inches(11.55), Inches(0.4),
|
||||||
|
[[("全链路内网闭环 · 数据零出域 · 权限分级 · 不可篡改日志 · 版本留痕", 13.5, CYAN, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
architecture()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 18 独立性与自审计 ==============
|
||||||
|
def independence():
|
||||||
|
s = new("制度 · 独立性", "独立性与系统自审计:系统本身也经得起审计", 18)
|
||||||
|
items = [
|
||||||
|
("防放水","规则配置、阈值调整全程留痕,任何改动可追溯,杜绝调教规则放水。", CYAN),
|
||||||
|
("防拦截","线索一旦生成即不可删除,处置过程全程记录,杜绝线索被拦下。", BLUE),
|
||||||
|
("权限分级","配规则、看线索、改阈值、出报告分权管理,相互制衡。", PURPLE),
|
||||||
|
("三重留痕","模型版本、规则版本、数据版本可回溯,任一结论可还原当时状态。", GOLD),
|
||||||
|
]
|
||||||
|
W, H = Inches(5.7), Inches(1.95)
|
||||||
|
gx,gy = Inches(0.95), Inches(1.9)
|
||||||
|
for i,(t,d,col) in enumerate(items):
|
||||||
|
r,c = divmod(i,2)
|
||||||
|
x = gx + c*(W+Inches(0.18)); y = gy + r*(H+Inches(0.22))
|
||||||
|
rrect(s, x, y, W, H, color=CARD, radius=0.08)
|
||||||
|
circle(s, x+Inches(0.3), y+Inches(0.32), Inches(0.7), grad=(col, BG2))
|
||||||
|
txt(s, x+Inches(0.3), y+Inches(0.32), Inches(0.7), Inches(0.7),
|
||||||
|
[[("🔒" if False else "■", 18, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, x+Inches(1.2), y+Inches(0.3), W-Inches(1.4), Inches(0.5),
|
||||||
|
[[(t, 17, col, True)]], sa=0)
|
||||||
|
txt(s, x+Inches(1.2), y+Inches(0.85), W-Inches(1.45), Inches(1.0),
|
||||||
|
[[(d, 12.5, LIGHT, False)]], sa=0, line_spacing=1.15)
|
||||||
|
txt(s, Inches(0.95), Inches(6.4), Inches(11.55), Inches(0.4),
|
||||||
|
[[("既当运动员又当裁判是内审大忌——用制度化留痕与分权,让系统自己也透明可查。", 13, CYAN, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
independence()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 19 四重跃升 ==============
|
||||||
|
table_slide(19, "价值 · 跃升", "本地 LLM 带来的四重跃升",
|
||||||
|
["关键跃升", "从 → 到", "价值内涵"],
|
||||||
|
[
|
||||||
|
["审计覆盖面", "5% → 100%", "全量扫描,异常无处藏身"],
|
||||||
|
["数据出域风险", "存在 → 归零", "全链路内网闭环,满足等保最严要求"],
|
||||||
|
["审计节奏", "年度快照 → 7×24 常态化", "动态舞弊实时捕捉"],
|
||||||
|
["能力归属", "外部租用 → 本地永久沉淀", "规则进化,越用越聪明"],
|
||||||
|
],
|
||||||
|
note="安全 · 能力 · 效率 · 进化——四重价值,远超传统 BI 工具。",
|
||||||
|
col_widths=[2.8, 4.2, 4.55], fs=13, hfs=13.5)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 20 ROI ==============
|
||||||
|
table_slide(20, "测算 · 回报", "价值测算:把“异常”变成客户的钱",
|
||||||
|
["价值来源", "测算逻辑", "年化收益(保守)"],
|
||||||
|
[
|
||||||
|
["可挽回收入/止损", "全量覆盖挖出抽样漏掉的异常并整改", "数千万级"],
|
||||||
|
["外部咨询费节省", "常态化自有能力替代重复性项目采购", "百万级 / 年"],
|
||||||
|
["人力释放", "审计员从翻表取数转向研判处置", "数倍效率提升"],
|
||||||
|
["风险事件预防", "提前发现合规风险,规避处罚与声誉损失", "难以估量"],
|
||||||
|
],
|
||||||
|
note="投入一次本地化建设,沉淀的是持续产生收益的永久资产,而非每年重复支出的项目费用。",
|
||||||
|
col_widths=[2.9, 5.85, 2.8], fs=13, hfs=13.5)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 21 差异化 ==============
|
||||||
|
def differentiation():
|
||||||
|
s = new("差异化 · 主张", "我们的差异化:能力沉淀,而非一次性交付", 21)
|
||||||
|
pairs = [
|
||||||
|
("能力沉淀","项目制交付","项目制是租大脑、人走经验走;我们是装一个永久、越用越聪明的本地大脑。", CYAN),
|
||||||
|
("常态化","年度快照","舞弊是动态的,时序类造假正是本地 LLM + 全量数据的主场。", BLUE),
|
||||||
|
("数据不出域","数据出域","一比特不出机房是结构性优势,让安全合规部门站在我们这边。", PURPLE),
|
||||||
|
("共存切入","正面替代","先做以前做不动的全量穿透与常态化监控层,跑出线索、证明价值、自然扩展。", GOLD),
|
||||||
|
]
|
||||||
|
y = Inches(1.85); h = Inches(1.12)
|
||||||
|
for i,(a,b,d,col) in enumerate(pairs):
|
||||||
|
rrect(s, Inches(0.95), y, Inches(11.55), h, color=CARD if i%2 else CARD2, radius=0.07)
|
||||||
|
rrect(s, Inches(1.15), y+Inches(0.28), Inches(2.4), Inches(0.56), color=col, radius=0.3)
|
||||||
|
txt(s, Inches(1.15), y+Inches(0.28), Inches(2.4), Inches(0.56),
|
||||||
|
[[(a, 14, WHITE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(3.65), y, Inches(0.7), h, [[("vs", 14, MUTE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
rrect(s, Inches(4.35), y+Inches(0.28), Inches(2.2), Inches(0.56), color=BG2, line=MUTE, lw=1, radius=0.3)
|
||||||
|
txt(s, Inches(4.35), y+Inches(0.28), Inches(2.2), Inches(0.56),
|
||||||
|
[[(b, 13, MUTE, True)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, Inches(6.75), y, Inches(5.6), h, [[(d, 12.5, LIGHT, False)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=0, line_spacing=1.12)
|
||||||
|
y = y + h + Inches(0.13)
|
||||||
|
differentiation()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 22 实施路径(时间轴)==============
|
||||||
|
def roadmap():
|
||||||
|
s = new("实施 · 路径", "3 个月本地部署跑通(含同台盲测验证)", 22)
|
||||||
|
phases = [
|
||||||
|
("第 1 个月","算力 + 模型部署","GPU 到位;模型本地化部署;对接各业务系统;构建本地数据湖。", CYAN),
|
||||||
|
("第 2 个月","场景微调 + 历史盲测","行业微调与场景适配;用历史数据全量重跑,与既有审计结论同台盲测。", BLUE),
|
||||||
|
("第 3 个月","投产 + 线索闭环","正式上线;生成首批 200-500 条线索;核查反馈;规则库首轮进化。", GOLD),
|
||||||
|
]
|
||||||
|
# 时间轴
|
||||||
|
rect(s, Inches(1.2), Inches(2.55), Inches(11.0), Pt(3), color=CARD2)
|
||||||
|
W = Inches(3.7); gx = Inches(0.95)
|
||||||
|
for i,(ph,t,d,col) in enumerate(phases):
|
||||||
|
x = gx + i*(W+Inches(0.22))
|
||||||
|
cx = x + W/2
|
||||||
|
circle(s, cx-Inches(0.18), Inches(2.4), Inches(0.36), color=col)
|
||||||
|
rrect(s, x, Inches(2.95), W, Inches(2.4), color=CARD, radius=0.07)
|
||||||
|
rect(s, x, Inches(2.95), W, Inches(0.6), color=col)
|
||||||
|
txt(s, x, Inches(2.95), W, Inches(0.6), [[(ph, 16, WHITE, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, sa=0)
|
||||||
|
txt(s, x+Inches(0.25), Inches(3.7), W-Inches(0.5), Inches(0.5),
|
||||||
|
[[(t, 14.5, col, True)]], sa=0)
|
||||||
|
txt(s, x+Inches(0.25), Inches(4.2), W-Inches(0.5), Inches(1.1),
|
||||||
|
[[(d, 12.5, LIGHT, False)]], sa=0, line_spacing=1.18)
|
||||||
|
ny = Inches(5.7)
|
||||||
|
rrect(s, Inches(0.95), ny, Inches(11.55), Inches(0.95), color=CARD2, radius=0.1)
|
||||||
|
rect(s, Inches(0.95), ny, Inches(0.1), Inches(0.95), color=GOLD)
|
||||||
|
txt(s, Inches(1.25), ny, Inches(11.1), Inches(0.95),
|
||||||
|
[[("交付物", 14, GOLD, True)],
|
||||||
|
[("本地 AI 审计平台 + 可进化规则库 + 已验证高价值线索 + 同台盲测成效报告。", 13, WHITE, False)]],
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, sa=3)
|
||||||
|
roadmap()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 23 结尾 ==============
|
||||||
|
def closing():
|
||||||
|
s = prs.slides.add_slide(BLANK)
|
||||||
|
bg_gradient(s, RGBColor(0x07,0x10,0x24), RGBColor(0x10,0x29,0x52))
|
||||||
|
circle(s, Inches(-1.5), Inches(4.5), Inches(4.5), color=RGBColor(0x10,0x24,0x48))
|
||||||
|
circle(s, Inches(10.5), Inches(-1.5), Inches(4.5), color=RGBColor(0x10,0x24,0x48))
|
||||||
|
rect(s, 0, 0, SW, Inches(0.18), grad=(CYAN, BLUE))
|
||||||
|
rect(s, 0, Inches(7.32), SW, Inches(0.18), grad=(BLUE, CYAN))
|
||||||
|
txt(s, Inches(1.0), Inches(2.0), Inches(11.3), Inches(1.2),
|
||||||
|
[[("数据不动 · AI 动脑 · 造假者跑不掉", 40, WHITE, True)]], align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
rect(s, Inches(5.4), Inches(3.25), Inches(2.5), Pt(3), color=CYAN)
|
||||||
|
txt(s, Inches(1.0), Inches(3.5), Inches(11.3), Inches(0.7),
|
||||||
|
[[("本地大模型 + 全量穿透 + 规则进化 = 运营商内审的“新质生产力”", 19, CYAN, True)]],
|
||||||
|
align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
txt(s, Inches(1.0), Inches(4.5), Inches(11.3), Inches(1.2),
|
||||||
|
[[("让我们把千问 70B 装进您的机房", 17, LIGHT, False)],
|
||||||
|
[("150 亿业务全量扫描,敏感数据一比特不出域", 17, LIGHT, False)]],
|
||||||
|
align=PP_ALIGN.CENTER, sa=8)
|
||||||
|
txt(s, Inches(1.0), Inches(6.3), Inches(11.3), Inches(0.5),
|
||||||
|
[[("2026 年 6 月", 13, MUTE, False)]], align=PP_ALIGN.CENTER, sa=0)
|
||||||
|
closing()
|
||||||
|
|
||||||
|
|
||||||
|
out = "数据不出域,审计全穿透_精美版.pptx"
|
||||||
|
prs.save(out)
|
||||||
|
print("saved:", out, "slides:", len(prs.slides._sldIdLst))
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# 数据不出域,审计全穿透(优化版)
|
||||||
|
|
||||||
|
> 基于本地私有化大模型的电信运营商 AI 全域内审体系
|
||||||
|
> 让 150 亿业务里的每一分钱,都在本地 AI 的显微镜下原形毕露
|
||||||
|
> 2026 年 6 月
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 1 · 封面
|
||||||
|
|
||||||
|
### 数据不出域,审计全穿透
|
||||||
|
|
||||||
|
- 基于本地私有化大模型的电信运营商 AI 全域内审体系
|
||||||
|
- 不是一套工具,而是一套"建在自己机房里、越用越聪明"的审计能力体系
|
||||||
|
- 2026 年 6 月
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 2 · 为什么传统审计"查不过来"?
|
||||||
|
|
||||||
|
**三个数字看清困局**
|
||||||
|
|
||||||
|
- **150 亿** —— 年业务规模
|
||||||
|
- **5000 万** —— 潜在异常金额
|
||||||
|
- **5%** —— 传统抽样覆盖率
|
||||||
|
|
||||||
|
**三类典型困局**
|
||||||
|
|
||||||
|
- **"拆单规避"**:8 个客户各签 600 万 ICT 项目,全拆成 80 万以下小额合同,三重一大抽样完美避开。按金额抽样,大额拆分后消失在雷达之外。
|
||||||
|
- **"稳定的定,稳定的退"**:每月新增 6000 人订购彩铃,3 个月后首月用户全部退订。渠道已按新增量领取佣金,形成"骗补后弃养"闭环——造假藏在时序里。
|
||||||
|
- **"Excel 干不过来"**:安全云盘 + 宏 + 人工,面对海量单据只能抽样,查不全、查不深。150 亿业务海洋中,5000 万异常如针落大海。
|
||||||
|
|
||||||
|
> **核心矛盾**:审计数据涉及政企合同、用户隐私、财务凭证,上公有云大模型 = 裸奔;不上 AI 又干不过来。怎么办?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 3 · 本地 LLM 让"安全"和"智能"不再二选一
|
||||||
|
|
||||||
|
| 对比维度 | 传统抽样审计 | 公有云 AI 审计 | 本地 LLM 审计(我们) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 数据范围 | 按金额抽样,查不全 | 全量扫描,但数据出域 | 全量扫描,数据不出机房 |
|
||||||
|
| 规则能力 | 规则写死,反向规避 | 模型能力强,但合规风险高 | 模型私有化,合规可控 |
|
||||||
|
| 响应效率 | Excel 翻表,效率低 | 实时预警,但依赖外网 | 内网闭环,秒级响应 |
|
||||||
|
| 交互模式 | 人找数据 | 数据找人,但数据送人 | 数据找人,数据原地不动 |
|
||||||
|
| 能力归属 | 经验在人脑,人走经验走 | 能力在外部,租用即失 | 能力沉淀在本地,越用越聪明 |
|
||||||
|
|
||||||
|
> 把千问 70B / DeepSeek 装进本地机房,让 AI 在数据旁边干活,而不是把数据送给 AI。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 4 · 【新增】审计域全景图 + 风险分级模型
|
||||||
|
|
||||||
|
**不是 8 个孤立场景,而是一张覆盖全业务的审计地图**
|
||||||
|
|
||||||
|
把所有审计场景归入五大风险域,做到"全覆盖、有优先级":
|
||||||
|
|
||||||
|
| 风险域 | 覆盖场景 | 关注核心 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 收入域 | 政企收入穿透、收入成本跨期匹配、云业务空转 | 收入真实性、确认时点 |
|
||||||
|
| 成本域 | 渠道佣金、终端补贴、成本摊销 | 成本真实性、套利 |
|
||||||
|
| 采购域 | 网络建设、工程采购、围标串标 | 采购合规、虚增工程量 |
|
||||||
|
| 资金域 | 回款挂账、网间结算、资金流向 | 资金真实性、关联交易 |
|
||||||
|
| 合规域 | 员工舞弊、权限滥用、积分套现 | 内控有效性、权限合规 |
|
||||||
|
|
||||||
|
**风险热力图(发生概率 × 金额影响)**
|
||||||
|
|
||||||
|
- 高概率 + 高金额 → 优先全量监控(如政企拆单、渠道骗补)
|
||||||
|
- 低概率 + 高金额 → 重点定向穿透(如围标串标、云空转)
|
||||||
|
- 高概率 + 低金额 → 批量聚类筛查(如积分套现、内部号码)
|
||||||
|
|
||||||
|
> 让客户一眼看出:我们不是"工具集合",是"有体系、有优先级的全域审计框架"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 5 · "本地 AI 审计大脑"四大核心引擎
|
||||||
|
|
||||||
|
- **① 本地私有化 LLM 引擎(底座)**:千问 70B / DeepSeek 等模型本地化部署,审计数据绝不出域。负责异常模式推理、自然语言规则配置、报告自动生成、线索解释。
|
||||||
|
- **② 全量穿透引擎**:直连 BSS / OSS / ERP / 财务系统,本地数据库直接喂给本地 LLM。不抽样,对所有合同、回款、用户行为做关联扫描。
|
||||||
|
- **③ 规则进化引擎(护城河)**:审计人员用自然语言描述新造假模式,LLM 自动转化为可执行规则,沙箱验证命中率,持续对抗迭代——**把顾问脑子里的经验固化成机构永久资产**。
|
||||||
|
- **④ 线索驱动引擎**:LLM 对异常聚类做"人话解释"(如"这 8 个客户疑似同一实控人"),输出高价值线索并附证据链,直接推送审计人员桌面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 6 · 场景一:政企收入全链路穿透
|
||||||
|
|
||||||
|
- **业务链路**:客户立项 → 移动立项 → 审批 → 报价 → 签约 → 首款/二款开票 → 回款
|
||||||
|
- **经典案例(拆单规避 + 虚假回款)**:某地市公司 8 个"客户"各签 600 万 ICT 项目,全拆成 79 万-99 万合同规避三重一大审批。首款付 100 万、二款付 300 万,尾款 500 万长期挂账。经穿透,8 个客户注册地址在同一写字楼、法人为同一人亲属、付款账户来自同一实控企业。
|
||||||
|
- **AI 审计点**:合同金额分布异常(集中在阈值边缘);工商关联穿透(隐性实控人识别);回款时序聚类(批量违约模式)。
|
||||||
|
- **本地 LLM 能力**:自然语言查数、关联推理、一键生成《政企客户回款异常专项线索清单》。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 7 · 场景二:市场业务真实性("养卡骗补")
|
||||||
|
|
||||||
|
- **经典案例(稳定的定,稳定的退)**:某渠道每月新增 6000 人订购彩铃,每过三个月首月用户全部退订,渠道已按新增量领取佣金,形成"骗补后弃养"闭环。用户号码归属地高度集中在某几个乡镇,且通话记录为零。
|
||||||
|
- **扩展案例(物联网卡虚假激活)**:某代理商批量开通 10 万张物联网卡,声称用于"智慧停车",实际激活后无任何流量,已按激活量领取每台 50 元补贴,半年后卡片集体沉默。
|
||||||
|
- **AI 审计点**:用户生命周期时序模式识别;渠道佣金与业务质量匹配度;沉默/零通话用户批量聚类;项目交付物与收入确认交叉验证。
|
||||||
|
- **本地 LLM 能力**:识别"脉冲式增长 + 规律性衰减"的周期性造假,自动提炼为新规则。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 8 · 场景三:收入与成本跨期匹配
|
||||||
|
|
||||||
|
- **经典案例(趸交收入一次性确认)**:用户办 24 个月套餐送智能手表,收入应分 24 个月确认,但因趸交财务一把全确认,手表成本却摊 24 个月——确认时点严重错配。某省一年此类业务 5000 万,在 150 亿总收入中如针落大海。
|
||||||
|
- **扩展案例("以销定产"变"提前确认")**:某政企云项目约定"按实际使用量计费",但财务在设备上架当月即全额确认收入,客户前 6 个月几乎零使用。
|
||||||
|
- **AI 审计点**:自动勾稽收入确认政策 vs 实际账务 vs 合同条款;识别趸交/预收款一次性确认异常分录;成本摊销与收入确认跨期匹配;设备交付与收入确认时间差监控。
|
||||||
|
- **本地 LLM 能力**:跨系统自动勾稽,识别收入成本确认时点错配的异常分录模式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 9 · 场景四:渠道佣金与代理商套利
|
||||||
|
|
||||||
|
- **经典案例(虚假放号 + 套机套卡)**:某代理商为完成"5G 用户净增",批量买低价老人机插 5G SIM 卡激活后丢弃,用户从未产生 5G 流量,已领"5G 迁转"佣金每台 200 元 + 终端补贴 300 元,次月用户全部流失。
|
||||||
|
- **扩展案例(异地窜货套利)**:代理商从邻省低价采购同款手机,在本省以"新用户入网"名义领高额补贴,手机实际回流二级市场。
|
||||||
|
- **AI 审计点**:终端 IMEI 与用户绑定真实性;佣金发放与在网时长匹配度;终端流向追踪(激活即沉默/跨省流通);代理商业务质量时序衰减分析。
|
||||||
|
- **本地 LLM 能力**:IMEI 级终端流向追踪,识别"激活-沉默-流失"套利闭环。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 10 · 场景五:网络建设与工程采购
|
||||||
|
|
||||||
|
- **经典案例(围标串标 + 虚增工程量)**:某基站项目 3 家投标报价差异不足 1%,技术方案大量雷同,中标后施工队为同一班组,工程量签证单存在"同一笔迹不同日期"批量签字。
|
||||||
|
- **扩展案例(虚假巡检与虚报工单)**:某外包商系统显示每月完成 2000 次基站巡检,GPS 轨迹比对实际只到过 300 个站点,其余为"照片复用 + 坐标伪造"。
|
||||||
|
- **AI 审计点**:投标关联分析(报价相似度、文件雷同度);工程量与资源消耗匹配验证;巡检轨迹与工单交叉验证;供应商画像(同一实控人"马甲"识别)。
|
||||||
|
- **本地 LLM 能力**:NLP 比对投标文件雷同度,GPS 轨迹与工单交叉验证,识别"马甲"供应商。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 11 · 场景六:互联互通与网间结算
|
||||||
|
|
||||||
|
- **经典案例(话务量操纵套利)**:某运营商与境外运营商合谋虚假国际来话刷量,主叫归属地为虚商号段,通话时长均为 30 秒/60 秒整数倍,明显非真人。
|
||||||
|
- **扩展案例(短信网关刷量)**:某 SP 伪造发送记录申报"成功发送"10 亿条行业短信按 0.05 元/条结算,实际到达率不足 10%。
|
||||||
|
- **AI 审计点**:话务量时序异常(突发峰值、整数时长聚集);网间结算数据与网络侧原始信令比对;SP/CP 业务量与收入结算交叉验证;国际来话真实路由溯源。
|
||||||
|
- **本地 LLM 能力**:识别"整数倍通话时长"等非人类行为,信令级原始数据比对。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 12 · 场景七:云业务 / IDC 与新兴业务
|
||||||
|
|
||||||
|
- **经典案例(云资源"空转"确认收入)**:某政企客户签 3 年云服务年付 100 万,实际 CPU 利用率长期低于 5%、存储几乎为空,但财务按合同全额确认收入,且该"客户"实控人为地市公司某领导亲属。
|
||||||
|
- **扩展案例(IDC 机柜"虚租")**:某 IDC 宣称出租率 90%,实际大量机柜无设备、电费为零,收入来自关联方"预付租金"。
|
||||||
|
- **AI 审计点**:云资源实际使用量 vs 合同计费量匹配度;IDC 出租率与电力消耗勾稽;新兴业务客户画像(关联方识别、预付模式异常);收入确认与交付验收时序一致性。
|
||||||
|
- **本地 LLM 能力**:资源利用率与计费量自动比对,关联方网络挖掘,识别"空转"收入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 13 · 场景八:员工内部舞弊与资源滥用
|
||||||
|
|
||||||
|
- **经典案例(内部号码套利)**:某营业厅员工利用权限批量开通"员工测试号"对外出租"免流套餐",测试号产生大量流量收入但全部计入内部成本未确认收入。
|
||||||
|
- **扩展案例(积分/会员体系套现)**:某员工勾结外部商户虚构消费批量刷积分,兑换高价值礼品卡在二级市场变现,某商户单日积分发放量超正常 100 倍。
|
||||||
|
- **AI 审计点**:员工权限操作日志异常模式识别;内部测试号实际用途偏离;积分/电子券流向追踪;权限与岗位匹配度(如客服岗有财务调账权限)。
|
||||||
|
- **本地 LLM 能力**:操作日志异常模式挖掘,权限-岗位匹配度分析,积分流向网络追踪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 14 · 【新增】数据接入与治理层(地基工程)
|
||||||
|
|
||||||
|
**全量穿透的前提,是把脏活干在前面**
|
||||||
|
|
||||||
|
- **多源异构接入**:适配 BSS / OSS / ERP / 财务 / 合同 / 工单 / 信令各系统的接口、数据库、文件,统一汇入本地数据湖。
|
||||||
|
- **主数据对齐**:客户、合同、号码、工单、供应商跨系统实体统一,解决"主键对不上"。
|
||||||
|
- **数据质量探查与清洗**:缺失、重复、口径不一自动探查并清洗,建立质量评分。
|
||||||
|
- **增量同步与时效**:从年度快照升级为近实时增量,支撑常态化监控。
|
||||||
|
|
||||||
|
> 数据治理是这套体系工作量最大、最该提前立项的一环。我们把它写进方案、承担下来,而不是回避。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 15 · 【新增】人机协同闭环:线索之后才是价值
|
||||||
|
|
||||||
|
**从"发现工具"升级为"办案平台"**
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 全量扫描 → 生成线索 + 初步证据链 → 审计员复核研判 →
|
||||||
|
系统自动生成审计底稿 → 定性分类 → 整改 / 移交 → 复核销项闭环
|
||||||
|
```
|
||||||
|
|
||||||
|
- **AI 侧**:出线索、附证据链、给判定理由、自动生成可追溯底稿。
|
||||||
|
- **审计员侧**:复核研判、定性、决定整改或移交、最终签字。
|
||||||
|
- **闭环管理**:线索分派、取证留痕、整改跟踪、销项复核全流程在线。
|
||||||
|
|
||||||
|
> 不是"给你一堆线索然后呢",而是"从发现到闭环,每一步都接得住、留得痕"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 16 · 【新增】误报治理与置信度分级(专业 = 诚实)
|
||||||
|
|
||||||
|
**全量扫描必然产生海量疑似项——关键是不让审计员淹死在假阳性里**
|
||||||
|
|
||||||
|
- **三级置信分流**:高置信直接推送处置、中置信人工复核、低置信归档备查。
|
||||||
|
- **每条线索可解释**:附证据链 + 判定理由,拒绝"黑盒打分"。
|
||||||
|
- **反馈学习闭环**:审计员标注"误报/属实",系统持续校准阈值,准确率随使用上升。
|
||||||
|
- **公开运营指标**:命中率、准确率、线索转化率上看板,成效可量化、可追溯。
|
||||||
|
|
||||||
|
> 主动交代精准度,反而显专业。藏着不说,才是最大的风险。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 17 · 本地私有化 LLM 审计平台架构
|
||||||
|
|
||||||
|
- **应用层**:自然语言查询 · 线索看板 · 智能报告 · 预警推送 —— 审计人员零门槛使用
|
||||||
|
- **引擎层**:全量穿透引擎 + 规则进化引擎 + 线索生成引擎 —— LLM 驱动三大引擎
|
||||||
|
- **数据层**:本地数据湖(BSS / OSS / ERP / 财务 / 合同 / 工单 / 信令)—— 直连内网,零出域
|
||||||
|
- **模型层**:千问 70B / DeepSeek / 自研行业模型 —— 审计领域微调,懂电信业务
|
||||||
|
- **算力层**:本地 A100 / H100 / 国产 GPU 集群 —— 承载 70B 级大模型推理,信创可适配
|
||||||
|
- **安全合规与自审计层(贯穿全栈)**:权限分级 · 操作不可篡改日志 · 模型/规则版本留痕 · 全链路审计轨迹
|
||||||
|
|
||||||
|
> 全链路内网闭环 · 数据零出域
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 18 · 【新增】独立性与系统自审计(制度设计)
|
||||||
|
|
||||||
|
**审计系统本身,也要经得起审计**
|
||||||
|
|
||||||
|
- **防放水**:规则配置、阈值调整全程留痕,任何人改动可追溯,杜绝"调教规则放水"。
|
||||||
|
- **防拦截**:线索一旦生成即不可删除,处置过程全程记录,杜绝"线索被领导拦下"。
|
||||||
|
- **权限分级**:配规则、看线索、改阈值、出报告分权管理,相互制衡。
|
||||||
|
- **可追溯**:模型版本、规则版本、数据版本三重留痕,任一结论可回溯到当时的模型与数据状态。
|
||||||
|
|
||||||
|
> 既当运动员又当裁判是内审的大忌——我们用制度化的留痕和分权,让这套系统自己也透明可查。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 19 · 本地 LLM 带来的四重跃升
|
||||||
|
|
||||||
|
- **安全价值**:敏感数据不出机房,满足国资/运营商/等保最严要求,模型-数据-推理-结果全链路内网闭环。
|
||||||
|
- **能力价值**:70B 级本地模型具备语义推理、规则自生长、报告生成能力,远超传统 BI;行业微调,懂电信业务。
|
||||||
|
- **效率价值**:自然语言交互,不写 SQL、不翻 Excel,问一句就出线索,从"人找数据"到"数据找人"。
|
||||||
|
- **进化价值**:每发现一种造假,LLM 自动提炼规则,系统越用越精准,形成机构专属审计知识库。
|
||||||
|
|
||||||
|
| 关键跃升 | 从 → 到 |
|
||||||
|
| --- | --- |
|
||||||
|
| 审计覆盖面 | 5% → 100% |
|
||||||
|
| 数据出域风险 | 存在 → 归零 |
|
||||||
|
| 审计节奏 | 年度快照 → 7×24 常态化 |
|
||||||
|
| 能力归属 | 外部租用 → 本地永久沉淀 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 20 · 【新增】价值测算:把"异常"变成客户的钱
|
||||||
|
|
||||||
|
**以 150 亿业务规模、5000 万潜在异常为基准的保守测算**
|
||||||
|
|
||||||
|
| 价值来源 | 测算逻辑 | 年化收益(保守) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 可挽回收入/止损 | 全量覆盖挖出抽样漏掉的异常并整改 | 数千万级 |
|
||||||
|
| 外部咨询费节省 | 常态化自有能力替代重复性项目制采购 | 百万级/年 |
|
||||||
|
| 人力释放 | 审计员从翻表取数转向研判处置 | 数倍效率提升 |
|
||||||
|
| 风险事件预防 | 提前发现合规风险,规避处罚与声誉损失 | 难以估量 |
|
||||||
|
|
||||||
|
> 投入一次本地化建设,沉淀的是持续产生收益的永久资产,而非每年重复支出的项目费用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 21 · 【新增】我们的差异化:能力沉淀,而非一次性交付
|
||||||
|
|
||||||
|
**为什么是"建一套体系",而不是"买一份报告"**
|
||||||
|
|
||||||
|
- **能力沉淀 vs 项目制交付**:项目制是"租大脑",人走经验走、明年再付一次;我们是"装一个永久的、越用越聪明的本地大脑",规则进化引擎把每一次审计经验固化为机构资产。
|
||||||
|
- **常态化 vs 年度快照**:舞弊是动态的,审计不能一年一次。时序类造假(养卡、骗补、脉冲式增长)恰恰是抽样和年度审计抓不到的,正是本地 LLM + 全量数据的主场。
|
||||||
|
- **数据不出域 vs 数据出域**:对等保/国资/数据安全红线极高的运营商,"一比特不出机房"是结构性优势,让安全合规部门站在我们这边。
|
||||||
|
- **共存切入 vs 正面替代**:先做底层全量穿透与常态化监控这块"以前做不动的层",跑出线索、证明价值,能力自然沉淀、份额自然扩展。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 22 · 3 个月本地部署跑通(含同台盲测验证)
|
||||||
|
|
||||||
|
- **第 1 个月 · 算力 + 模型部署**:机房 GPU 到位;千问 70B / DeepSeek 本地化部署;对接 BSS/OSS/ERP/财务/工单/信令;构建本地数据湖。
|
||||||
|
- **第 2 个月 · 场景微调 + 历史盲测**:历史审计案例行业微调;政企/市场/财务/工程场景适配;**用过去 2-3 年历史数据全量重跑,与既有审计结论同台盲测,验证能否挖出此前抽样漏掉的真实线索**。
|
||||||
|
- **第 3 个月 · 投产 + 线索闭环**:正式上线;生成首批 200-500 条线索;审计人员跟进核查反馈;规则库首轮进化。
|
||||||
|
|
||||||
|
> **交付物**:一套本地私有化 AI 审计平台 + 一套可进化的审计规则库 + 一批已验证的高价值线索 + 一份同台盲测成效报告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 幻灯片 23 · 审计的终极形态
|
||||||
|
|
||||||
|
- **数据不动、AI 动脑、造假者跑不掉**
|
||||||
|
- 本地大模型 + 全量穿透 + 规则进化 = 运营商内审的"新质生产力"
|
||||||
|
- 让我们把千问 70B 装进您的机房
|
||||||
|
- 150 亿业务全量扫描,敏感数据一比特不出域——这才是电信运营商该有的 AI 审计
|
||||||
|
|
||||||
|
> 2026 年 6 月
|
||||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
|||||||
|
-- 初始化 AIAudit 数据中台所需的 PostgreSQL 扩展
|
||||||
|
-- 容器首次启动时由官方 entrypoint 自动执行(/docker-entrypoint-initdb.d)
|
||||||
|
|
||||||
|
-- 时序(TimescaleDB):支撑时间序列建模
|
||||||
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||||
|
|
||||||
|
-- 向量检索(pgvector):支撑语义检索
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
|
-- btree_gist:支撑双时态有效期的排他约束(防止时间区间重叠)
|
||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||||
|
|
||||||
|
-- 注:MVP 阶段知识图谱采用关系表(实体/关系边)+ 递归 CTE 建模,
|
||||||
|
-- 不依赖 Apache AGE 扩展。后续按需再引入。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本地 PostgreSQL 16 初始化脚本(弃用 Docker,使用本机 Homebrew PG16)
|
||||||
|
# 创建 aiaudit 角色与数据库,并安装所需扩展(TimescaleDB / pgvector / btree_gist)。
|
||||||
|
#
|
||||||
|
# 前置:已通过 Homebrew 安装并启动 postgresql@16、timescaledb、pgvector。
|
||||||
|
# 用法:bash infra/postgres/setup_local.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_NAME="${DB_NAME:-aiaudit}"
|
||||||
|
DB_USER="${DB_USER:-aiaudit}"
|
||||||
|
DB_PASSWORD="${DB_PASSWORD:-aiaudit_dev}"
|
||||||
|
|
||||||
|
# 定位本地 PG16 的 psql(Homebrew)
|
||||||
|
PG_BIN="$(brew --prefix postgresql@16)/bin"
|
||||||
|
export PATH="$PG_BIN:$PATH"
|
||||||
|
|
||||||
|
echo "==> 使用 psql: $(which psql)"
|
||||||
|
|
||||||
|
# 以当前系统用户连接默认库(Homebrew PG 默认信任本地超级用户)
|
||||||
|
psql postgres <<SQL
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
|
||||||
|
CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASSWORD}';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
SELECT 'CREATE DATABASE ${DB_NAME} OWNER ${DB_USER}'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}')\gexec
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "==> 安装扩展到数据库 ${DB_NAME}"
|
||||||
|
# pgvector 与 btree_gist 为必需;TimescaleDB 为可选(macOS 本地常因编译问题装不上,
|
||||||
|
# 生产 Linux 环境再启用)。缺失 timescaledb 不影响功能,仅少了超表分区/压缩优化。
|
||||||
|
psql -d "${DB_NAME}" <<'SQL'
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'timescaledb') THEN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||||
|
RAISE NOTICE 'TimescaleDB 已启用';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'TimescaleDB 不可用,跳过(本地开发可忽略)';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "==> 完成。扩展清单:"
|
||||||
|
psql -d "${DB_NAME}" -c "SELECT extname, extversion FROM pg_extension ORDER BY extname;"
|
||||||
Reference in New Issue
Block a user