Files
group_fqcd_jr/docs/演示用/代码结构与关键逻辑梳理-2026-09-14.md
T
lzf_0626 484cccc145 记录三个演示文档的移动,并把 .workbuddy/ 加进 .gitignore
## 文档移动(docs/ -> docs/演示用/)

`代码结构与关键逻辑梳理-2026-09-14.md`、`后端接口文档-2026-09-14.md`、
`软件需求文档-2026-09-14.md` 移入 `docs/演示用/`。
git 识别为 **rename**(状态 `R`),三份文档的历史完整保留,不是"删掉再新建"。

## .workbuddy/ 加入 .gitignore

它与文件里已有的 `.agents/`、`skills-lock.json` 属同一类 —— AI 编码助手的工具产物,
不是项目内容。放在同一节下,避免以后 `git add -A` 时误提交。

## 未包含(有意留下)

`docs/23-记忆分层与画像设计.md` 的工作区改动**没有**一并提交:那不是我改的,
也不在这次确认的范围内,留在工作区由作者决定何时提交。
(暂存时被 `git add docs` 一并带上,已 `git restore --staged` 退回。)
2026-09-14 12:09:23 +08:00

23 KiB
Raw Blame History

代码结构与关键逻辑梳理(只读分析报告)

分析日期:2026-09-14 分析范围:C:\Users\Windows\Desktop\项目代码 全仓库(app/ + tools/ + docs/ + tests/) 分析方式:纯阅读,未修改任何代码或文档(本报告为新增文件) 分析依据:AGENTS.md 的强制约束、docs/ 核心 7 份文档、全部 Python 业务代码、app/static/portal/ 全部 44 个前端模块


〇、结论摘要(先看这一段)

这是一套**「通用 Agent 平台 + 多业务域」**的金融模拟交易系统,架构为 MVC+S,核心是一个被治理骨架严格约束的 Agent 运行时。三条主线:

主线 入口 关键机制
请求链路 HTTP 受理 202 → Outbox 事件 → Worker 领单 → Agent 执行 → 落结果 三段式异步、租约围栏、幂等回执
记忆链路 对话 → 记忆单元 → 事实晋升 → 画像快照 → 向量/图投影 MySQL 唯一真相、投影单向可重建
治理链路 Agent 输出 → 引用校验 → 负面词拦截 → 脱敏 → 免责声明 同步、无 DB、失败关闭

本次发现的最重要问题(1 个 P0 级):

🔴 app/static/portal/employee-console/workspace/workspace.js 存在真实语法错误,整个管理员工作台无法加载。已用真实 ESM 解析器确认(44 个前端模块中唯一失败项)。详见 §六.1。


一、整体结构

1.1 目录分层(MVC+S 的物理映射)

app/
├── main.py                 # FastAPI 组装入口(模块级变量是 app,不是 application)
├── core/                   # 契约与横切关注点:contracts / errors / config / knowledge_schema
├── api/                    # ── Controller 层 ──
│   ├── controllers/        # 22 个路由模块(按业务域切分)
│   ├── dependencies/       # auth / database / rate_limit 等注入
│   ├── middleware.py       # trace_id 注入
│   └── views/envelope.py   # 统一信封(envelope / list_envelope)
├── service/                # ── Service 层(含全部业务 Agent)──
│   ├── agent/              # BaseAgent / AgentFactory / AgentExecutor / governance
│   │   ├── implementations/# 7 个业务 Agent
│   │   └── tools/          # 工具注册表与 handler
│   ├── *_service.py        # 43 个业务 Service
│   └── ...
├── repository/             # ── Model 层(数据访问)── 15 个 Repository
├── model/                  # ORM 定义
├── infrastructure/         # 外部依赖适配器(Milvus / Neo4j / MySQL / Redis / 行情源)
├── worker/                 # 异步执行进程(与 API 进程分离)
└── static/portal/          # ── View 层(四套前端页面)──

1.2 关键架构决策(代码中已固化的)

