EvoCode 架构设计(详细版)¶
4+1 视图 · 类级分解 · 插件 SPI · 线程模型 · 时序 · 演进规划 版本:v1.0 · 2026-08-10
使用说明(灵活性声明):本文档是架构蓝图与基线,服务于"能跑、能演示、能讲清"。类名、包名、SPI 定义为建议形态,实现时可增删调整;凡偏离关键依赖方向(§3.1)与安全底线(§9)之外的选择,记录 AD 后即可执行,事后回填本文档。本设计的核心承诺:三端可替换性(LLM/Sonar/队列)、新增语言与新增分析器是配置级工作而非改造级工作。
目录¶
- 架构目标与设计原则
- 总体架构(4+1 视图总览)
- 逻辑视图(分层与模块分解)
- 进程视图(进程/线程/异步模型)
- 物理视图(部署拓扑)
- 数据架构(ER 关系与数据流)
- 关键场景时序
- 扩展点与 SPI 清单
- 质量属性与架构对策
- 架构治理(防腐与保障)
- 架构演进规划(A0 → A3)
- 架构决策索引与待决问题
1. 架构目标与设计原则¶
1.1 架构目标(优先级从高到低)¶
| 目标 | 衡量方式 |
|---|---|
| G1 可演示 | 一键启动;离线/无 Key 全功能可走通(降级设计) |
| G2 可替换 | LLM / Sonar / 任务队列 / 存储均为可替换 SPI |
| G3 可扩展 | 新增语言 ≤1 天(配置级);新增分析器 ≤2 天(插件级) |
| G4 可解释 | 分析结论可溯源(引用文件/行号);健康分可复现 |
| G5 可维护 | 依赖方向单向、分层清晰、单人也能长期维护 |
1.2 设计原则¶
- 单向依赖:上层依赖下层,禁止反向;同层依赖走接口
- 契约先行:analyzer
schemas.py是分析结果唯一事实来源 - 降级优先:每个对外能力定义"不可用时的降级行为",先降级后报错
- 状态中心化:任务/技术债等状态机只在一个地方流转
- 静态只读:被分析代码绝不执行,全部走只读解析
- 渐进架构:A0 满足 v0.1,A1/A2 能力通过插件位预埋,不提前实现
2. 总体架构(4+1 视图总览)¶
| 视图 | 回答的问题 | 对应章节 |
|---|---|---|
| 逻辑视图 | 系统由哪些模块/类组成,依赖方向 | §3 |
| 进程视图 | 运行时有哪些进程/线程,如何协作 | §4 |
| 物理视图 | 部署在哪,如何连接 | §5 |
| 数据视图 | 数据如何组织与流转 | §6 |
| 场景视图 | 关键用例如何穿过各层 | §7 |
┌────────────────────────────────────────────────────────────┐
│ 展示层 Vue3 前端(SPA) │
│ 页面 → 组件 → store → api(axios) → /api/v1 │
└───────────────┬────────────────────────────────────────────┘
│ HTTP/JSON(REST + SSE)
┌───────────────▼────────────────────────────────────────────┐
│ 应用层 Spring Boot 3 后端(业务编排 + 持久化 + 任务调度) │
│ controller → service → mapper → PostgreSQL │
│ service → AnalyzerClient → analyzer(内部 HTTP;AI 能力在 analyzer 内,backend 永不直连 LLM)│
└───────────────┬────────────────────────────────────────────┘
│ /analyze/v1(仅 127.0.0.1)
┌───────────────▼────────────────────────────────────────────┐
│ 分析层 Python Analyzer(无状态,被调用执行) │
│ 管线编排 → 扫描器 → 解析器(插件) → 分析器 → AI(LLM/RAG) │
└───────────────┬────────────────────────────────────────────┘
│
┌───────────────▼────────────────────────────────────────────┐
│ 数据层 PostgreSQL(pgvector) · Redis(预留) · 磁盘代码库 │
│ 外部:LLM API(OpenAI 兼容) · GitHub · SonarQube(自部署) │
└────────────────────────────────────────────────────────────┘
三个进程的职责边界(铁律):
| 进程 | 拥有 | 不拥有 |
|---|---|---|
| backend | 业务状态、任务编排、鉴权(预留)、对外 API、会话持久化 | 任何代码解析、任何 LLM 直接调用(LLM 出口唯一在 analyzer,见 §3.5) |
| analyzer | 代码解析、质量扫描、AI 生成、RAG | 业务状态、页面数据 |
| frontend | 交互、展示、流式渲染 | 业务逻辑 |
边界理由:解析生态在 Python、业务生态在 Java;两者只通过契约 JSON 通信,互不侵入。
3. 逻辑视图(分层与模块分解)¶
3.1 分层与依赖规则(防腐基线)¶
frontend ── HTTP ──▶ controller ──▶ service ──▶ mapper ──▶ PostgreSQL
│ │
│ └──▶ AnalyzerClient ──▶ analyzer(HTTP) ──▶ AI(LLM/RAG)
▼
dto / common(共享形态)
禁止项(ArchUnit 强制,见 §10):
| # | 禁止 | 原因 |
|---|---|---|
| D-1 | controller 依赖 mapper / entity 直接返回 | 破坏分层,越层访问 |
| D-2 | service 依赖 controller | 反向依赖,循环风险 |
| D-3 | service 之间直接 new 对方实现 | 必须走接口(构造器注入) |
| D-4 | 任何 Java 类依赖 analyzer 实现细节 | 只允许通过 AnalyzerClient + 契约 DTO |
| D-5 | backend 业务代码直接调用任何 LLM 客户端 | LLM 出口唯一在 analyzer(P0-1A);backend 只经 AnalyzerClient(含 /chat 透传) |
| D-6 | 事务内调用 LLM / 外部 IO(长事务) | 锁占用、拖垮连接池 |
3.2 backend 包结构与类清单(类级分解)¶
com.evocode
├── EvocodeApplication.java
├── common/
│ ├── Result.java # 统一响应 {code,message,data}
│ ├── PageResult.java # 分页包装
│ ├── ErrorCode.java # 错误码枚举(1xxx/2xxx/3xxx/5xxx)
│ ├── BusinessException.java
│ ├── GlobalExceptionHandler.java # 全局兜底(校验/业务/系统)
│ └── util/ FileUtil, JsonUtil, PathSafetyUtil, TimeUtil
├── config/
│ ├── AsyncConfig.java # analysisExecutor 线程池定义
│ ├── ChatExecutorConfig.java # SSE 流式线程池(P6)
│ ├── MybatisPlusConfig.java # 分页插件/逻辑删除/自动填充
│ ├── CorsConfig.java
│ └── EvocodeProperties.java # @ConfigurationProperties 集中配置(含 analyzer-url;LLM 配置归 analyzer)
├── enums/ ProjectSourceType, ProjectStatus, AnalysisStatus,
│ AnalysisType, RiskLevel, IssueStatus, TechDebtSource, DocType,
│ ChatRole, AiStatus, HotspotLevel, ReportSource, NodeType, EdgeRelation
├── entity/ Project, Analysis, FileNode, Dependency, QualityIssue,
│ ArchitectureNode, ArchitectureEdge, ArchViolation, TechDebt,
│ CommitStat, FileChangeStat, ChatSession, ChatMessage, GeneratedDoc,
│ KnowledgeChunk
├── mapper/ ProjectMapper, AnalysisMapper, FileNodeMapper, …(每实体一个)
├── dto/
│ ├── project/ ProjectCreateReq, ProjectResp, ProjectDetailResp, ProjectSummaryResp
│ ├── analysis/ AnalyzeReq, AnalysisResp, AnalysisStatusResp
│ ├── report/ ReportResp, ReportDimension, ReportRisk, ReportRecommendation
│ ├── scan/ ScanResultResp, ScanFileResp
│ ├── quality/ QualityMetricsResp, QualityIssueResp, IssueExplainReq
│ ├── arch/ ArchGraphResp, ArchNodeResp, ArchEdgeResp, ViolationResp
│ ├── evolution/ EvolutionResp, TrendPoint, FileChangeResp, HotspotResp
│ ├── debt/ TechDebtReq, TechDebtResp
│ ├── chat/ ChatSessionResp, ChatMessageResp, ChatAskReq, ChatDelta
│ └── doc/ DocResp, DocEditReq
├── service/
│ ├── project/
│ │ ├── ProjectService/Impl # 项目 CRUD + 删除级联编排
│ │ ├── UploadService/Impl # zip 校验/解压/临时目录/原子移入
│ │ └── GitCloneService/Impl # clone --depth1 / 全量 / 代理
│ ├── analysis/
│ │ ├── AnalysisService/Impl # 状态机流转(唯一入口)
│ │ ├── AnalysisAsyncRunner # @Async 执行体,分阶段调 analyzer
│ │ └── AnalyzerClient # RestClient 封装,错误映射 3xxx
│ ├── scan/ FileNodeService # 快照落库 + 前后对比
│ ├── report/ ReportService # 报告组装/读取/重新生成
│ ├── quality/ QualityService # 指标 + issue 分页 + 解释任务入队
│ ├── architecture/ ArchService # 图数据 + 违规查询
│ ├── evolution/ EvolutionService
│ ├── chat/
│ │ ├── ChatService/Impl # 会话持久化 + 调 analyzer /analyze/v1/chat 并透传 SSE
│ │ └── ChatSseForwarder # 流式转发(不缓冲;断流 → error 事件 → 落库)
│ ├── debt/ TechDebtService # 自动生成/状态机/复发复查
│ └── doc/ DocService # 三类文档 + 编辑版本
└── controller/
├── ProjectController / AnalysisController / ReportController
├── FileController / QualityController / ArchitectureController
├── EvolutionController / TechDebtController
├── ChatController(SSE)/ DocController / HealthController
关键服务职责细化:
| 服务 | 核心职责 | 关键约束 |
|---|---|---|
| AnalysisService | 发起任务(并发校验)、状态机流转、进度上报 | 唯一允许修改 analysis 状态的地方 |
| AnalysisAsyncRunner | 编排分析阶段:SCAN→REPORT;超时/降级处理 | 无状态,只靠 analysisId 驱动 |
| UploadService | 校验→解压到临时目录→安全校验→原子移入 | 失败即清理临时目录 |
| ReportService | 调 analyzer /analyze/report(内部含 LLM→规则版降级)→ 校验 JSON → 落库 |
不直连 LLM(LLM 出口在 analyzer) |
| TechDebtService | 从各分析结果生成/去重 issue;复查复发 | 去重键 (project,source,rule) |
3.3 analyzer 模块与插件架构¶
analyzer/app/
├── main.py # FastAPI 实例;路由注册;全局异常
├── config.py # Settings(pydantic-settings)
├── schemas.py # 契约(唯一事实来源):
│ ScanRequest/Result, QualityRequest/Result, ArchRequest/Result,
│ EvolutionRequest/Result, ReportRequest/Result, RagRequest/Result,
│ ChatRequest/ChatDelta, ExplainRequest/Result
├── api/routes/
│ scan.py quality.py architecture.py evolution.py report.py rag.py
│ chat.py explain.py health.py
├── core/ # 无状态核心(不 import FastAPI)
│ ├── pipeline.py # PipelineRegistry:阶段注册 + 权重 + 超时 + 进度
│ ├── filescanner.py # 目录遍历(os.walk 剪枝)
│ ├── langdetect.py # 后缀表 + 文件名 + shebang
│ ├── stackdetect.py # 清单文件解析 → 框架标签
│ ├── loc.py # LOC 统计(流式读,跳过二进制)
│ ├── ignore.py # 忽略规则引擎(默认 + .evocodeignore)
│ └── limits.py # 文件数/大小/总超时
├── parsers/ # 插件式语言解析器(SPI-1)
│ ├── base.py # BaseParser 抽象 + ParsedUnit(Symbol/Relation)
│ ├── registry.py # ParserRegistry:language → parser
│ ├── java_parser.py python_parser.py ts_parser.py
│ ├── go_parser.py vue_parser.py
├── analyzers/ # 分析器(面向结果产出,SPI-2)
│ ├── dependency_analyzer.py # 依赖清单解析 + eol 规则引擎(rules/eol.json)
│ ├── quality_analyzer.py # sonar-scanner 调用 + 指标归一化 + 超时
│ ├── architecture_analyzer.py # 节点/边提取 + 分层违规/循环/上帝类
│ └── evolution_analyzer.py # git log --numstat 统计 + 热点规则
├── ai/
│ ├── llm_client.py # OpenAI 兼容客户端(超时/重试/JSON 校验)
│ ├── embed_client.py # embedding 客户端(bge-m3 等)
│ ├── prompts.py # 全部 prompt 模板(版本化)
│ ├── report_generator.py # 报告生成(LLM → 规则版降级链)
│ ├── explainer.py # issue 解释(信号量限流,并发 ≤4)
│ └── rag.py # 切片(chunk) / 索引 / 混合检索
└── utils/
├── path.py # 安全路径(根内校验 + 只读)
└── process.py # git 子进程安全封装(参数白名单)
管线 SPI(PipelineRegistry):
@dataclass
class Step:
name: str # 如 FILES / LANG / STACK / LOC
handler: Callable[[StepContext], StepResult]
class PipelineRegistry:
"""端点内子步骤组合器(跨阶段编排在后端 AnalysisAsyncRunner):
一个分析端点 = 若干 Step 顺序执行;同步完成、无状态、可多 worker"""
_pipelines: dict[str, list[Step]]
def register(self, endpoint: str, step: Step): ...
def run(self, endpoint: str, ctx: StepContext) -> PipelineResult:
for step in self._pipelines[endpoint]:
ctx.check_timeout() # 单端点总超时
ctx.remember(step.name, step.handler(ctx))
return PipelineResult(ctx)
端点内组合示例:
/analyze/v1/scan= FILES → LANG → STACK → LOC;/analyze/v1/quality= SONAR → NORMALIZE。 新分析器接入 = 新增 analyzer 模块(新端点 + 端点内注册 Step)+ 契约扩展 + 后端AnalysisAsyncRunner增加一个阶段与进度权重(后端仅此一处小改)。整体 ≤2 天(A1 目标)。
解析器 SPI(BaseParser):
class BaseParser(ABC):
language: str # "java"
@abstractmethod
def parse(self, source: str) -> ParsedUnit:
"""输出 symbols(类/函数/方法) + relations(调用/导入)"""
class ParserRegistry:
parsers: dict[str, BaseParser]
@classmethod
def register(cls, parser: BaseParser): ...
@classmethod
def get(cls, language: str) -> BaseParser | None: ...
新增语言 = 实现 parse + 注册 + 单测 + 后缀表加一行。A0 只实现 java;A1 补 python/ts/go/vue。
3.4 frontend 模块结构¶
路由表(懒加载):
| 路径 | 页面 | 说明 |
|---|---|---|
/ |
redirect → /projects |
|
/projects |
project-list | 列表 + 搜索 + 筛选 |
/projects/new |
project-create | 上传/克隆(或弹窗) |
/projects/:id/overview |
project-detail/overview | 档案 + 最近分析 |
/projects/:id/report/:analysisId |
project-detail/report | 体检报告 |
/projects/:id/architecture |
project-detail/architecture | 架构图 |
/projects/:id/quality |
project-detail/quality | 质量 |
/projects/:id/evolution |
project-detail/evolution | 演化 |
/projects/:id/tech-debt |
project-detail/debt | 技术债 |
/projects/:id/doctor |
project-detail/doctor | AI 医生(SSE) |
/projects/:id/docs |
project-detail/docs | 文档 |
/dashboard |
dashboard | 跨项目总览(P7) |
组件树与数据流:
stores/project.ts(当前项目 + 档案) stores/analysis.ts(轮询任务状态)
│ │
▼ ▼
api/ project.ts analysis.ts report.ts quality.ts architecture.ts
evolution.ts debt.ts chat.ts doc.ts ← 每资源一个文件,返回 types/api.ts 类型
│
composables/ useProject.ts useAnalysisPolling.ts useChatStream.ts(SSE 解析)
│
components/ base/(Empty/Loading/Tag/Modal/Pagination)
charts/(ChartContainer + RelationChart/RadarChart/TrendLine/Donut)
feature/(UploadPanel/ReportCard/RiskItem/IssueDrawer/ChatMessage/CitationCard/FilePreview)
│
views/ project-list/ project-create/ project-detail/{overview,report,architecture,
quality,evolution,debt,doctor,docs}.vue dashboard/
图表抽象:所有图表复用 ChartContainer.vue(ECharts 初始化/resize/主题/空态),业务只传 options 工厂函数——保证图表风格统一与按需加载。
SSE 流式:useChatStream 用 fetch + ReadableStream 逐块解析 data: 行;delta 追加渲染,citations 到齐后渲染引用卡片。
3.5 AI 层细化(LLM 出口唯一在 analyzer)¶
┌─────────────────────────────────────────────┐
│ AI 出口唯一在 analyzer(backend 永不直连 LLM)│
│ /analyze/v1/report 报告(LLM → 规则版降级) │
│ /analyze/v1/explain issue 解释(P3,限流 4)│
│ /analyze/v1/chat AI 医生 SSE(P6) │
├─────────────────────────────────────────────┤
analyzer: llm_client.py(OpenAI 兼容:DeepSeek/OpenAI/Ollama)
prompts.py(全部 prompt,带 version 元数据)
配置:baseUrl / apiKey / model / timeout / retries / temperature
└─────────────────────────────────────────────┘
降级链(每个 AI 能力都定义,在 analyzer 内实现):
LLM 成功 → 使用结果
LLM 失败/无 Key → 规则版(rules 模板)→ 结果带 source=LLM|RULES
RAG 检索失败 → 纯摘要问答(无知识块,prompt 说明)
RAG 管线(P6,检索在 analyzer,会话在 backend):
代码 → 切片(函数/类 ≤800token) → embed → pgvector(HNSW cosine)
提问(经 backend 转发) → 检索 topK → 注入 prompt → SSE 流式 → backend 透传前端
backend 侧 ChatService 职责(仅编排,不调 LLM):会话 CRUD → 组装消息 → 调 /analyze/v1/chat → 透传 SSE 事件(delta/citations/done/error)→ 结束落库 + 引用校验(引用路径必须存在于本次知识块集合)。
Prompt 管理:全部在 analyzer/app/ai/prompts.py,每版 prompt 带 version 字段写入结果元数据(promptVersion),便于问题复现与答辩讲解。
4. 进程视图(进程/线程/异步模型)¶
4.1 进程清单¶
| 进程 | 数量 | 说明 |
|---|---|---|
| backend | 1(JVM) | Tomcat 8080;内部多线程 |
| analyzer | 1(可演进为多 worker) | uvicorn 8091,仅内网 |
| frontend | dev: vite / prod: nginx 静态 | — |
| postgres/redis/sonar | 容器 | 127.0.0.1 |
4.2 backend 线程模型¶
| 线程池 | 配置 | 用途 | 说明 |
|---|---|---|---|
| Tomcat HTTP | 默认 200 | REST | 阻塞短任务(DB 查询) |
| analysisExecutor | core2/max4/queue8 | 分析任务执行 | @Async;队列满 → Abort 并标记任务 FAILED("队列已满"),不丢业务数据 |
| SSE 转发(P6) | Servlet 异步,不占线程 | 透传 analyzer 事件流 | 断流时向客户端发 error 事件并落库 |
backend 无 LLM 线程:所有 AI 调用发生在 analyzer(llm_pool,见 §4.3),backend 仅透传。
决策:分析任务不用虚拟线程(JDK17 无),若升级 JDK21 可评估替换 chatExecutor(待决 Q-1)。
4.3 analyzer 线程/GIL 策略¶
- 双执行池(避免 CPU 解析被 LLM IO 等待饿死):
parser_pool:CPU×2 线程,跑 tree-sitter 解析/扫描(CPU 密集)llm_pool:IO 型(httpx 异步或独立池 8 线程),跑 LLM 调用/embedding- 路由:解析类端点(scan/quality/arch/evolution)入 parser_pool;生成类端点(report/explain/chat)内部 LLM 重 → llm_pool
- GIL 注意:tree-sitter 解析是 CPU 密集,受 GIL 限制 → 并发解析受 parser_pool 上限约束;若 P4 压测吞吐不足,方案 B 为
uvicorn --workers N(多进程,注意 DB 连接池与内存翻倍)(待决 Q-2) - 所有分析端点无状态:请求内完成,不保存服务端会话 → 天然支持多 worker
4.4 内存与资源策略¶
- 上传/解压:流式;单文件 >2MB 跳过;总解压体积上限 500MB
- 解析结果:分页写库(每 500 条 flush),不全量驻留内存
- 报告 JSON:<1MB 预期;超限时截断 risks 列表(保留 TOP 50)
5. 物理视图(部署拓扑)¶
5.1 开发环境(单人)¶
[Windows 本机]
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ frontend │ │ backend │ │ analyzer │
│ vite :5173 │──▶│ :8080 │──▶│ :8091(仅本机) │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
┌─────────▼──────────────────▼─────────┐
│ Docker Desktop(仅本机端口) │
│ postgres:5432 · redis:6379 · sonar:9000│
└───────────────────────────────────────┘
外网出口(仅 backend/analyzer 进程)→ LLM API · GitHub
5.2 演示/答辩环境(离线要点)¶
- 全套本机;LLM 用 Ollama 本地模型(
EMBEDDING_BASE_URL=http://127.0.0.1:11434) - 断网兜底链:LLM→规则版;GitHub→zip 上传;Sonar→N/A;主演示路径(上传→扫描→报告)零外网依赖
- 演示前跑
scripts/security-smoke+ AC-1/3/5/6/7/8/9 回归
6. 数据架构(ER 关系与数据流)¶
6.1 ER 关系(核心)¶
project 1 ── N analysis(每次分析一批结果)
analysis 1 ── N file_node (扫描快照)
analysis 1 ── N dependency (依赖风险)
analysis 1 ── N quality_issue (质量 issue + AI 解释)
analysis 1 ── N architecture_node (架构节点)
architecture_node 1 ── N architecture_edge(source/target 两个 FK)
analysis 1 ── N arch_violation
project 1 ── N commit_stat (演化,跨分析累计)
project 1 ── N file_change_stat (热点文件,唯一约束 project+path)
project 1 ── N tech_debt (跨分析,闭环状态)
project 1 ── N generated_doc
project 1 ── N chat_session 1 ── N chat_message
project 1 ── N knowledge_chunk (向量,随分析重建)
设计要点:分析结果都挂
analysis_id(可对比两次分析);演化与技术债挂project_id(跨分析累积)。删除项目 = 按 6.2 顺序级联。
6.2 数据流¶
① 创建:zip/GitHub ──▶ data/projects/{id}/(磁盘,DB 只存元数据)
② 分析:磁盘 ──▶ analyzer.scan ──▶ file_node + project 档案更新
③ 报告:扫描摘要 ──▶ LLM(或规则版)──▶ analysis.report_json
④ 质量(P3):磁盘 ──▶ sonar-scanner ──▶ quality_issue
⑤ 架构(P4):磁盘 ──▶ parsers ──▶ nodes/edges/violations
⑥ 演化(P5):磁盘 git 目录 ──▶ git log ──▶ commit_stat / file_change_stat
⑦ RAG(P6):代码 ──▶ 切片 ──▶ embed ──▶ knowledge_chunk(向量)
⑧ 技术债(P7):各分析结果 ──▶ tech_debt(去重/闭环)
6.3 存储策略¶
| 数据 | 存储 | 说明 |
|---|---|---|
| 业务数据 | PostgreSQL | DDL 见《开发指导》§5(V001-V006) |
| 代码文件 | 磁盘 data/projects/{id} |
不入库;删除项目时清理 |
| 向量 | pgvector(HNSW cosine) | 维度随 embedding 模型(bge-m3=1024) |
| 配置/规则 | 配置文件 + rules/eol.json |
可编辑可热加载(读时加载) |
| 日志 | 磁盘 logs/ 按天滚动 14 天 |
任务级前缀 [analysisId=x] |
7. 关键场景时序¶
7.1 创建项目 + 分析(核心闭环,v0.1)¶
前端 backend analyzer LLM
│ POST /projects │ │ │
│ (multipart zip) │ UploadService: │ │
│────────────────▶│ 校验→临时目录→安全校验 │ │
│ │ →原子移入 data/…/{id} │ │
│ │ 建档(快扫可异步) │ │
│◀──── 201 ──────▶│ │ │
│ POST /analyses │ AnalysisService: │ │
│────────────────▶│ FOR UPDATE 并发校验 │ │
│ │ 建任务 PENDING → @Async │ │
│ │ AnalysisAsyncRunner: │ │
│ │ SCAN 阶段 │ POST /scan │
│ │────────────────────────▶│────────────────▶│
│ │ │ 扫描/语言/技术栈 │
│ │◀──────── 结果 ─────────│◀────────────────│
│ │ 落库 file_node+档案 │ │
│ │ REPORT 阶段 │ POST /report │
│ │────────────────────────▶│───────────────▶│
│ │ │ summary+prompt │
│ │◀─── 报告(source=LLM) ──│◀───────────────│
│ │ 写 report_json, SUCCEEDED│ │
│ 轮询 2s ◀───────│ │ │
│ GET /report ◀───│ │ │
7.2 质量 issue 解释(P3,异步不阻塞)¶
前端 backend analyzer LLM
│ GET /quality-issues │ │ │
│────────────────────▶│ 分页返回(ai_status=PENDING)│ │
│ 点"解释" │ QualityService 入队 │ │
│────────────────────▶│ → 调 analyzer /explain │───────────────▶│
│ │ (信号量限流 4) │◀────解释──────│
│ │ 写回 ai_explanation │ │
│ 轮询/刷新 ◀─────────│ │ │
7.3 AI 医生对话(P6,SSE;LLM 出口在 analyzer)¶
前端 backend(编排+透传) analyzer(检索+生成)
│ POST /chats/{id}/messages │ │
│────────────────────────────▶│ ChatService: │
│ │ 1.会话持久化+组装历史(截断) │
│ │ 2.POST /analyze/v1/chat(SSE)│
│ │───────────────────────────▶│
│ │ │ RAG检索:向量top8+关键词top4
│ │ │ LLM.stream 生成事件流
│ SSE delta ◀─────────────────│ ◀────────── SSE 事件 ──────│
│ SSE citations ◀─────────────│ 透传(不缓冲) │
│ SSE done ◀──────────────────│ 引用校验在后端(存在性) │
│ │ analyzer 断流 → 发 error 事件│
│ │ → 落库(含 citations) │
7.4 降级链全景(演示保障)¶
LLM 不可用 → 报告=规则版(Mock) + source=RULES + 前端横幅
Sonar 未起 → 质量维度=N/A,其余正常
GitHub 不可达 → 提示转 zip 上传
分析中断(服务重启) → 任务标 FAILED("服务重启中断"),可重新发起
7.5 删除项目与分析中的竞态(P1+)¶
用户 backend
│ DELETE /projects/{id}
│───────────────────▶│ ProjectService:
│ │ ① 存在 RUNNING 任务 → 置 CANCELLED
│ │ ② 等待执行线程退出(轮询 ≤60s)
│ │ ③ 事务内按级联顺序清库(见 §6.1)
│ │ ④ 删磁盘目录(失败仅记日志,不阻塞)
│◀──── 200 ──────────│
8. 扩展点与 SPI 清单¶
| 扩展点 | 位置 | 接口/机制 | 接入成本 | 阶段 |
|---|---|---|---|---|
| SPI-1 新语言解析器 | analyzer/app/core/arch | BaseParser + ParserRegistry.register(2026-08-13 落地 registry.py,5 语言注册 + languages 过滤) |
≤1 天(配置级) | A1 达成 |
| SPI-2 新分析器 | analyzer/analyzers + PipelineRegistry | 新端点 + 端点内 Step 注册 + 后端 AnalysisAsyncRunner 加阶段 |
≤2 天(插件级) | A1 |
| SPI-3 新 LLM 提供商 | analyzer/ai | llm_client.py(OpenAI 兼容协议即可覆盖 DeepSeek/OpenAI/Ollama) |
0.5 天 | A0 |
| SPI-4 新规则(EOL 等) | rules/eol.json |
配置文件热加载 | 分钟级 | A1 |
| SPI-5 任务队列演进 | backend/analysis | DB 状态机 + 启动恢复(2026-08-13:AD-019 StartupTaskRecovery 标记中断任务 FAILED);Redis 队列维持可选(出现「自动重跑」硬需求时启用) | 1 天(预留) | A2 部分落地 |
| SPI-6 报告存储演进 | backend/service/report | report_json → 拆分表(2026-08-13 落地 analysis_report 表 + ReportStorageService 唯一读写入口,health_score/level/summary 列化) | 1 天(预留) | A2 达成 |
| SPI-7 前端图表 | frontend/charts | ChartContainer + options 工厂 |
分钟级 | A0 |
| SPI-8 权限 | backend | 预留拦截器位(v1.0 后) | — | 远期 |
演进示例(展示架构可扩展性,答辩加分点):A0 阶段 java 解析器已存在,A1 加 Python 支持 = 新增 python_parser.py + 注册 + 后缀表 1 行 + 单测 4 例,共 ~200 行。
9. 质量属性与架构对策¶
| NFR(引用需求 §8) | 架构对策 | 验证方式 |
|---|---|---|
| 性能:2 万行 ≤10min | 忽略剪枝、大文件跳过、异步任务、分页写库 | AC-5 计时 |
| 可用性:LLM 降级 | 降级链(§7.4):LLM 失败/无 Key → analyzer 规则版报告(source=RULES) | AC-6/AC-12 |
| 安全:不执行代码 | 静态只读 + path.py 安全封装 + 解析白名单 | security-smoke 脚本 |
| 安全:恶意 zip | UploadService 三重校验 + 临时目录 + 原子移入 | AC-8 |
| 可扩展:新增语言 ≤1 天 | SPI-1 插件注册表 | 演示加语言 |
| 可替换:LLM/Sonar/队列 | SPI-3/SPI-5,配置切换 | 换 baseUrl 即生效 |
| 可解释:结论可溯源 | 引用字段(file:line)+ prompt 强约束 + source 标记 | 报告抽查 |
| 可维护:防腐化 | ArchUnit(§10)+ 契约测试 + 单向依赖 | CI 三命令 |
10. 架构治理(防腐与保障)¶
10.1 ArchUnit 规则(backend 测试,P1 起步)¶
@AnalyzeClasses(packages = "com.evocode")
public class ArchitectureTest {
@Test
void controller不得依赖mapper与entity() {
classes().that().resideInAPackage("..controller..")
.should().notDependOnClassesThat().resideInAnyPackage("..mapper..", "..entity..")
.check(importedClasses);
}
@Test
void service不得依赖controller() {
classes().that().resideInAPackage("..service..")
.should().notDependOnClassesThat().resideInAPackage("..controller..")
.check(importedClasses);
}
@Test
void backend不得直连LLM() {
// LLM 出口唯一在 analyzer:backend 仅能通过 AnalyzerClient(含 chat 透传)通信
classes().that().resideInAPackage("..service..")
.should().notDependOnClassesThat().resideInAPackage("..ai..")
.andShould().notDependOnClassesThat().haveSimpleName("OpenAiCompatibleClient")
.check(importedClasses);
}
}
10.2 契约测试(analyzer ↔ backend)¶
- 方式:
schemas.py字段清单与后端 DTO 字段名做一致性脚本(Python 侧导出 JSON schema → Java 测试读取比对) - 触发:契约变更 PR 必跑;失败阻止合并
10.3 架构评审清单(每阶段自查)¶
- [ ] ArchUnit 全绿(分层/反向依赖/LLM 隔离)
- [ ] 新分析器走管线注册,未在 controller/service 硬编码阶段
- [ ] 新语言走 ParserRegistry,后缀表已加
- [ ] 所有外部调用(LLM/git/sonar)有超时与降级
- [ ] 无新增"偷偷执行"代码的路径(grep eval/exec/subprocess)
- [ ] 契约字段三端一致(grep 核对)
- [ ] 大 JSON 未整表查询;列表有分页
11. 架构演进规划(A0 → A3)¶
| 架构版本 | 对应产品 | 关键架构变化 | 完成指标 |
|---|---|---|---|
| A0 | v0.1 | 三端骨架 + 独立 analyzer + DB 状态机 + 规则版降级链 + SPI-1/3/7 预埋 | 一键启动;AC-1~12 全过;ArchUnit 引入 |
| A1 | v0.2~v0.3 | 管线插件化(Quality/Arch 阶段注册)+ 解析器扩充(py/ts/go/vue)+ EOL 规则配置化 + Sonar 接入(SPI-4 生效) | 新增分析器 ≤2 天;新增语言 ≤1 天(演示验证) |
| A2 | v0.4~v1.0 | RAG 全链路(切片/向量/混合检索/SSE)+ 技术债闭环 + 报告拆表(SPI-6,2026-08-13 达成)+ 可选 Redis 队列(SPI-5,读缓存方向已落地 TD-05) | AI 医生引用准确率抽查 ≥90%;技术债复发可自动复查 |
| A3 | 远期 | 多用户鉴权(SPI-8)、多节点部署(analyzer 多实例)、增量分析、多仓库 | 视论文篇幅与时间裁剪,不承诺 |
关键路径与裁剪策略:
A0 主线不可裁剪:上传→扫描→报告(12 AC)
A1 若时间不足 → 质量/架构二选一演示(文档结构与报告维度已预埋)
A2 若时间不足 → 技术债闭环(纯后端逻辑,性价比最高)优先于 RAG 深度优化
12. 架构决策索引与待决问题¶
12.1 决策索引(对应《开发指导》AD-1~16)¶
| AD | 决策 | 架构影响 |
|---|---|---|
| AD-1/2 | 单体 + 独立 analyzer | 逻辑视图 §3 边界 |
| AD-3/4 | 自写 LLM 客户端(analyzer)+ pgvector | AI 层 §3.5、数据 §6.3 |
| AD-5 | DB 状态机 + @Async | 进程视图 §4.2、SPI-5 |
| AD-8 | Sonar 社区版 | 降级链 §7.4 |
| AD-9 | 规则版降级 | §7.4、analyzer 降级链 |
| AD-13 | 报告存 report_json | SPI-6 预留 |
| AD-14 | 健康分规则+AI 修正 | 报告契约(可复现) |
| AD-16 | SSE 流式 | §7.3 |
| AD-17 | Redis 宿主端口改绑 6380(本机 polycode 冲突) | §8 SPI-5 |
| AD-18 | Redis 列表读缓存(TD-05:选读缓存非任务队列,TTL+降级) | §8 SPI-5 |
| AD-19 | 分析任务中断恢复(启动标记 FAILED;SPI-5 压测评估,Redis 队列维持可选) | §8 SPI-5、进程视图 §4.2 |
12.2 待决问题(实现时验证,记录进 AD)¶
| # | 问题 | 影响 | 决策时机 |
|---|---|---|---|
| Q-1 | JDK 21 虚拟线程 vs SSE 转发线程模型 | 流式并发上限 | P6 前 |
| Q-2 | analyzer 单进程线程池 vs 多 worker | 解析吞吐/内存 | P4 压测后(已压测:单进程 4 并发 FULL 3 秒内完成、满足常规;多 worker 留待吞吐需求) |
| Q-3 | 报告拆表阈值(>1MB?版本历史需求?) | 数据层演进 | 已解决(SPI-6 V010:healthScore/level/summary 列化 + report_json 独立存储,2026-08-13) |
| Q-4 | "全文发送"开关的存储实现(标记文件白名单) | 隐私合规 | P6 前 |
| Q-5 | Sonar 太重时的内置规则替代方案(超长方法等已内置) | A1 范围 | 已解决(TD-07 Sonar 可选化 profile + 内置规则兜底,2026-08-13) |
| Q-6 | .evocodeignore 语法解析器选择(复用 gitignore 库) | 用户体验 | P1 起步时 |
附:架构一句话总结¶
一个无状态的 Python 解析内核 + 一个有状态的 Java 业务中枢 + 一个只做展示的 Vue 壳,三者通过契约 JSON 连接;一切可替换的(LLM/Sonar/队列/语言/规则)都做成 SPI,一切不可靠的(外网/第三方)都有降级路径,一切结论都可溯源到文件与行号。