审计日期:2026-09-30 范围:
ai-server/src/main/java(约 860 文件、53 个 Controller、216 个端点)+ 配置 + 文档 方法:3 轮全仓扫描(容错与资源管理 / 异常处理与可观测性 / API 易用性与开发者体验),Top 发现经人工源码核实。 前置说明:并发问题已在concurrency-issues-and-fixes.md闭环(29 项,27 项已修复),本报告不含并发类问题。 结论计数:稳定性 17 项(高 3 / 中 8 / 低 6)| 可观测性 12 项(高 3 / 中 6 / 低 3)| 易用性 22 项(高 4 / 中高 3 / 中 8 / 低 7)。
本报告中以下三项已实施并测试通过(Top10 表中 #1、#5、#10 的稳定性部分):
| 项 | 实施 | 改动 |
|---|---|---|
| O-1 PII 日志 | 已修复 | AbstractDataCleaner:提取结果日志改为脱敏输出(新增 maskForLog:保留前4后2、短值留首字符),"预处理后文本"整段内容不再打印(只留长度,降 debug);GraphService 的 System.out.println(sql)(含手机号/卡号节点值)移除;删除整文件注释的死代码 GraphServicecopy.java(613 行,内含同类打印) |
| S-2 ES 写入主链路 | 已修复 | SearchDocIndexService:add()/flush() 全部容错化(ES 故障记日志+丢弃计数,绝不上抛清洗链路);新增熔断——连续 3 批失败后熔断 60s,期间 add() 直接跳过(不再每批承受 60s 超时),成功即复位。缺失数据按案件重建索引补偿。新增 SearchDocIndexServiceTest(4 用例:不上抛/熔断开/成功复位/flush 降级) |
| S-4 附件 RAG | 已修复 | ① ChatAttachmentService 路径 C 检索失败降级为"无 RAG 片段的普通回答"(fail-open,不阻断整条消息);② AttachmentDocIndexer.index() 增加单附件总预算 zsjz.chat-attachment.index-budget-seconds(默认 600s),超时后剩余 chunk 跳过并置 truncated,杜绝上传请求被占住几十分钟。新增 AttachmentDocIndexerTest(3 用例) |
新增测试 7 个全部通过;全量 370 个测试仅剩 HEAD 既有的 8 个过时断言(工具注册数,与本轮无关),零新增回归。
一句话:系统的"骨架"是健康的(异常处理框架、流式解析、AI 清洗看门狗、LLM 失败语义都是样板级),但存在三类系统性风险——
.block() 无超时(配置项存在却没接线,挂死会永久卡住会话锁)、ES 在清洗写入主链路上无任何容错(ES 挂 = 文件级清洗失败 + 半截数据)、对话主流无应用层超时。这三处都是"好的模式已经存在,只是没铺到位"。file_ai_profile 的 PENDING 态无看门狗,重启后永久卡死;dm 清洗与治理没有 ai_clean 那样的重启自愈,且全项目无优雅停机配置。root=debug 仅控制台输出、无文件无滚动——量大、无档、敏感。这是全报告优先级最高的一条。易用性侧的系统性问题只有一个:新模块(agent/insight/aiclean)与旧模块像两代人所写——前者 REST + VO + @Valid + 测试,后者 RPC 动词 + 实体直出 + 零校验;同一语义"删除"有 5 种接口形状、分页 4 种形状、"每页条数"3 个名字。逐个端点修补无意义,需要一次契约层统一。
Top 10 行动项(按性价比排序):
| # | 问题 | 严重度 | 类型 | 工作量 |
|---|---|---|---|---|
| 1 | PII 逐条进 info 日志 + root=debug 无文件日志 | 高 | 合规/稳定 | 0.5 天 |
| 2 | 意图识别 .block() 无超时(配置项未接线)→ 卡死会话锁 |
高 | 稳定 | 0.5 天 |
| 3 | 治理 createTimeSeries 吞 SQLException 还报完成 | 高 | 稳定 | 0.5 天 |
| 4 | 预处理整批失败吞 ServerException 返回空列表 | 高 | 稳定 | 0.5 天 |
| 5 | ES 移出清洗写入主链路(旁路化 + 失败降级) | 高 | 稳定 | 1-2 天 |
| 6 | SheetProbe POI 全量加载改 SAX(OOM 风险) | 中高 | 稳定 | 1 天 |
| 7 | dm 清洗/govern 重启自愈(对齐 ai_clean 看门狗)+ 优雅停机 | 中高 | 稳定 | 2 天 |
| 8 | file_ai_profile PENDING 态补看门狗 |
中 | 稳定 | 0.5 天 |
| 9 | SSE 断线丢进度:事件落缓冲支持回放(或 /dm、/govern 补轮询通道) | 中高 | 可观测 | 1-2 天 |
| 10 | API 契约统一(错误码枚举、统一分页 VO、写操作改 POST) | 中高 | 易用 | 渐进 |
module/agent/intent/IntentService.java:84agent.call(userMsg, IntentResult.class).block() 无任何 timeout。而 IntentProperties.java:17 定义了 timeoutSeconds = 5(注释写明"超时即降级为原始问题直发"),但 IntentService/IntentMiddleware 全程未引用该配置——配置是死的。对照:FollowupService 有 blockLast(Duration.ofSeconds(8)) + 全 catch(FollowupService.java:85-91),Intent 漏了同款。IntentMiddleware.onAgent 在主对话 Flux 管线内同步调用;fail-open 只覆盖"抛异常",不覆盖"永不返回"——整条 SSE 无任何输出,且 chat 会话锁在 doFinally 释放,流不终结 → 该会话此后永远收到"正在回复中"。.block(Duration.ofSeconds(props.getTimeoutSeconds())) + TimeoutException 并入 fail-open 分支;补一条单测覆盖"配置超时生效"。module/dm/clean/AbstractCallRecordDataLoader.java:142(AbstractTransRecordDataLoader.java:107 及各 External*Loader 同型);写入服务 module/search/service/SearchDocIndexService.java:79-95doWrite 逐行调 searchDocIndexService.add(...),无 try/catch;add 攒满 1000 条时同步 repository.saveAll(batch) 直连 ES。异常沿链路抛到 CleanTask.java:108-119 → 文件标 FILE_READ_FAIL。SearchDocIndexService.buffers 里该案件尾批(≤999 条)滞留内存。add() 内部 catch(ES 失败只 log + 计数,绝不上抛清洗链路),另加"ES 连续失败 N 批后熔断 60s"防止 60s 超时反复拖慢清洗;搜索缺失数据靠文件级重建索引补偿(/search 已有按案件删除重建的底座)。module/agent/service/impl/AgentChatServiceImpl.java(doStreamLocked 的 agent.streamEvents 无 timeout 操作符);AgentModelFactory.create(未设置连接/读超时);chat SSE 无心跳(SseService 的 5s 心跳只覆盖清洗/治理通道)。events.timeout(Duration.ofMinutes(2), fallbackError)(首事件超时)+ 给 chat SSE 加与清洗通道同款心跳帧。module/agent/attachment/ChatAttachmentService.java:290(DOCUMENT 分支 docIndexer.search(...) 无 try/catch);AttachmentDocIndexer.java:96-153(500 chunk × 每块 60s 超时,无总预算)。application-dev.yaml:19-22;application.yaml:53-54 注释自认"Redis 不可用则所有 StpUtil.* 抛异常,无降级"。org.springframework.data.redis.RedisConnectionFailureException 给出专属文案"认证服务暂不可用,请稍后重试",把 500 变成可理解的提示。module/aiclean/probe/SheetProbe.java:62(WorkbookFactory.create(file),文件上限 80MB)DmService.java(saveUploadedFile 落 workspace/{caseId}/upload/{batchId}/;sweepStaleTmpBatches 只扫 PathConst.TMP_PATH;clearUploadBatch 仅用户手动触发)。application-dev.yaml(无 hikari 节,默认 maximum-pool-size=10),被请求线程 + boundedElastic 异步落库 + GlobalCache.initCache 全表读共享。SearchQueryService.java:62 leading-wildcard 查询仅性能提示。server.shutdown: graceful / spring.lifecycle.timeout-per-shutdown-phase(Boot 3 默认 IMMEDIATE);AppStopEndEventListener 停机只 flush RocksDB。FileSheetStateEnum 没有"清洗中/治理中"中间态,重启后前端看不出"上次没跑完",全靠用户记得重跑。AI 回复落库在停机窗口静默丢。FileSheetStateEnum 增加 CLEANING/GOVERNING 中间态,启动时把中间态标为 FAIL(原因:服务重启中断)——对齐 AiCleanWatchdog 的启动 sweep 范式,用户可一键重跑。file_ai_profile 的 PENDING/RUNNING 态无看门狗,重启后永久卡死FileRecognitionService(内存队列无启动重扫);FileAiProfileService.java:142(建档即 PENDING)。DmService.java createDefaultCallback(GlobalCache.initCache() 在 drain/flush 之前,且无 try/catch)。GovernService.java createTimeSeries:catch (SQLException e) 只 log.error("数据库执行错误: {}", e.getMessage())(无堆栈),随后照样 sendProgress("创建时序图完成!")——流水线继续、最终报"全部执行完成"。GlobalExceptionHandler(common/exception)是样板级的好:15 类异常全覆盖、业务异常 cause 链拆包(救回被 MyBatisSystemException 包住的"请先打开案件")、兜底 500 不泄堆栈且服务端留全栈、401 按前端约定返回 HTTP 200+code 401 且写明理由。问题都在"框架没铺到的地方"。
module/dm/clean/AbstractDataCleaner.java:211(log.info("预处理后文本:{}", cleanText)——整段原文含开户人姓名/卡号)、:244(log.info("数据提取完成,结果:姓名={}, 卡号={}, 证件号={}", ...));module/graph/service/GraphService.java:112(System.out.println(sql)——图谱查询 SQL 含手机号/卡号节点值,绕过 logback 直打 stdout);放大器:MyBatis log-impl: Slf4jImpl + root=debug 会把 WHERE 子句里的手机号/卡号字面量全量输出。GraphServicecopy.java(613 行死代码文件);见 O-2 调 root。src/main/resources/logback-spring.xml(全文 8 行)。<springProfile> 区分 dev(可 debug)/ prod。ex.getMessage() 原样透传前端,未过兜底滤网AgentChatServiceImpl.java:305-317、InsightServiceImpl.java:354-357(onErrorResume 直接 sse("error", Map.of("error", ex.getMessage())))。DmService.java:142-146(catch (Exception e) { log.error(...) } return List.of());同型 :459-462(resolvePreviewFiles,try 块内 :454 刻意抛出的"所有文件不存在或格式错误!"被吞)。SseService.java:97-111(emitter 不存在直接跳过推送;事件 id 是随机串,无 Last-Event-ID 回放)。see() 带 Last-Event-ID 回放。AiSmartCleanService.SmartCleanStatusVO(返回 stage/计数/needReview,不含 lastError)。lastError 字段(脱敏截断)。MDC.put("traceId", ...)(有 caseId/userId 也一并放入)+ logback pattern 输出;成本一天以内。/sys/health 不探测任何依赖;无 ActuatorSystemController.java:47-50(return Result.succeed());pom.xml 无 actuator。SELECT 1、Redis ping、ES exists、LLM 配置存在性,任一失败返回 code 非 0 + 各依赖状态明细);不引入 actuator 也够用,引入则加 micrometer 导出更好。log.error(:118-192 共 8 处)——污染 error 告警;GovernService.calcTreePersonData 循环级 info("根据卡号统计数据!")大案件成倍刷屏;IntentService.java:89 预期内的 fail-open 降级用 error(同文件 :141 用 warn,不一致)。SlowDownProgress.java:按"越接近 100 越慢"的随机步进模拟,与真实完成量无关;叠加 GovernService 失败分支推 SseDTO(102, 100, "治理失败:…")——前端按"进度=100 即成功"渲染会误判。status: FAILED 字段(与文案解耦)。ErrorEnum 只有 9 个条目,全仓 new ServerException(400, ...) 裸 int 遍地(AgentChatServiceImpl 5 处、ChatAttachmentService、AiSmartCleanService 等),前端只能匹配 message 文本。AgentChatController.java:126-129——GET /chat/messages/{messageId} 实际是删除消息,URL 无 delete 字样、返回 void,浏览器预取/curl 即删数据。其余:GET /chat/sessions/{id}/delete、/title(改标题)、/pin、/messages/{id}/star;GET /insight/sessions/{id}/delete;GET /capability/skills/{name}/delete、/toggle;旧模块 GET /sdc/delete、/pbi/...、/case/...、/tpl/...、/dm/cancelClean、/govern/calcTask(启动任务)等。/chat/messages/{id})改名,其余在契约版本化时一并处理。GET /sessions/{id}/delete vs GET /xxx/delete?id= vs POST /user/delete(body)vs POST /dm/deletedFiles(ids 数组)vs GET /chat/messages/{id}(无字样)。Page<T> / Map{list,total}(GovernTreeController 7 个端点)/ IPage(InsightController)/ 纯 List。page/limit(Query 基类,46 类继承)vs page/size(AiCleanController)vs pageSize(agent 工具层)。Result<PageVO<T>>{records,total,page,size} + 删除一律 POST /xxx/delete(body 传 id(s))+ 入参统一 Query 基类;新代码立即执行,旧代码随版本化迁移。AgentChatController.java:63 返回裸 ChatSessionVO、多处返回 void;InsightController.java:94 直接返回 MP IPage;其余 185/216 端点返回 Result<T>。前端 defHttp 维护两套 transform(GlobalExceptionHandler 注释自己承认 /chat/** 走 isTransformResponse:false 特殊路径容易漏处理 401)。CallRecordController Result<Page<CallRecord>>、TransRecordController、GovernTreeController Result<List<GovernTree>>、DmController Result<List<GovernConf>>、AiCleanController 返回 AiCleanJob 实体。表加字段即破坏前端契约,内部字段无遮蔽。UserController.java:77 dto == null ? null : dto.getId() 手写兜底);SpecialDateService.java:37/70 注释自我承认"来自请求体(无 @Valid 校验),缺参时不能直接 == 1L(自动拆箱 NPE)"——用防御性补偿代替校验。LlmService.java:180 把上游 401/429 原始报文透传终端用户(timeout 已修成 504,说明只修了一半);好的对照:401 文案按 sa-token 类型细分、"请先打开案件后再执行数据治理"可行动。code 字段并与 Result.code 同枚举(见 O-12);LlmService 按上游状态码映射为面向用户的文案("模型服务认证失败,请检查模型配置")。/sdc、/pbi、/pg、/gt、/tpl、/qry、/cr);大小写混用;部分映射缺前导斜杠、注解内多余空格。/模块/资源/动作 kebab 或 camel 二选一,禁新缩写;15 个 stat 控制器是同一模板,可抽公共基类一次收敛。application.yaml:10 写死 active: dev;application-dev.yaml 内 postgres/postgres、neo4j/neo4j123、注释掉的 ES 密码全部在 git 里。active: ${SPRING_PROFILES_ACTIVE:dev} + 新增 application-prod.yaml(凭证走环境变量);已泄露的密码轮换。application-dev.yaml:54 注释写 Redis 是 .109,实际配的 .103;QUICK_START.md 同错且引用不存在的 ai-frontend/ 目录;server.port/context-path/storage.local.base-url 三处必须手工同步改(注释自己警告了)。GraphServicecopy.java:613 行整文件块注释的死代码(文件名就叫 copy);WebConfig.java:28-38 注释掉的 db1 bean(含个人本机路径);App.java 的 init() 死代码。TransInOutDataCleaner2~6、CallDataCleaner1、TransStdDataCleaner1、TransCoreHisBalanceCleaner2 等——序号即复制代际;15 个 stat* Controller 同模板复制 15 份;三胞胎 KeyController 逐行同构;GovernTreeController 7 个 getTreeXxxPage 仅表名不同。govern/serivce(已知)、SseService.see()(sse→see,连锁传播到调用点与注释)、TrackMeetSummeryDTO、/saveGovernCon、getMaplocal、SUCCEED_CODE。DmService(967 行且无接口的单体 @Service)、TransAnalysisTool(942)、AgentChatServiceImpl(874);800 边缘还有 5 个。sql/ 下 20+ 个无版本脚本(aasd.sql、case_table_1.sql、testSql/交易共同.sql…),无 Flyway/Liquibase,QUICK_START.md:102 承认手工执行且混有 Solon 时代旧表。多人环境无法可重复建库。readme.md 是个人便签(许可证工具 + pmtiles 命令);ai-server/CODE_WIKI.md(849 行)是 Solon 时代遗留且未删(QUICK_START 里加了 ⚠ 但没删文件);QUICK_START 本身质量不错。common/config/BigDecimalSerializer.java:24-26(null 序列化为 "0.00")、:31(经 DataUtil.convertAmount 输出 "1,234.00" 带千分位)。null(前端空态展示);千分位格式化移到前端展示层(后端输出纯数字字符串或数值)。这是接口行为变更,需与前端同步排期。第一批:止血(1-2 天,全部是局部小改)
第二批:稳定性加固(3-5 天)
第三批:可观测性与运维(2-3 天)
第四批:契约治理(渐进,随版本)