决策 体现位置 为什么
Agent 属于 Service 层,必须有 BaseAgent 骨架 app/service/agent/base.py AGENTS.md 规则 7;__init_subclass__ 禁止子类覆盖执行顺序
API 与 Worker 是两个独立进程 app/main.py / app/worker/__main__.py 受理与执行解耦,可独立扩缩
MySQL 是唯一真相,向量/图是单向投影 memory_sync_outbox 表 投影可随时重建;投影挂掉不影响主流程
治理审查同步、不落库 governance.review_output 治理必须在返回用户前完成,且不能因 DB 故障而跳过

二、主要模块与职责

2.1 Agent 底座(最核心)

文件 职责 关键约束
agent/base.py BaseAgent 执行骨架 固定顺序:validate_input → validate_access → resolve_config → recall_memory → classify_intent → _execute_governed → governance.review;子类只能实现 _execute_governed
agent/factory.py AgentFactory 按 agent_type 创建 每个请求创建一个 Agent 实例,注入治理/模型/工具/意图分类器
agent/executor.py 薄封装,迭代 agent.execute() 不承载业务逻辑
agent/governance.py PlatformGovernance.review_output 四步:引用校验 → 负面词/硬规则拦截 → 脱敏 → 追加强制免责声明
agent/authorizer.py 工具/角色鉴权 工具可用范围 = 代码上限 ∩ active config_release 白名单,缺配置则失败关闭
agent/bootstrap.py 唯一组装入口 @lru_cache(maxsize=1);HTTP 与 Worker 共用;7 个 Agent + 全部工具在此注册

7 个已注册 Agent:FundQueryDemoAgent、CustomerServiceAgent、RiskAgent、PlatformProbeAgent、AdvisorAgent、OffsiteFundAgent、PromotionMaterialAgent。

2.2 异步执行与可靠性

机制 实现 要点
Outbox 模式 DomainEventOutbox / MemorySyncOutbox 持久化 SQL 队列,with_for_update(skip_locked=True);retry_count >= 5 → 死信
Worker 租约围栏 agent_run_repository.claim() 每次领单生成新 worker_id = uuid4(),同时围住进程重启;locked_until + 心跳续约
幂等 RequestIdempotency + api_request_receipt 按 (user_id, agent_type, key) 查找;取消路径写 status="failed", error_code="RUN_CANCELLED"
统一错误信封 main.py 两个异常处理器 AgentError → {error:{code,message,retryable,field_errors}, meta:{trace_id}};RequestValidationError → 422 AGENT_INPUT_INVALID

运行前提(易踩坑):客服/风控对话必须有常驻 Worker(python -m app.worker)。没有 Worker 时 agent_run.status 恒为 queued,前端只显示"响应超时/客服繁忙"——看起来像链路慢,实际是没人处理。

2.3 记忆 / 画像 / 投影(三层)

memory_unit (中期,权威)
    ↓ 晋升:MIN_EVIDENCE = 2  或  HIGH_CONFIDENCE = 0.90
user_facts (长期,已确认)
    ↓ 重建
fin_customer_profile + profile_snapshots (画像版本)
    ↓ 投影(两条 outbox 行共享一个 event_uuid)
Milvus 向量  /  Neo4j 图

关键不变量:

  • profile_snapshots 有唯一键 uk_profile_snapshot_current → 必须先 is_current=0, current_customer_id=NULL,再插入新 current 行。
  • Milvus 读写物理隔离:MilvusKnowledgeClient(读)与 MilvusKnowledgeWriter(写)互不 import,双向故障隔离。
  • 字段名运行时探测,不得硬编码:Milvus 知识读(core/knowledge_schema.py)与写(resolve_schema)都把逻辑名(doc_id/content/visibility)映射到环境物理名。

2.4 业务域

域 关键文件 备注
场内交易 trade_service.py 行情有效期仅 15 分钟(MAX_QUOTE_AGE),超时所有委托 503且无自动刷新
行情源 fund_market_adapter.py push2/push2his 域不可达,实际可用源是腾讯(qt.gtimg.cn);费率只能爬 fundf10.eastmoney.com;fetch_nav_history 的分页参数被服务端忽略(固定 20/页)
风控 risk_scan_service.py 跨进程排他锁 GET_LOCK('jr_risk_scan_schedule', 0) 必须加在入口层,不能放进 scan()
投顾 recommendations.py + investment_goals.py 两类内容寻址键不同:推荐方案用 content_id、方案书用 goal_no
场外/推广 offsite_fund.py / promotion_material.py AGENTS.md 规则 8:不得写入场内交易表
知识库 knowledge_management.py K002/K003/K004 三端点;上传切成 N 块入库

2.5 前端(四套页面)

角色 路径 守卫
访客 guest/ 无(用临时访客令牌,仅 agent:run + knowledge:query)
客户 customer/ requireCustomer / requireCustomerOnly
管理员 employee-console/ requireAdmin
风控 employee-risk/ requireRiskStaff
投顾 employee-advisor/ requireAdvisor
运营 employee-operations/ requireOperator

契约中枢:common/api-client.js 的 ENDPOINTS 冻结表(约 120 条)是端点调用的唯一来源;put()/del() 只表达意图,真实 HTTP 动词仍来自该表。


三、执行流程

3.1 一次 Agent 请求的完整链路

1. 前端 apiClient.post('R001', ...)  →  带 X-Trace-ID、Idempotency-Key
2. auth controller / build_request_context  →  解析身份 + 权限 + data_scope
3. AgentRunApplicationService.accept()
     ├─ 幂等检查(RequestIdempotency / api_request_receipt)
     ├─ 写 agent_run (status='queued')
     └─ 写 DomainEventOutbox 事件 'agent.run_requested'
   ← 立即返回 202 + run_id(不阻塞)
4. 【异步】WorkerRuntime.dispatch_batch()  →  OutboxWorker.publish_one()
5. WorkerRuntime.run_once()  →  选最老的 queued/running/cancel_requested 且租约空闲的 run
6. WorkerRuntime.execute(run_id)
     ├─ 新 worker_id = uuid4()(围栏)
     ├─ claim() 条件 UPDATE
     ├─ _execute_claimed() 任务  +  _heartbeat() 心跳任务
7. AgentExecutor → BaseAgent.execute()  →  七步骨架
     └─ PlatformGovernance.review_output()  →  引用校验/拦截/脱敏/免责
8. AgentPersistenceService.complete_run()  →  写 status='succeeded' + result
9. 前端轮询 R002(300ms 起,每 500ms,共 40 次 ≈ 20s)或 SSE R003

实测端到端耗时(Worker 在跑 + deepseek-flash):访客一问 4.1–4.8 秒。

3.2 记忆 → 画像的完整链路

对话结束
  → memory.extraction_requested 事件
  → MemoryExtractionService 提取候选记忆单元
  → memory_unit 落库(含 MemoryEvidence,idempotency_key 唯一)
  → [满足阈值] ProfileAssemblyService.promote_facts()  →  user_facts
  → profile.rebuild_requested 事件 ★
  → ProfileAssemblyService.rebuild() + ProfileGraphProjectionService.project_customer()
  → fin_customer_profile + profile_snapshots(新版本)
  → memory_sync_outbox 写两行(target_store = 'milvus' / 'neo4j',共享同一 event_uuid)
  → WorkerRuntime.consume_profile_projections()
       ├─ milvus  → MilvusProfileProjection
       └─ neo4j   → ProfileGraphProjectionService(MERGE 幂等)

★ 为什么 profile.rebuild_requested 必须走事件而非内联调用:提取时记忆行还在未提交事务里,内联调用看不到那一行——真实症状是"快照生成了、事实缺失、图里没有边"。

3.3 投影清理(反向链路)

memory.invalidated / memory.deleted 事件
  → ProjectionCleanupService.cleanup()
       ├─ 删 UserFact
       ├─ 触发 rebuild
       ├─ 图 reconcile
       └─ Milvus 按 memory_uuid 删除
  → 无 client 时:warn + InteractionAudit(status="skipped_no_client"),仍标记消费

四、关键逻辑(设计意图)

4.1 为什么"工具可用范围"要两层

工具可用范围 = 代码上限 ∩ 当前 active config_release 的发布白名单,缺发布配置则失败关闭。原因是 config_release 是环境数据、不随代码合并——本机 active 版本 id 与架构师环境不同。所以"白名单已发布"必须带环境限定,换环境要重发。

4.2 访客与登录客户走不同工具

工具 权限码 角色
search_knowledge knowledge:reference:read customer, advisor, operator, admin
query_knowledge(别名,同 handler) knowledge:query visitor, customer

知识类意图要同时发这两条——缺哪一条,对应人群就一问即失败。

4.3 Worker 组装的三条硬规矩

  1. 入口不得再起第二个 MemorySyncOutboxWorker —— PR #7 曾有两个消费者用不同 neo4j handler 消费同一队列,导致"同一事实在图里有两种说法"。
  2. OffsiteMailWorker 必须共享同一进程 —— 曾有文件覆盖事故删掉了这处接线。
  3. consume_profile_projections 的 client 为 None 时拒绝消费 —— 让 Worker 把它判死信,会把"配置缺失"伪装成"投递失败"。

4.4 几个刻意的实现选择

选择 位置 原因
fetch_kline 用同步 httpx + asyncio.to_thread fund_market_adapter.py AsyncClient 访问 push2his 会被断开
Milvus 写路径没有"已存在则跳过" milvus_knowledge_writer.py 旧向量会继续答旧内容
知识检索 _as_row 丢弃无 uuid 的行而不是编一个 milvus_adapter.py 避免产生假引用
_with_memory_sources 只在 key 缺失或 None 时回填 worker/runtime.py 格式错误要保持失败关闭
advance_clarification 在 >= 10 时拒绝 session_repository.py 澄清轮次上限

五、各部分相互关系

                ┌─────────────────────────────────────┐
                │  app/main.py  (create_app)          │
                │  22 routers + 2 异常处理器           │
                └──────────────┬──────────────────────┘
                               │
        ┌──────────────────────┼──────────────────────┐
        ▼                      ▼                      ▼
  api/controllers/      api/dependencies/       api/views/envelope
  (Controller)        (auth/db/rate_limit)  (统一信封)
        │
        ▼
  service/*_service.py ──► service/agent/ ──► infrastructure/
  (Service 业务逻辑)      (BaseAgent 骨架)    (Milvus/Neo4j/MySQL/行情)
        │                      │                      ▲
        ▼                      ▼                      │
  repository/*.py  ◄────── agent_run / outbox ────────┘
  (Model 数据访问)
        │
        ▼
  worker/  ◄── 独立进程,消费 Outbox,驱动记忆/画像/投影

依赖方向严格单向:Controller → Service → Repository → Model;infrastructure 只被 Service 调用;worker 依赖 Service 但不被 Service 依赖(除 bootstrap)。


六、疑问、潜在问题与边界情况

🔴 6.1【P0 · 已确证】管理员工作台存在语法错误,整页无法加载

位置:app/static/portal/employee-console/workspace/workspace.js 第 500–540 行

async function submitRule(event, releaseId) 在第 539 行 finally { submit.disabled = false; } 之后缺少闭合的 },导致后续的 loadDriftReviews(L540)等函数被解析为 submitRule 的函数体内容。

我的核查过程(三层,逐层确证):

检查方式 结果 说明
node --check(逐文件) ✅ 通过 误判——--check 的 ASI 宽容度让它放过
真实 ESM 解析(import()) ❌ SyntaxError: Unexpected end of input 确证
vm.SourceTextModule 扫描全部 44 个模块 ❌ 仅此一个失败 排除其他模块,定位唯一

影响:

  • 管理员工作台 employee-console/workspace/ 整页白屏(模块解析期即失败,requireAdmin() 都不执行)。
  • 连带失效:RBAC 角色查看、配置发布(校验/审核/激活)、模型端点、审计记录、转人工工单、画像候选、投顾复核、知识库管理、画像漂移复核。
  • 因为 workspace.js 是 import(ESM),解析失败不会执行任何一行业务代码,不会降级到任何 fallback。

为什么 CI 没拦住(推断):现有 pytest tests/contract 校验的是 ENDPOINTS 表与页面结构的静态一致性,不做前端模块的真实解析。node --check 若被用作检查手段,则会因 ASI 而漏判。

修复方向(仅建议,未实施):在第 539 行后补一个 }。


🟠 6.2【P1】文本/表格标"未知技术字段"的兜底会掩盖契约漂移

位置:employee-risk/dashboard/dashboard.js 的 FIELD_LABELS / SNAPSHOT_FIELD_LABELS + detailMarkup/snapshotLabel

任何后端新增字段而前端未登记时,界面显示 `${key}(技术字段)` 而不是报错或告警。风险:字段改名或语义变化不会引起任何注意,风控人员会看到"某某(技术字段)"却不知道它是什么。风控是合规敏感域,这里的静默降级值得加一条显式告警。


🟠 6.3【P1】前端 K004 标了 idempotent,但该操作语义上难以幂等

位置:common/api-client.js L96 K004: { method: 'DELETE', path: '/api/v1/knowledge/{knowledgeId}', idempotent: true }

idempotent: true 会让 apiClient 自动附带 Idempotency-Key。对"删除知识"而言,第二次调用与第一次的意图本就相同(都是让该条失效),语义上确实幂等;但若前端因超时重试而换了 key,就可能对同一 knowledgeId 发起两次"失效"——取决于后端是否按 Idempotency-Key 还是按资源状态判幂等。建议在 docs/05 明确该端点的幂等键语义,目前文档里未见说明。


🟠 6.4【P1】RK013–RK015 未登记进 docs/05 §19

位置:api-client.js L60–65 的注释已自述此事

RK013(非流式日报)当前无人调用,仅因"前端契约测试要求这张表里有它"而保留。RK014(SSE 流式)/RK015(邮件)在用但未登记。风险:docs/05 自称"接口唯一权威",此处已出现权威文档与实际端点的偏差——这正好削弱了它作为唯一权威的地位。


🟡 6.5【P2】ADVISOR_GOAL 注册但不可用

位置:api-client.js L79 ADVISOR_GOAL: { GET /api/v1/advisor/investment-goals/current }

页面从不调用它;且对投顾本人访问会返回 404。注释里点明了"注册与调用是两件事"。保留它是为了满足契约测试的表完整性——但这意味着约 120 条端点表里存在"只为测试存在"的条目,后续维护者容易误以为它可用。


🟡 6.6【P2】行情 15 分钟有效期是演示最容易翻的一环

trade_service.py 的 MAX_QUOTE_AGE = 15min,超时后所有委托一律 503「行情已过期」且无自动刷新。补刷需手动跑 python tools/sync_market_prices.py(立即生效、无需重启)。这不是 bug,但这是环境脆弱点:任何演示或验收前若没刷新,交易链路会整体不可用,且错误信息(503)不直接指向"行情过期"这一根因。


🟡 6.7【P2】workspace.js 的 submitRule 里 max_attempts 硬编码为 1

位置:workspace.js L515(在损坏区域附近,但本身逻辑正确)

代码注释已明确:max_attempts 不能超过"端点总数"(主端点 + fallbacks),而界面不编辑 fallbacks(提交空数组),所以只能写 1——写 2 会被后端 422 拒绝。这说明界面能力与后端约束不匹配:用户在界面上无法配置 fallback 端点,模型高可用能力在前端不可达。


🟡 6.8【P2】投影清理在"无 client"时仍标记消费,可能造成静默漂移

位置:worker/runtime.py 的 _cleanup_projection

无 client 时记 InteractionAudit(status="skipped_no_client") 并仍标记消费。这是刻意的(避免把"配置缺失"伪装成"投递失败"),但审计记录需要有人看:如果没人定期检查这些 skipped_no_client,向量库/图里就会积累已失效的记忆,而主流程完全正常。建议:给这类审计加一个可观测指标或定期巡检脚本。


🟡 6.9【P2】历史文档的分类边界需要人工判断

AGENTS.md 已把 docs/04/06/10/13/99 归为 D 类"不要用来判断当前进度",但它们仍在仓库中。这是 2026-09-11 评审的结论(保留成本为零)。风险:新人不会先读 AGENTS.md 的分类表,很可能直接读 docs/04 并采信过期结论。TODO.md 同样如此(5 处"49 张表"实为 51 张业务表)。


🟡 6.10【P2】风控证据表来源枚举与后端一致性依赖人工

位置:employee-risk/dashboard/dashboard.js 的 EVIDENCE_COLUMNS(8 类:customers/products/transactions/capital_flows/holdings/login_records/alerts/notifications)

前端的 EVIDENCE_COLUMNS[state.evidence.source] 是直接索引——若用户提交了不在该枚举里的 source,tableMarkup 的 columns.map 会在 undefined 上抛错。有 delete values.behavior_level 之类的防御,但没有对 source 本身的校验。建议确认后端 RK004 的路径参数枚举是否与前端的 8 类完全一致。


✅ 6.11 已核查、确认无问题的项(避免误报)

疑点 核查结论
A038 前端期望 identity 结构 vs rbac.py 的 /users/{user_id}/roles 一致。rbac.py 前缀同为 /api/v1/admin,get_user_identity() 返回 envelope(data),data 含 username/user_no/permissions/data_scope/roles
K002/K003 为何标 raw: true 正确且必要。其成功体是裸的 {knowledge_ids,...} / {items,count},无 data 信封。不标会导致上传后显示"已入库 0 块"而 DB 其实正常
A048/A049 存在的必要性 必要。PUT 需要 If-Match etag,而列表 meta 不携带 etag,必须先取详情
V001 为何 raw: true 正确。访客令牌接口返回裸 token
全部 44 个前端模块的语法 仅 workspace.js 一个失败,其余 43 个全部通过真实 ESM 解析

七、值得注意的边界情况(速查)

  1. 行情:15 分钟过期 → 全部委托 503,需手动补刷。
  2. Worker:不在跑 → agent_run 恒为 queued,前端显示"超时/繁忙"。
  3. config_release:环境数据,换环境必须重发白名单。
  4. RBAC 种子:tools/seed_test_rbac.py 是 DELETE 重建语义,没并进去的权限码重建一次就没了,表现是"接口突然 403"而无任何报错线索。
  5. Milvus schema:环境而异,检索层已运行时探测;不得硬编码字段名。
  6. 澄清轮次:advance_clarification 在 >= 10 拒绝。
  7. profile_snapshots:必须先清 current 再插新行。
  8. Outbox 枚举值:target_store/operation/status 必须小写。
  9. 时区:DB session 通过 asyncmy init_command 设 SET time_zone = '+00:00';业务计算用 local_hour/local_date。
  10. start.ps1:必须存为 UTF-8 with BOM,否则 Windows PowerShell 5.1 按 GBK 解析中文注释会语法报错。

八、附:核查方法与可复现命令

# 1. 确证 workspace.js 语法错误(node --check 会漏判,必须用真实解析)
node -e "import('file:///C:/Users/Windows/Desktop/项目代码/app/static/portal/employee-console/workspace/workspace.js').catch(e=>console.log(e.constructor.name, e.message))"

# 2. 全量扫描前端模块(仅 workspace.js 失败)
#    见本报告 §6.1 表格中的 vm.SourceTextModule 方式

# 3. 后端静态检查
mypy app                     # 249 files, 0 errors
pytest tests/unit tests/contract   # 1376 passed
pytest tests/integration     # 104 passed

# 4. 交付自检(两条线互补)
python tools/e2e_smoke_test.py       # 业务链路冒烟,6 条线 40 项
python tools/portal_api_check.py     # 接口契约体检,41 项

报告结束。本次分析全程只读,未修改任何代码或既有文档。