Compare commits

..
Author SHA1 Message Date
zhuyongxin 473bb5d004 docs(mvp): update RAG docs for py-rag extraction
- rewrite RAG architecture doc for py-rag service contract mapping, ingest and rebuild ops
- refresh observability/trace doc for post-L0 single-attempt semantics
- add py-rag chain exploration note; mark superseded Milvus/L0 notes with status banners
- close ISS-017 (superseded by L0 sinking); mark knowledge_domain table orphaned
- refresh architecture/mvp/engineering indexes
2026-09-30 17:03:50 +08:00
zhuyongxin 9cf162482d refactor(rag): extract retrieval and ingest to py-rag service
- replace in-process Milvus stack with PyRagClient + PyRagKnowledgeSearchAdapter behind KnowledgeSearchPort (RERANK score passthrough)
- move document ingest to py-rag /documents:ingest; DocumentManagementService keeps MySQL ledger + local files
- sink L0 query understanding to py-rag; drop KnowledgeQueryTransformer, single UNFILTERED_VECTOR attempt
- remove Milvus deps, config classes, dead demo services and obsolete rebuild scripts
- compose/Makefile reduced to MySQL/Redis; add pyrag.* config
2026-09-30 17:03:21 +08:00
zhuyongxin 83193bdf4a docs(harness): add overall architecture learning note
- Add architecture note: config assembly (three-layer organization),
  HTTP entry (thin controller + SSE state machine + disconnect cancel),
  session & memory system (PreviousTurn injection, terminology calibration,
  skill as procedural long-term memory), knowledge base write pipeline
2026-08-10 18:35:23 +08:00
zhuyongxin de56551fea docs(harness): add LLM judge design note (interview Q&A) 2026-08-10 11:34:37 +08:00
zhuyongxin da18fdf4e1 docs(harness): add agent domain learning note and mark all nine domains complete
- Add agent note: factory assembly, dual interceptors (model budget/token audit,
  tool five gates), use case loop shell, controlled stop recovery, dual-view projection
- Mark agent domain complete in roadmap (all nine domains done)
2026-08-10 10:12:04 +08:00
aruo e1b8d1fb2c docs(harness): add evidence-chain, application-audit, contract-state and interview review notes; annotate guard/release core classes 2026-08-08 00:12:25 +08:00
zhuyongxin 074d1aa5a9 docs(harness): add MySQL sandbox learning note and mark tool domain complete
- Add MySQL sandbox note: three defense layers (semantic/connection/output),
  fail-closed validation, allowlist, parameterization, cancellation, redaction
- Mark tool domain complete in roadmap; next: guard
2026-08-06 17:46:53 +08:00
zhuyongxin f26d395650 docs(harness): annotate RAG backend core classes and add retrieval learning note
- Annotate LookupKnowledgeTool, KnowledgeEvidencePostProcessor, RrfFusion, KnowledgeDocumentRetriever
- Add RAG retrieval learning note: L0 navigation, multi-recall + RRF, qualityScore,
  degradation, contract semantics, validation (audit + offline eval), discussion insights
2026-08-05 18:37:50 +08:00
zhuyongxin ff0752a16c docs(harness): annotate tool domain classes and add tool chain learning notes
- Annotate 43 tool domain classes (contract/projection/boundary/store/adapter/mysql)
- Add tool registration and execution chain learning note
- Add tool call chain runtime journey note (model decision to observation)
2026-08-04 18:37:17 +08:00
zhuyongxin 7844bcea40 docs(harness): annotate progress core classes and add code-level learning notes
- Annotate DiagnosisProgressTracker, HarnessToolInterceptor, DiagnosisProgressProjector,
  DiagnosisReleaseUseCase, ToolBoundary, ToolBoundaryResult, CanonicalToolInvocation
- Add progress code learning note: interceptor gates, tracker state machine,
  canonical lifecycle, execution gate, projection/release pipeline
- Update learning roadmap: progress marked as deeply learned, next is tool domain
2026-08-03 18:37:23 +08:00
wdm1802 e564863c43 docs(harness): add retry guide and learning roadmap; annotate retry core 2026-08-03 01:29:36 +08:00
wdm1802 d084202166 docs(harness): add budget flow sequence and execution control notes 2026-08-02 20:58:13 +08:00
zhuyongxin 5b2fb985d9 docs(harness): annotate entry orchestration, enums, and exception recovery paths 2026-07-30 19:08:03 +08:00
zhuyongxin b39a625e5b docs(mvp): add remaining harness audit guides and engineering index 2026-07-30 19:04:59 +08:00
zhuyongxin e9f1c48d34 docs(harness): add loop inner/outer exception handling guide 2026-07-30 18:55:43 +08:00
zhuyongxin 3a7eee8af4 docs(mvp): add harness design and progressive guides 2026-07-29 19:04:44 +08:00
zhuyongxin 584639fa2a docs(mvp): move engineering notes under mvp/engineering
Relocate RAG and diagnosis decision/E2E writeups from docs/ root into
mvp/engineering so architecture, issues, and engineering narrative stay
together. Update indexes and cross-links; leave docs/learning as legacy.
2026-07-29 10:49:45 +08:00
zhuyongxin bdac35567c docs(issues): add ISS-017 L0 filter fallback hardening
Track L0 hard-filter + unfiltered retry brittleness. Keep current
behavior; prefer A+C later and defer soft-constraint redesign (D).
2026-07-29 10:48:47 +08:00
zhuyongxin 7ae9707a3b feat(harness,rag): dual LLM audit fields, run conclusion, and hybrid quality
Persist provider reasoning and assistant text separately on agent_reasoning_audit
(DeepSeekAssistantMessage path), extract diagnosis_run.conclusion, enrich RAG
tool audit (step_id/query/qualityScore), gate empty mysql tools, drop devtools,
and align MVP docs after live E2E verification.
2026-07-28 19:43:13 +08:00
zhuyongxin 2f40536248 docs(mvp): document dense+BM25 hybrid knowledge retrieval
Add current RAG architecture covering MilvusClientV2 hybrid search,
chunk evidence identity, rebuild ops, and update the MVP architecture
index and system overview links.
2026-07-28 09:56:08 +08:00
zhuyongxin 729cd3544a chore(rag): remove obsolete PowerShell rebuild script 2026-07-28 09:50:15 +08:00
zhuyongxin 1e532fb851 chore(rag): rebuild script in Python and use biz collection
Switch default knowledge collection back to biz (drop+recreate on rebuild),
replace PowerShell rebuild runner with Python, skip README.md imports, and
merge duplicate rag keys in application.yml.
2026-07-28 09:49:43 +08:00
zhuyongxin 38f781b157 feat(harness): complete protocol repair stop and archive ISS-016
Add repairable INVALID_PROGRESS_PROTOCOL observations, independent
PROGRESS_PROTOCOL_VIOLATED saturation, and controlled release paths.
Archive the OpenSpec change after syncing main specs and devflow.
2026-07-27 19:10:07 +08:00
zhuyongxin 5c369f3b6c feat(rag): add hybrid knowledge rebuild API and script
Add confirm-gated rebuild-hybrid endpoint that drops biz_hybrid, clears
api_document and L0, then force-imports knowledge_base markdown into the
dense+BM25 store. Include PowerShell runner and ops README.
2026-07-27 18:55:00 +08:00
zhuyongxin f035538531 feat(rag): dense+BM25 hybrid on MilvusClientV2, drop SDK path
Replace legacy MilvusServiceClient knowledge search/write with a single
MilvusClientV2 hybrid store (BM25 function + dense ANN + RRFRanker).
Use collection biz_hybrid and require knowledge reindex.
2026-07-27 18:49:20 +08:00
zhuyongxin 376ad0c241 feat(rag): hybrid multi-path search with RRF fusion
Add configurable hybrid mode on KnowledgeSearchPort that fuses dense
unfiltered, dense filtered, and lexical ranks via RRF while preserving
dense-compatible threshold scores. Archives Delivery 2 OpenSpec change.
2026-07-27 18:33:31 +08:00
zhuyongxin ac1f831903 feat(rag): chunk evidence identity, dedup, and search port
Preserve same-document multi-chunk evidence with evidenceKey identity,
per-document caps, retrieve-k/return-n split, and a dense KnowledgeSearchPort.
Archives Delivery 1 OpenSpec change as the foundation for hybrid retrieval.
2026-07-27 18:26:15 +08:00
zhuyongxin 99d4f6f216 docs(rag): add comments on knowledge retrieval pipeline
Document the lookup_knowledge flow from L0 hints through L1 retrieval,
post-processing, packing, and Agent projection so the boundaries and
current limitations are easier to follow.
2026-07-27 16:26:06 +08:00
aruo edb6153fd6 docs(issue): mark iss-015 partially implemented 2026-07-27 10:22:09 +08:00
aruo d0452184ee feat(harness): add information gain stop and audit 2026-07-27 01:03:34 +08:00
zhuyongxin de5a5b09d9 docs(interview): refresh materials for single-agent harness narrative
Archive pre-refactor interview notes and add current deep-dives on
architecture evolution, issue-derived stories, and evidence gates.
2026-07-24 18:14:49 +08:00
427 changed files with 34067 additions and 8878 deletions
+1 -1
View File
@@ -65,8 +65,8 @@ uploads/
### Windows / Runtime Artifacts
*.stackdump
NUL
### MVP Demo Generated Outputs
mvp/demo/output/*.json
!mvp/demo/output/README.md
.pi/extensions/emdash-hook.ts
+16 -54
View File
@@ -5,9 +5,9 @@
SERVER_URL = http://localhost:9900
UPLOAD_API = $(SERVER_URL)/api/upload
DOCS_DIR = aiops-docs
HEALTH_CHECK_API = $(SERVER_URL)/milvus/health
DOCKER_COMPOSE_FILE = vector-database.yml
MILVUS_CONTAINER = milvus-standalone
# 服务就绪探测:9900 端口有 HTTP 响应即视为就绪
HEALTH_CHECK = curl -s -o /dev/null --connect-timeout 2 $(SERVER_URL)
DOCKER_COMPOSE_FILE = docker-compose.yml
# 颜色输出
GREEN = \033[0;32m
@@ -23,7 +23,7 @@ help:
@echo ""
@echo "可用命令:"
@echo " $(YELLOW)make init$(NC) - 🚀 一键初始化(启动Docker → 启动服务 → 上传文档)"
@echo " $(YELLOW)make up$(NC) - 启动 Docker Compose(Milvus 向量数据库)"
@echo " $(YELLOW)make up$(NC) - 启动 Docker Compose(MySQL/Redis)"
@echo " $(YELLOW)make down$(NC) - 停止 Docker Compose"
@echo " $(YELLOW)make status$(NC) - 查看 Docker 容器状态"
@echo " $(YELLOW)make start$(NC) - 启动 Spring Boot 服务(后台运行)"
@@ -42,7 +42,7 @@ help:
init:
@echo "$(GREEN)🚀 开始一键初始化 SuperBizAgent...$(NC)"
@echo ""
@echo "$(YELLOW)步骤 1/4: 启动 Docker Compose(Milvus 向量数据库)$(NC)"
@echo "$(YELLOW)步骤 1/4: 启动 Docker Compose(MySQL/Redis)$(NC)"
@$(MAKE) up
@echo ""
@echo "$(YELLOW)步骤 2/4: 启动 Spring Boot 服务$(NC)"
@@ -51,23 +51,23 @@ init:
@echo "$(YELLOW)步骤 3/4: 等待服务就绪$(NC)"
@$(MAKE) wait
@echo ""
@echo "$(YELLOW)步骤 4/4: 上传 AIOps 文档到向量数据库$(NC)"
@echo "$(YELLOW)步骤 4/4: 上传 AIOps 文档(经 py-rag 入库)$(NC)"
@$(MAKE) upload
@echo ""
@echo "$(GREEN)═══════════════════════════════════════════════════════$(NC)"
@echo "$(GREEN)✅ 初始化完成!所有文档已成功向量化存储到数据库$(NC)"
@echo "$(GREEN)✅ 初始化完成!所有文档已成功入库(py-rag)$(NC)"
@echo "$(GREEN)═══════════════════════════════════════════════════════$(NC)"
@echo ""
@echo "$(GREEN)🌐 服务访问地址:$(NC)"
@echo " API 服务: $(SERVER_URL)"
@echo " Attu (Web UI): http://localhost:8000"
@echo "$(YELLOW)💡 提示: 知识检索/入库由 py-rag 服务承担,请在其仓库单独启动$(NC)"
@echo ""
@echo "$(YELLOW)💡 提示: 服务正在后台运行,查看日志: tail -f server.log$(NC)"
# 启动 Spring Boot 服务(后台运行)
start:
@echo "$(YELLOW)🚀 启动 Spring Boot 服务...$(NC)"
@if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
@if curl -s -o /dev/null --connect-timeout 2 $(SERVER_URL); then \
echo "$(GREEN)✅ 服务已经在运行中 ($(SERVER_URL))$(NC)"; \
else \
echo "$(YELLOW)📦 正在启动服务(后台运行)...$(NC)"; \
@@ -84,7 +84,7 @@ wait:
@max_attempts=60; \
attempt=0; \
while [ $$attempt -lt $$max_attempts ]; do \
if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
if curl -s -o /dev/null --connect-timeout 2 $(SERVER_URL); then \
echo "$(GREEN)✅ 服务器已就绪!($(SERVER_URL))$(NC)"; \
exit 0; \
fi; \
@@ -100,7 +100,7 @@ wait:
# 检查服务器是否运行
check:
@echo "$(YELLOW)🔍 检查服务器状态...$(NC)"
@if curl -s -f $(HEALTH_CHECK_API) > /dev/null 2>&1; then \
@if curl -s -o /dev/null --connect-timeout 2 $(SERVER_URL); then \
echo "$(GREEN)✅ 服务器运行正常 ($(SERVER_URL))$(NC)"; \
else \
echo "$(RED)❌ 服务器未运行或无法连接!$(NC)"; \
@@ -205,38 +205,14 @@ test-upload:
echo "$(RED)测试文件不存在$(NC)"; \
fi
# 启动 Docker Compose(智能检测,避免重复启动)
# 启动 Docker Compose(MySQL/Redis;py-rag 服务在其仓库单独启动)
up:
@echo "$(YELLOW)🐳 检查 Docker 容器状态...$(NC)"
@echo "$(YELLOW)🐳 启动 Docker Compose(MySQL/Redis)...$(NC)"
@if [ ! -f "$(DOCKER_COMPOSE_FILE)" ]; then \
echo "$(RED)❌ Docker Compose 文件不存在: $(DOCKER_COMPOSE_FILE)$(NC)"; \
exit 1; \
fi
@if docker ps --format '{{.Names}}' | grep -q "^$(MILVUS_CONTAINER)$$"; then \
echo "$(GREEN)✅ Milvus 容器已经在运行中$(NC)"; \
echo "$(YELLOW)📋 当前运行的容器:$(NC)"; \
docker ps --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
else \
echo "$(YELLOW)🚀 启动 Docker Compose...$(NC)"; \
docker-compose -f $(DOCKER_COMPOSE_FILE) up -d; \
echo ""; \
echo "$(YELLOW)⏳ 等待容器启动...$(NC)"; \
sleep 5; \
if docker ps --format '{{.Names}}' | grep -q "^$(MILVUS_CONTAINER)$$"; then \
echo "$(GREEN)✅ Docker Compose 启动成功!$(NC)"; \
echo ""; \
echo "$(GREEN)📋 运行中的容器:$(NC)"; \
docker ps --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
echo ""; \
echo "$(GREEN)🌐 服务访问地址:$(NC)"; \
echo " Milvus: localhost:19530"; \
echo " Attu (Web UI): http://localhost:8000"; \
echo " MinIO: http://localhost:9001 (admin/minioadmin)"; \
else \
echo "$(RED)❌ 容器启动失败,请检查日志: docker-compose -f $(DOCKER_COMPOSE_FILE) logs$(NC)"; \
exit 1; \
fi; \
fi
@docker-compose -f $(DOCKER_COMPOSE_FILE) up -d && echo "$(GREEN)✅ Docker Compose 启动完成$(NC)"
# 停止 Docker Compose
down:
@@ -245,24 +221,10 @@ down:
echo "$(RED)❌ Docker Compose 文件不存在: $(DOCKER_COMPOSE_FILE)$(NC)"; \
exit 1; \
fi
@if docker ps --format '{{.Names}}' | grep -q "milvus"; then \
docker-compose -f $(DOCKER_COMPOSE_FILE) down; \
echo "$(GREEN)✅ Docker Compose 已停止$(NC)"; \
else \
echo "$(YELLOW)⚠️ 没有运行中的 Milvus 容器$(NC)"; \
fi
@docker-compose -f $(DOCKER_COMPOSE_FILE) down && echo "$(GREEN)✅ Docker Compose 已停止$(NC)"
# 查看 Docker 容器状态
status:
@echo "$(YELLOW)📊 Docker 容器状态:$(NC)"
@echo ""
@if docker ps -a --format '{{.Names}}' | grep -q "milvus"; then \
docker ps -a --filter "name=milvus" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"; \
echo ""; \
running=$$(docker ps --filter "name=milvus" --format '{{.Names}}' | wc -l | tr -d ' '); \
total=$$(docker ps -a --filter "name=milvus" --format '{{.Names}}' | wc -l | tr -d ' '); \
echo "$(GREEN)运行中: $$running / $$total$(NC)"; \
else \
echo "$(YELLOW)⚠️ 没有找到 Milvus 相关容器$(NC)"; \
echo "$(YELLOW)提示: 运行 'make docker-up' 启动容器$(NC)"; \
fi
@docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
+38 -2
View File
@@ -181,7 +181,43 @@
### Evidence Status
- 定义:证据 Tool 的结果语义,固定为 `EVIDENCE_FOUND`、`NO_EVIDENCE`、`ERROR`。
- 边界:`NO_EVIDENCE` 只表示当前查询范围内没有匹配结果,不能解释为问题不存在、根因被排除或系统健康。
- 边界:`EVIDENCE_FOUND` 只表示存在候选内容,不保证内容能够支持当前诊断;`NO_EVIDENCE` 只表示当前查询范围内没有匹配结果,不能解释为问题不存在、根因被排除或系统健康。
### Information Gain
- 定义:一次 Tool 结果是否推进当前 Diagnosis Run 的语义评价,固定为 `GAINED` 或 `NO_GAIN`。
- 边界:它评价的是结果对当前诊断的作用,不评价 Tool 产品质量;`NO_EVIDENCE` 和重复的规范化 `tool + scope` 可由 Harness 机械标记为 `NO_GAIN`,其他成功非空结果(包括 RAG `REFERENCE`)由模型评价。
### Collection State
- 定义:Diagnosis Harness 对当前 Run 是否允许继续收集证据的控制状态,固定为 `COLLECTING` 或 `SATURATED`。
- 边界:状态由 Harness 维护;`SATURATED` 可因连续 `NO_GAIN` 或连续进展协议错误达到各自配置阈值而进入,不包括硬预算耗尽。模型可以请求新的 Tool 调用,但不能绕过 `SATURATED`。
### Diagnosis Stop Reason
- 定义:Harness 停止当前 Run 继续调用 Tool 的内部原因,首版区分 `INFORMATION_SATURATED`、`BUDGET_LIMIT_REACHED` 与 `PROGRESS_PROTOCOL_VIOLATED`。
- 边界:它用于控制、Trace 和 Release 输入,不是用户可见生命周期状态,也不进入模型上下文;真正的不可恢复技术故障走失败通道。协议错误不累计为 `NO_GAIN`,使用独立阈值与 stop reason。
### Progress Protocol Violation
- 定义:模型未遵守 Tool Call Envelope 进展协议时的安全错误分类,例如缺失/错序/意外 `previous_observation`、缺失 `input` 或非法 Envelope。
- 边界:返回可修正 observation(`repair_required`、`violation_type`、期望上一轮 Tool Call ID、允许的 `information_gain`);连续错误达到阈值后交付一次 `STOP_REQUIRED/PROGRESS_PROTOCOL_VIOLATED`。不泄露业务参数、上一轮观察正文、raw response 或内部异常。
### Progress Snapshot
- 定义:Tool Loop 结束时,从当前 Run 的 Canonical Tool Result 一次性投影出的有界发布视图,用于生成已检查范围和客观结果。
- 边界:Canonical Tool Result 是真理源;Progress Snapshot 不逐轮维护、不保存原始 Tool Response、Prompt 或内部 thought,也不直接进入模型上下文。
### Safe Fallback Type
- 定义:`SafeFallback.type` 对没有发布诊断结论的业务原因分类,例如 `INSUFFICIENT_EVIDENCE`、`MISSING_REQUIRED_CONTEXT`、`BUDGET_EXHAUSTED` 或安全校验失败。
- 边界:它是 `ReleaseOutcome.FALLBACK` 的原因字段,不是与 `SUCCESS / FALLBACK / FAILED / CANCELLED` 平行的第二套生命周期状态。
### Diagnosis Release Use Case
- 定义:诊断业务发布的唯一决策入口,接收 DiagnosisDraft 和/或 Harness `stop_reason + ProgressSnapshot`,生成安全的 `SUCCESS / FALLBACK` 结果。
- 边界:`conclusion=null` 不触发 EvidenceRepair;只有存在结论时才执行完整 EvidenceGuard、EvidenceRepair 和 SemanticGuard 链路。不可形成安全业务内容的技术故障由 Chat Application Use Case 映射为 `FAILED / CANCELLED`。
### Diagnosis Draft Contract Failure
- 定义:Diagnosis Agent 最终文本为空、不是严格 JSON,或不满足 `DiagnosisDraft` Schema 时产生的 Agent 输出合同失败。
- 边界:非法文本始终丢弃,不做 Markdown/自然语言提取,也不调用模型修复;仅当当前 Run 的 `ProgressSnapshot` 含已验真 observed facts 时,Release 才能确定性发布 `INSUFFICIENT_EVIDENCE`,否则保持 `FAILED`。它不是 `Diagnosis Stop Reason`,不得伪装成信息饱和或预算终止。
### Model Observation
- 定义:Tool 内部标准化结果经过白名单投影后,作为 Tool Response 进入 Diagnosis Agent 上下文的有界视图。
- 边界:只包含模型完成语义判断和证据引用所需的信息;预算、阈值、重复指纹、原始相似度、原始 Tool Response 和完整 Harness 控制状态不得进入该视图。
### RunContext
- 定义:一次 Diagnosis Run 的显式执行上下文,结构不可变地携带 `sessionId`、`runId`、deadline,以及该 Run 独占的取消、预算、重试策略和生命周期状态句柄。
@@ -201,7 +237,7 @@
### Chat Application Use Case
- 定义:一次 Chat 请求的唯一业务入口,拥有 Session/Run、意图路由、固定执行器、PreviousTurn 和最终持久化。
- 边界:不拥有 HTTP/SSE 连接,也不把 ChatModel 或 Tool 选择权交给 Controller。
- 边界:不拥有 HTTP/SSE 连接,不把 ChatModel 或 Tool 选择权交给 Controller,也不在 Diagnosis Release Use Case 之外单独决定预算 Fallback 的业务内容。
### Chat SSE Contract
- 定义:Chat 公开入口的五事件协议,顺序固定为 `metadata -> status* -> content|failure -> done`。
+9 -2
View File
@@ -1,16 +1,23 @@
# devflow 索引
# devflow 索引
## Issue 生命周期
| Issue | 状态 | 说明 |
|---|---|---|
| ISS-014 | archived | 阶段 0-7 的单体 Diagnosis Agent、Harness、ACI、SSE、清理和最终 E2E 已完成并归档;阶段实现对应的 11 个 devflow/OpenSpec 项目均已 archived。 |
| ISS-015 | active | 承接 ISS-014 E2E 后发现的 Agent 硬停止、Evidence Repair Schema、Reasoning 审计验证/治理和 Fallback 信息质量问题。 |
| ISS-015 | active | 阶段 1 硬停止已由 ISS-016 收口;剩余 Evidence Repair Schema、Reasoning 审计验证/治理与最终综合验收。 |
| ISS-016 | archived | Diagnosis 信息增益停止契约、协议修复反馈与统一 Release 已完成并归档。 |
## 项目
| 日期 | slug | 说明 | 领域 | 关键词 | 关联 OpenSpec | 状态 |
|---|---|---|---|---|---|---|
| 2026-07-28 | rag-eval-hybrid-baseline | 离线 RAG eval 对齐 hybrid:search.mode 生成器、fixture meta、baseline 重刷;hybrid 质量闸门可用 denseDistance。 | RAG/eval/baseline | search.mode, fixture meta, kb_scope rag-eval, L0 filter fallback, denseDistance | openspec/changes/archive/2026-07-28-rag-eval-hybrid-baseline | archived |
| 2026-07-28 | rag-quality-score-unify | 统一 dense/hybrid scoreLabel 与 qualityScore;保检索序;去掉关键词 boost 改序与 hybrid L2 伪装。 | RAG/质量分/后处理 | qualityScore, scoreLabel dense/hybrid, originalRank, RetrievalScoreNormalizer, no boost rerank | openspec/changes/archive/2026-07-28-rag-quality-score-unify | archived |
| 2026-07-26 | diagnosis-information-gain-stop-contract | Diagnosis 信息增益停止、协议修复反馈、ProgressSnapshot 与统一 Release。 | Harness/Diagnosis stop/Release | ISS-016, GAINED, NO_GAIN, STOP_REQUIRED, ProgressSnapshot, PROGRESS_PROTOCOL_VIOLATED, INSUFFICIENT_EVIDENCE | openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract | archived |
| 2026-07-27 | rag-chunk-evidence-identity-dedup | chunk 级证据身份、去重、retrieve-k/return-n 与 SearchPort 地基,为 hybrid 铺路。 | RAG/证据身份/去重 | evidenceKey, maxChunksPerDocument, retrieve-k, return-n, KnowledgeSearchPort, document_id chunk-scoped | openspec/changes/archive/2026-07-27-rag-chunk-evidence-identity-dedup | archived |
| 2026-07-27 | rag-bm25-hybrid-drop-sdk | 真 dense+BM25 hybrid(MilvusClientV2),废弃知识路径旧 SDK 检索/写入。 | RAG/BM25/hybrid | MilvusClientV2, BM25, hybridSearch, RRFRanker, biz_hybrid, drop SDK path | openspec/changes/archive/2026-07-27-rag-bm25-hybrid-drop-sdk | archived |
| 2026-07-27 | rag-hybrid-search-rrf | Delivery 2:可配置 hybrid 检索与 RRF 多路融合(不绑旧 SDK)。 | RAG/hybrid/RRF | hybrid mode, RRF, KnowledgeSearchPort, filtered+unfiltered fusion, sparse-lite lexical | openspec/changes/archive/2026-07-27-rag-hybrid-search-rrf | archived |
| 2026-07-21 | single-react-tool-invocation-store | 建立统一 ToolBoundary 与 Redis canonical invocation store,集中生命周期、证据状态、TTL、容量和 Run 所有权。 | Harness/Tool boundary/Canonical store | ISS-014, ToolBoundary, canonical invocation, PROJECTING, READY, ERROR, TTL, RESULT_TOO_LARGE | openspec/changes/archive/2026-07-21-single-react-tool-invocation-store | archived |
| 2026-07-21 | single-react-harness-run-context | 建立显式 RunContext、Harness Core、预算、取消、类型化重试和 Tool Store 基础。 | Harness/Run lifecycle/Budget | ISS-014, RunContext, deadline, cancellation, budget, retry, ToolCallKey | openspec/changes/archive/2026-07-21-single-react-harness-run-context | archived |
| 2026-07-21 | single-react-aci-tool-contracts | 冻结 RAG、日志和 MySQL evidence Tool 的 Agent-facing ACI Schema、状态、框架调用引用和描述边界。 | Harness/Agent Tool contract | ISS-014, ACI, tool_call_id, evidence_status, RAG, query_logs, query_mysql, MOCK | openspec/changes/archive/2026-07-21-single-react-aci-tool-contracts | archived |
@@ -0,0 +1,68 @@
# Diagnosis 信息增益停止契约 验收
## 结果
已接受。OpenSpec tasks 37/37 完成;用户确认归档 OpenSpec、提交并推送。
## 验证
### 静态验证
- 命令/检查:`openspec validate diagnosis-information-gain-stop-contract --strict`
- 结果:passed
- 备注:Committed OpenSpec 与最终 tasks 一致
- 命令/检查:OpenSpec delta → main specs 同步(6 个 capability)
- 结果:passed
- 备注:新建 `openspec/specs/diagnosis-information-gain-stop-contract/`,并更新 5 个既有 main specs
- 命令/检查:对照 OpenSpec 与代码路径(progress tracker、interceptor、release、trace)
- 结果:passed
- 备注:Task 8 行为与 design/spec 对齐
### 脚本验证
- 命令:`mvn -q "-Dtest=DiagnosisProgressTrackerTest,HarnessToolInterceptorTest,DiagnosisReleaseUseCaseTest,DiagnosisAgentUseCaseTest,HarnessChatConfigurationTest" test`
- 结果:passed(exit 0)
- 备注:覆盖协议 violation_type、可修正 observation、连续协议错误 STOP、Release fail-closed、Agent 受控停止
- 命令:历史全量回归 `mvn -q -Dtest='!MilvusConnectionTest' test`(tasks 6.2/7.5 阶段)
- 结果:passed(`Tests=292, Failures=0, Errors=0, Skipped=3`)
- 备注:未设置 `MILVUS_TOKEN` 时 `MilvusConnectionTest` 失败属外部凭据边界,非本变更回归
### 浏览器/人工验证
- 步骤:Maven 启动真实应用;Query `诊断切换企业失败的问题`;named SSE + logs + `scripts/query_mysql.py` 按 exact sessionId/runId 核对
- 结果:passed
- 备注:
- `sessionId=iss016-final-20260726-a`
- `runId=3ab22ed7-d0ed-45d8-b928-dce5790c0542`
- SSE:`SAFE_FALLBACK` / `MISSING_REQUIRED_CONTEXT` / `done.outcome=FALLBACK`
- DB:`status=SUCCESS`、`intent=DIAGNOSIS`、`release_outcome=FALLBACK`、`tool_call_count=0`、`total_token_count=2890`
- Trace:`RUN_STARTED -> ROUTING_* -> AGENT_MODEL_STEP -> EVIDENCE_GUARD_INITIAL -> RELEASE_DECISION/FALLBACK -> RUN_FINISHED/FALLBACK`
### 未验证
- 本轮 Archive 未再重跑全量 `mvn test` 与完整 live SSE E2E;依赖 Apply 阶段记录与本轮 focused tests。
- 真实 Provider 下连续协议错误的 live E2E 未单独复跑;协议停止由 focused/scripted loop 覆盖。
- ISS-015 阶段 2(Evidence Repair Schema)与阶段 3(Reasoning 治理)不在本 change 范围。
## 已完成范围
- 信息增益停止、scope 去重、STOP_REQUIRED、ProgressSnapshot、统一 Release
- Token 审计与 Tool 拒绝 Trace
- 协议修复反馈 + `PROGRESS_PROTOCOL_VIOLATED` 兜底停止
- 文档:ISS-015 阶段 1、ISS-016、架构文档、glossary 术语、devflow 档案
## 已知限制
- 重复检测只比较确定性 `tool_name + normalized_scope`,不做自然语言语义去重。
- 协议错误阈值默认 2,与无增益阈值独立配置。
- 无安全 ProgressSnapshot 的受控停止继续 fail closed,不伪造用户可见事实。
- 公开 SSE/前端协议无新增字段;模型侧 Tool Envelope 是已确认 L3 变更。
## 交接
- 下一步:OpenSpec 已用户确认归档;代码提交并推送到当前分支。
- OpenSpec 归档确认:用户确认归档(“执行,完后提交推送”)
- 归档位置:`openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract/`
@@ -0,0 +1,45 @@
# Diagnosis 信息增益停止契约 Brief
## 背景
- 用户目标:未知问题、空证据或缺少查询条件时,Diagnosis 能以正常业务 Fallback 结束,而不是空转到预算耗尽或 `INTERNAL_FAILURE`。
- 当前问题:停止主要依赖模型自觉结束或硬预算;缺少信息增益回传、确定性饱和停止、协议修复反馈和统一 Release。
- 关联 OpenSpec:`openspec/changes/diagnosis-information-gain-stop-contract/`(归档后见 `openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract/`)
- 关联 Issue:ISS-016(承接 ISS-015 阶段 1 硬停止)
- devflow 分档:complex
- 接口影响:L3(模型可见 Tool Envelope 有意变更;公开 HTTP/SSE 与业务 Tool backend 不变)
## 范围
### 本次要做
- Run 内二值信息增益 `GAINED / NO_GAIN`、连续无增益阈值与 `COLLECTING / SATURATED`。
- 服务端注册的 Tool Call Envelope:`previous_observation + input`。
- 确定性 `NO_GAIN`(`NO_EVIDENCE`、重复 `tool_name + normalized_scope`)。
- 一次 `STOP_REQUIRED` 收尾机会与受控停止异常。
- Canonical 控制视图 / 模型白名单观察双视图;RAG 保留 `relevance_level`。
- Tool loop 结束时一次性投影 `ProgressSnapshot`。
- `DiagnosisReleaseUseCase` 统一有结论、无结论、信息饱和、预算终止、协议违规停止的发布。
- 可修正 `INVALID_PROGRESS_PROTOCOL` observation,以及独立 `PROGRESS_PROTOCOL_VIOLATED` 兜底停止。
- 模型 Token 组件/轮次审计与 `TOOL_REQUEST_REJECTED` 安全 Trace。
- Prompt、配置、focused/回归测试与真实 named SSE E2E。
### 本次不做
- `new_count`、`next_action`、多级质量分数、独立 Judge。
- 自然语言语义去重。
- 第二套诊断生命周期状态。
- 公开 HTTP/SSE 字段或前端进度协议新增。
- ISS-015 Reasoning 原文审计治理与 Evidence Repair Schema 注入。
### 影响区域
- `harness.progress`、`HarnessToolInterceptor`、`HarnessEvidenceTools`
- `DiagnosisAgentUseCase` / Prompt / Release / Application 预算兜底迁移
- Trace 审计、配置绑定、ISS-015/016 与架构文档
## OpenSpec 对齐
- proposal 覆盖状态:已覆盖
- specs 覆盖状态:已覆盖(6 个 capability delta,已同步 main specs)
- tasks 覆盖状态:已覆盖(37/37 完成)
@@ -0,0 +1,188 @@
# Diagnosis 信息增益停止契约 Decisions
## Discover Status
- Checkpoint:Discover。
- Capability source:`sm-flow` 内置 Discover 协议;grill 使用 `grill-with-docs`,代码可证问题通过源码、测试和引用搜索处理。
- Scale:`complex`。变更跨越 Agent、Tool 协议、Run 生命周期、Release、Guard、配置、Trace 和 E2E。
- 接口影响:L3。模型可见 Tool Schema 发生有意协议变更,公开 HTTP/SSE 和业务 Tool backend 协议不变。
- 工具降级:当前没有 `codebase-retrieval` 和 LSP 工具;以 `rg`、源码和测试引用核查替代。用户已明确“可以忽略gitnexus”。
## Question Pool
| # | 维度 | 问题 | 模式 | 状态 |
|---|---|---|---|---|
| Q1 | 术语 | Tool 客观状态、信息增益、收集状态、停止原因和最终发布状态是否应合并为一个枚举? | user-interview | 已解决 |
| Q2 | 语义 | Tool 返回价值由谁判断,是否需要多级质量分数? | user-interview | 已解决 |
| Q3 | 协议 | 模型继续调用 Tool 时如何回传上一轮信息增益,是否需要 `next_action`? | user-interview | 已解决 |
| Q4 | 边界 | Tool Schema 应来自 Prompt 还是服务端原生 Tool Calling 注册? | user-interview | 已解决 |
| Q5 | 边界 | Harness 能确定性判定哪些 `NO_GAIN`,RAG `REFERENCE` 由谁判定? | user-interview | 已解决 |
| Q6 | 范围 | 首版是否需要 `new_count` 或自然语言语义去重? | user-interview | 已解决 |
| Q7 | 配置 | 连续无增益阈值是否可配置,默认值与生效时机是什么? | user-interview | 已解决 |
| Q8 | 发布 | 信息饱和、预算终止和 `conclusion=null` 由谁转换为用户可见结果? | user-interview | 已解决 |
| Q9 | 验收 | 如何证明未知问题不再以通用内部错误结束,同时不放过无证据结论? | evidence-driven | 已解决 |
| Q10 | 技术 | 当前 Tool schema 是否能直接容纳 `previous_observation`? | evidence-driven | 已解决 |
| Q11 | 技术 | 进展控制状态应扩展 Redis Store 还是放入 RunContext handle? | evidence-driven | 已解决 |
| Q12 | 技术 | RAG `relevance_level` 在哪一层丢失,前端是否已有过程展示能力? | evidence-driven | 已解决 |
## Evidence-driven
| 结论 | 证据来源 | 是否已汇报用户 |
|---|---|---|
| 当前三个 Agent-facing Tool 直接使用 `RagToolRequest`、`QueryLogsRequest`、`MysqlToolRequest` 生成 Schema;要增加 `previous_observation + input` 必须显式演进 Tool Schema,不能只改 interceptor。 | `HarnessEvidenceTools`、三个 request records、`DiagnosisAgentFactory` | 已汇报 |
| `HarnessToolInterceptor` 当前把完整 `agentResult` 放入 `ToolCallResponse.content`,控制视图与模型观察尚未分离。 | `HarnessToolInterceptor` | 已汇报 |
| `RunContext` 已采用结构不可变、可变状态存在线程安全 handle 的模式;进展 tracker 放入 RunContext 比扩展 Redis 按 Run 枚举更符合现有所有权。 | `RunContext`、`DiagnosisHarnessCore.startRun` | 已汇报 |
| `CanonicalInvocationStore` 只有 begin/find/markReady/markError,扩展按 Run 枚举会影响 Redis 实现和多组 fake store;首版可由 tracker 保存完成调用 key,在结束时按 key 读取 canonical 记录。 | `CanonicalInvocationStore` 及其引用测试 | 已汇报 |
| `RagResultProjector` 只按 evidence 是否为空生成 `EVIDENCE_FOUND / NO_EVIDENCE`,没有读取上游 `relevanceLevel / relevance_level`。 | `RagResultProjector`、`LookupResult`、`KnowledgeEvidencePostProcessor` | 已汇报 |
| `DiagnosisReleaseUseCase.execute` 当前强制 draft 非空并对所有 Draft 运行 EvidenceGuard;`EvidenceGuard` 又把空 analysis 判为 `ANALYSIS_MISSING`,与合法无结论结果冲突。 | `DiagnosisReleaseUseCase`、`EvidenceGuard` | 已汇报 |
| `ChatApplicationUseCase.recoverBudgetExhaustion` 已有未提交预算 Fallback,但它绕过 Diagnosis Release,需迁移而不是丢弃用户价值。 | `ChatApplicationUseCase`、`SafeFallbackFactory`、现有测试 diff | 已汇报 |
| 前端已渲染 `observed_facts / verified_sources / limitations / next_steps`,不需要新增公开展示协议。 | `src/main/resources/static/app.js` | 已汇报 |
| 验收必须同时覆盖主动无结论、Harness 饱和、预算终止、无证据结论被 Guard 拦截,以及原始未知 Query 的 live SSE、日志和 exact run 数据。 | 当前事故现象、ISS-016 验收项、现有 E2E 工具 | 已汇报 |
## User-interview
| 问题原文 | 用户原话 | 确认状态 | OpenSpec 回写 |
|---|---|---|---|
| 是否精简状态而不建立第二套生命周期? | “这里我觉得设计得太混乱了,怎么简化”以及对最终简化架构“我觉得可以” | 已确认 | 已回写 |
| 是否只保留 `GAINED / NO_GAIN`? | “质量状态只要 information_gain = GAINED \| NO_GAIN 就够了?”后确认“我觉得可以” | 已确认 | 已回写 |
| 是否需要 `new_count`? | “好那就去掉new_count” | 已确认 | 已回写 |
| 是否需要 `next_action`? | “那就去掉next_action,我觉得由llm自己去判断就好了,不用显示的指定” | 已确认 | 已回写 |
| Tool 如何注入? | “首先 tool注入,由服务端注入,而不是写死在提示词中” | 已确认 | 已回写 |
| Prompt 是否强调合法放弃和正确但无用的内容? | “需要说明 模型不必须要给出一个答案”以及“如果你发现工具返回的是正确但对推导无用的废话,请停止调用” | 已确认 | 已回写 |
| 阈值是否可配置? | “我觉得这个可以暴露出一个配置来控制” | 已确认 | 已回写 |
| 首版重复检测做到什么程度? | “也就是说这一版只是做参数的去重校验”后确认“可以” | 已确认 | 已回写 |
| 是否按当前简化方案进入实施? | “可以用更简单的方式”“可以。修正一下文档”“用sm-flow开始实施把” | 已确认 | 已回写,并授权完成 Commit 后进入 Apply |
## 关键取舍
- 决策:停止权归 Harness,语义价值判断由模型与确定性规则共同产生。
- 原因:Tool 只能知道客观返回,模型才能判断内容是否推进当前假设;但空结果和完全重复 scope 可由代码零 Token 判定。
- 影响:Harness 只消费二值信息增益,不引入独立 Judge 或质量分数。
- 决策:使用下一次 Tool Call Envelope 回传上一轮模型评价。
- 原因:模型只有看到 Tool Observation 后才能评价,下一次真实行为正好提供受 Schema 约束的回传边界。
- 影响:这是 L3 Agent-facing Tool Schema 变更,业务 request 在 interceptor 内解包后保持不变。
- 决策:进展 tracker 是 RunContext handle,Canonical Store 保持 Tool 真相源。
- 原因:停止决策需要低延迟 Run 内状态,完整证据仍应由 canonical 记录提供;两者职责不同。
- 影响:tracker 保存计数、scope、待评价调用和 canonical keys,不复制 raw payload。
- 决策:不创建 ADR。
- 原因:这些是 ISS-016 范围内可通过 OpenSpec 回滚的内部协议演进,已有架构文档详细记录取舍,尚不满足独立 ADR 的必要性。
## OpenSpec 回写
- 必须进入 proposal/design/spec/tasks:Tool Envelope、L3 影响、Run tracker、确定性 `NO_GAIN`、RAG `relevance_level`、双视图、STOP_REQUIRED、ProgressSnapshot、统一 Release、Prompt、配置和 E2E。
- 必须保留非目标:无 `new_count`、无 `next_action`、无 Judge、无语义去重、无第二套诊断生命周期、无公开 SSE 协议新增。
- 当前没有未确认的 user-interview 问题,也没有 devflow/OpenSpec 冲突。
## Cross-Artifact 对齐检查
| 上游 → 下游 | 检查内容 | 状态 |
|---|---|---|
| ISS-016/架构文档 → proposal | 未知问题、合法放弃、信息增益、饱和停止、双视图、统一 Release、非目标和 E2E | 已对齐 |
| proposal → design | L3 Envelope、Run tracker、scope、STOP_REQUIRED、ProgressSnapshot、预算终态、Prompt 和迁移方案 | 已对齐 |
| design → specs/tasks | 状态机、Tool 门禁、RAG relevance、无结论 Guard、Release 所有权、Trace 和兼容边界 | 已对齐 |
| specs → tasks | 每个可观察行为均有 contract/state/loop/release/config/E2E 可执行切片 | 已对齐 |
### Gap 详情
- 无。
## Architecture Audit
- Capability source:`zoom-out`。按 glossary 的 Diagnosis Agent、Diagnosis Harness、RunContext、Evidence Status、Invocation Status、Release Outcome 术语审计。
- 顶层链路:`ChatApplicationUseCase -> DiagnosisChatExecutor -> DiagnosisAgentUseCase -> ReactAgent/interceptors -> ToolBoundary/canonical store -> ProgressSnapshot -> DiagnosisReleaseUseCase -> SSE/persistence`。
- 所有权:Agent 负责诊断语义;Tool/Projector 负责客观结果;Run tracker 负责停止控制;Canonical Store 负责 Tool 真相;Guard 负责引用与结论安全;Release 负责用户可见 SUCCESS/FALLBACK;Application 只负责编排和持久化。
- `RunContext` 的生产代码构造点只有 `DiagnosisHarnessCore.startRun`,大量测试通过该工厂获取;新增 tracker 不需要扩散手工构造。
- `DiagnosisAgentUseCase`、`HarnessToolInterceptor`、`HarnessEvidenceTools` 和 `DiagnosisReleaseUseCase` 的直接消费者均已由配置类和 focused tests 覆盖,任务清单包含所有构造调用更新。
- `FallbackType` 新语义只通过通用 SafeFallback JSON/前端渲染消费,没有前端枚举 switch;公开协议不新增字段。
- 最大框架风险是 Tool Envelope Schema 和 STOP_REQUIRED 后异常传播;design 要求三个具体 record、真实 callback schema 测试和 scripted framework-loop 测试在 Release 迁移前锁定行为。
- 最大生命周期风险是预算已把 RunLifecycle 置为 `BUDGET_EXHAUSTED` 后 Application 再次 `checkActive`;design 将其限制为“Diagnosis Release 已处理的预算 Fallback”窄分支,并禁止 Application 重建业务内容。
- 审计结论:模块职责没有形成新的循环依赖或第二真相源;L3 风险已进入 specs 和 tasks,可进入 commit gate。
## Commit Gate Preflight
- `proposal.md`、`design.md`、六份 capability delta specs 和 `tasks.md` 均存在。
- `openspec status --change diagnosis-information-gain-stop-contract --json` 返回 `isComplete=true`。
- `openspec validate diagnosis-information-gain-stop-contract --strict` 通过。
- question pool 全部已解决;evidence-driven 结论已汇报;user-interview 决策均有用户原话和确认状态。
- 接口影响已判为 L3,并有独立 Interface Impact、兼容、迁移、回滚和验收说明。
- Cross-artifact 检查无 gap;架构风险均已进入 design/tasks。
- 用户已通过“用sm-flow开始实施把”明确授权 Commit 后进入 Apply。
## Pre-apply Research
### 参考实现
- `HarnessEvidenceTools`:现有三类 `FunctionToolCallback` 注册点和 adapter bridge,继续作为 Agent-facing Schema 唯一入口。
- `HarnessToolInterceptor`:可获得 exact framework Tool Call ID,适合消费 Envelope 和执行 progress gate。
- `ToolBoundary`:Tool 预算、Run 校验、canonical 写入和安全错误的单点,不在 interceptor 重复 reserve。
- `RunContext` / `DiagnosisHarnessCore.startRun`:结构不可变 + 可变 handle 模式和唯一生产构造点。
- `RagResultProjector` / `QueryLogsResultProjector` / `MysqlResultProjector`:bounded canonical agent result 的现有标准化模式。
- `EvidenceGuard` / `DiagnosisReleaseUseCase`:当前结论验证链和无结论冲突位置。
- `ChatApplicationUseCase.recoverBudgetExhaustion`:保留用户价值、需要迁移所有权的临时预算 Fallback。
- `DiagnosisAgentUseCaseTest.ScriptedChatModel`:真实框架 model -> Tool -> model loop 回归模式。
### 技术栈清单
- Tool Schema:三个具体 record 交给 Spring AI `FunctionToolCallback.inputType`,共享 `PreviousObservation`,不使用泛型擦除或 JsonNode Schema。
- JSON:继续使用项目 `ObjectMapper` 严格解析/序列化;控制字段在 interceptor 消费后只传业务 input。
- Run 状态:新增线程安全 tracker handle,由 `DiagnosisHarnessCore.startRun` 创建,不使用 ThreadLocal。
- Canonical 真相:继续使用 `ToolCallKeyFactory + CanonicalInvocationStore.find`;tracker 只记录 identity。
- 视图:从 bounded canonical `agent_result` 白名单投影 Model Observation,不读取 raw response。
- 异常:受控停止使用专用异常和 cause-chain 分类;未知异常保持 fail closed。
- 测试:JUnit 5、scripted ChatModel、现有 fake store/adapter fixture;不增加 Maven 依赖。
### 新建基础设施
- `harness.progress`:信息增益、收集状态、停止原因、tracker、scope、snapshot/projector。
- `harness.agent`:三个 Agent-facing Envelope、白名单 observation projector、受控停止异常和执行结果。
- 不新增数据库表、Redis 数据结构、HTTP DTO、SSE event 或外部依赖。
## Apply 期间设计补充:Draft 合同失败
- 真实 E2E `runId=4e667111-524e-4407-87ab-b4b262952017` 已完成一次 READY RAG 调用,第二轮模型返回文本后在 Draft/Release 边界失败;后续三次同 Query 均走零 Tool 的 `MISSING_REQUIRED_CONTEXT`,证明模型输出存在随机分支。
- 用户确认采用窄化降级:非法 Draft 自身不被接受;已有当前 Run 的安全 ProgressSnapshot 时发布 `INSUFFICIENT_EVIDENCE`,没有安全过程时继续 `FAILED`。
- 这是有意行为变更:从“所有非法 Draft 都发布技术失败”调整为“非法 Draft + 已验真过程可发布过程型 Fallback”;公开 SSE 字段、Tool 协议和最终生命周期枚举不变。
- 不新增 stop reason,不把 Draft 解析失败伪装成 `INFORMATION_SATURATED` 或 `BUDGET_LIMIT_REACHED`;使用 Agent 输出异常携带有界 snapshot,并以脱敏 Trace 区分输出合同失败。
## Apply Verification
- Focused tests:`DiagnosisAgentUseCaseTest`、`DiagnosisReleaseUseCaseTest`、`DiagnosisChatExecutorTest`、`HarnessChatConfigurationTest` 通过。
- 完整回归:`mvn -q -Dtest='!MilvusConnectionTest' test` 退出码为 `0`;本轮 Surefire 报告汇总 `Tests=292, Failures=0, Errors=0, Skipped=3`。
- 外部凭据边界:未排除时唯一失败为 `MilvusConnectionTest.connect`,原因是当前测试进程未设置 `MILVUS_TOKEN`;这不是本变更回归。
- OpenSpec:`openspec.cmd validate diagnosis-information-gain-stop-contract --strict` 通过。
- 格式与清理:`git diff --check` 通过;未发现临时 E2E JSON、DEBUG 或 tmp 文件。
- named SSE E2E:Query `诊断切换企业失败的问题`,`sessionId=iss016-final-20260726-a`,`runId=3ab22ed7-d0ed-45d8-b928-dce5790c0542`;SSE 返回 `SAFE_FALLBACK`、`type=MISSING_REQUIRED_CONTEXT`、`done.outcome=FALLBACK`。
- 数据库核对:`status=SUCCESS`、`intent=DIAGNOSIS`、`release_outcome=FALLBACK`、`tool_call_count=0`、`total_token_count=2890`、answer 非空。
- Trace 核对:`RUN_STARTED -> ROUTING_ATTEMPT -> ROUTING_DECISION -> AGENT_MODEL_STEP -> EVIDENCE_GUARD_INITIAL -> RELEASE_DECISION/FALLBACK -> RUN_FINISHED/FALLBACK`。
- 兼容性:公开 HTTP/SSE 字段、前端 SafeFallback 消费结构、数据库表和业务 Tool request 均未新增字段;模型侧 Tool Envelope 是本变更已确认的 L3 协议变更。
## Apply Continuation: Task 8 Protocol Repair + Bounded Stop
- Checkpoint:Apply。
- Capability source:`openspec-apply-change` + sm-flow apply 协议。
- 背景:tasks 1–7 已完成;真实 E2E 暴露连续 `INVALID_PROGRESS_PROTOCOL` 不会累计 `NO_GAIN`,可能在硬预算前空转。Task 8 补齐协议修复反馈与独立兜底停止。
- 实现事实(代码已在工作区,本轮补齐测试与收口):
- `ProgressProtocolViolationType` / `ProgressProtocolViolationException` 覆盖 MISSING_PREVIOUS_OBSERVATION、OUT_OF_ORDER、UNEXPECTED、MISSING_INPUT、INVALID_ENVELOPE。
- `DiagnosisProgressTracker` 独立累计连续协议错误,默认阈值 2,达到后 `stop_reason=PROGRESS_PROTOCOL_VIOLATED`。
- `HarnessToolInterceptor` 返回可修正 observation(repair_required、violation_type、missing_field、expected_previous_tool_call_id、allowed_information_gain);达阈一次 STOP_REQUIRED,再请求抛 `DiagnosisCollectionStoppedException`。
- `DiagnosisReleaseUseCase` 支持 `PROGRESS_PROTOCOL_VIOLATED`:有安全 ProgressSnapshot 发 `INSUFFICIENT_EVIDENCE`,无进展 fail closed。
- `TOOL_REQUEST_REJECTED` 记录 violation_type、repair_prompt_delivered、consecutive_protocol_violations、stop_reason,不记录参数/观察正文/异常。
- 验证:
- Focused:`DiagnosisProgressTrackerTest`、`HarnessToolInterceptorTest`、`DiagnosisReleaseUseCaseTest`、`DiagnosisAgentUseCaseTest`、`HarnessChatConfigurationTest` 通过。
- OpenSpec strict validate 通过。
- 文档:ISS-016 剩余协议停止项勾选完成;ISS-015 阶段 1 标记已完成;架构文档同步协议错误独立停止语义。
- OpenSpec tasks 8.1–8.6 全部完成。剩余 Apply 工作:无。可进入 Archive checkpoint(需用户确认是否归档 OpenSpec)。
## Archive
- Checkpoint:Archive。
- Capability source:`sm-flow` archive 协议 + `openspec-archive-change`。
- 用户确认:明确要求“执行(archive),完后提交推送”。
- devflow 档案:
- `brief.md`、`evidence.md`、`decisions.md`、`acceptance.md`
- 更新 `devflow/index.md`、`devflow/glossary/CONTEXT.md`
- OpenSpec:
- delta specs 已同步到 main specs(含新建 `diagnosis-information-gain-stop-contract`)
- change 归档至 `openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract/`
- 不创建独立 ADR:决策已由 OpenSpec/ISS/架构文档承载,且可通过 OpenSpec 回滚。
- 状态:archived。
@@ -0,0 +1,38 @@
# Diagnosis 信息增益停止契约 Evidence
## 证据
| 来源 | 证据 | 结论 | 是否已汇报 |
| --- | --- | --- | --- |
| `HarnessEvidenceTools` / request records | Agent-facing Tool 直接使用业务 request 生成 Schema | 要增加 `previous_observation + input` 必须显式演进 Tool Schema | 是 |
| `HarnessToolInterceptor` | 曾把完整 `agentResult` 放入 Tool Response | 必须拆分控制视图与模型观察 | 是 |
| `RunContext` / `DiagnosisHarnessCore.startRun` | 结构不可变 + 线程安全 handle | 进展 tracker 放 RunContext,不扩 Redis 枚举 | 是 |
| `CanonicalInvocationStore` | 仅 begin/find/markReady/markError | tracker 只存 identity,结束时投影 | 是 |
| `RagResultProjector` | 只按 evidence 空否生成状态 | 需兼容 `relevanceLevel/relevance_level` | 是 |
| `DiagnosisReleaseUseCase` / `EvidenceGuard` | 强制 draft 非空且空 analysis=`ANALYSIS_MISSING` | 与合法无结论冲突,需统一 Release | 是 |
| `ChatApplicationUseCase.recoverBudgetExhaustion` | 未提交预算 Fallback 绕过 Release | 迁移意图到 Diagnosis Release | 是 |
| 前端 `app.js` | 已渲染 observed_facts/sources/limitations/next_steps | 不新增公开 SSE 字段 | 是 |
| 真实 E2E(实施前) | 多轮空转后 `BUDGET_EXHAUSTED`/`INTERNAL_FAILURE` | 需要信息增益停止契约 | 是 |
| 真实 E2E(实施后) | `iss016-final-20260726-a` → `MISSING_REQUIRED_CONTEXT` FALLBACK | 未知 Query 可正常业务结束 | 是 |
| Token/拒绝审计 E2E | 9 次 `INVALID_PROGRESS_PROTOCOL` 拒绝不累计 NO_GAIN | 需独立协议错误阈值与 STOP | 是 |
## Evidence-driven 结论
- 结论:Tool Envelope 是 L3 模型侧协议变更,业务 request 在 interceptor 解包后保持不变。
- 证据:三个 FunctionToolCallback inputType、adapter bridge 只收业务 JSON。
- 风险:框架 Schema/拦截器假设不匹配。
- 用户确认:不需要(技术事实)
- 结论:协议错误不得累计为 `NO_GAIN`,必须独立 `PROGRESS_PROTOCOL_VIOLATED`。
- 证据:真实审计 9 次协议拒绝 + 13 Agent 轮次;OpenSpec design 6.1。
- 风险:只返回通用错误码不足以自修复。
- 用户确认:已通过 Task 8 OpenSpec 与实现收口
- 结论:Release 是业务 Fallback 唯一决策入口;Application 不重建业务内容。
- 证据:`DiagnosisReleaseUseCase` 统一路径 + Application 窄化预算终态持久化。
- 用户确认:已确认
## 实现期补充证据
- Draft 合同失败窄化降级:非法 Draft 丢弃;仅当 ProgressSnapshot 有已验真 facts 时发 `INSUFFICIENT_EVIDENCE`。
- Task 8 focused tests:tracker / interceptor / release / agent-loop / config 全部通过。
@@ -0,0 +1,22 @@
# Acceptance
## Done
- `MilvusHybridKnowledgeStore`: schema BM25 function + dense, hybridSearch+RRFRanker, dense search
- `VectorSearchService` only routes dense|hybrid to V2 store
- `VectorIndexService` writes via V2 store
- Removed knowledge-path `MilvusServiceClient` bean wiring
- Config: `milvus.collection=biz_hybrid`, `retrieval.search.mode=hybrid`
## Tests
```text
mvn -Dtest=LookupKnowledgeToolTest,KnowledgeEvidencePostProcessorTest,RagResultProjectorTest,RrfFusionTest,VectorKnowledgeSearchAdapterHybridTest,VectorSearchServiceTest,VectorIndexServiceTest test
```
EXIT:0
## Ops note
Reindex all knowledge docs into `biz_hybrid` before production hybrid search is meaningful.
Legacy `biz` collection is unused by knowledge path.
@@ -0,0 +1,3 @@
# Brief: rag-bm25-hybrid-drop-sdk
True dense+BM25 hybrid on a single MilvusClientV2 backend. Legacy SDK search/write for knowledge path removed. New collection `biz_hybrid` requires reindex.
@@ -0,0 +1,7 @@
# Decisions
- Single backend: MilvusClientV2 only for knowledge RAG
- Drop sdk/spring/auto retrieval routing
- New collection biz_hybrid to avoid mutating legacy biz schema in place
- Hybrid = dense ANN + BM25 sparse ANN + RRFRanker
- Dense L2 enrichment for threshold compatibility on hybrid hits
@@ -0,0 +1,44 @@
# Acceptance: rag-chunk-evidence-identity-dedup
## 实现结果
Delivery 1 completed:
- chunk identity fields on candidates/evidence blocks
- `KnowledgeSearchPort` + dense adapter
- evidenceKey dedup + maxChunksPerDocument + return-n
- retrieve-k on lookup tool
- projector keeps same-source distinct chunks; `document_id` is chunk-scoped
## 验证
### 脚本验证
```text
mvn -q "-Dtest=LookupKnowledgeToolTest,KnowledgeEvidencePostProcessorTest,RagResultProjectorTest" test
```
结果:通过(exit 0)
### 静态验证
- Search/port wiring reviewed against OpenSpec tasks
- No new legacy SDK dependency added
### 浏览器/人工验证
未运行(纯检索契约变更,无 UI)
### 未验证
- 全量 harness E2E / 真实 Milvus 联调(Delivery 2 前可补)
- 生产配置默认 retrieve-k/return-n 调优
## 归档状态
- OpenSpec change ready to archive
- User pre-authorized archive for sm-flow staged delivery
## 后续
- Delivery 2: milvus hybrid search (separate change)
@@ -0,0 +1,29 @@
# Brief: rag-chunk-evidence-identity-dedup
## 背景
lookup_knowledge 同文档多 chunk 在后处理与投影阶段被 source 级去重吞掉。Hybrid 多路召回前必须先修证据身份与裁剪契约。
## 目标
- chunk 级 evidenceKey 身份
- 按 evidenceKey 去重 + 每文档 chunk 上限
- retrieve-k / return-n 分离
- Projector 保留同 source 不同 chunk
- 薄 KnowledgeSearchPort,为 Delivery 2 hybrid 铺路
## 范围
Delivery 1 only(见 `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1)。
## 非目标
hybrid schema、BM25、删 SDK、session dedup、邻块重建、模型 rerank。
## 分档
standard
## 关联 OpenSpec
`openspec/changes/rag-chunk-evidence-identity-dedup`
@@ -0,0 +1,102 @@
# Decisions: rag-chunk-evidence-identity-dedup
## Capability sources
- sm-flow orchestration
- OpenSpec fallback protocol (file-based propose/apply/archive) — external openspec-propose/apply skills used as reference; execution via sm-flow fallback
- grill: fallback built-in protocol
- audit: fallback built-in protocol
## Scale
standard
## Clarify
- Problem: same-document multi-chunk evidence collapsed by source-level dedup.
- Outcome: Delivery 1 foundation before hybrid Delivery 2.
- Slug: `rag-chunk-evidence-identity-dedup`
- User authorized apply + archive in advance for sm-flow staged changes.
## Context
- Read: `devflow/glossary/CONTEXT.md`, modular-rag-pipeline brief, `openspec/specs/rag-knowledge-retrieval`, `rag-log-projections`, checklist doc §1.1
- Constraints into OpenSpec:
- L0 hint-only remains
- Do not thicken legacy SDK path
- Agent tool name/input stable
- Hybrid out of scope this change
## Question pool (grill)
| # | Dimension | Mode | Question | Status |
|---|---|---|---|---|
| Q1 | 术语 | evidence-driven | evidenceKey / document_id 语义? | Resolved: evidenceKey=chunk id; projected document_id=evidenceKey |
| Q2 | 边界 | evidence-driven | Delivery 1 vs 2 边界? | Resolved: per checklist; no schema/hybrid/SDK delete |
| Q3 | 验收 | evidence-driven | 如何验收多 chunk? | Resolved: unit tests multi-chunk keep + projector |
| Q4 | 接口 | user-interview | document_id 改为 chunk 级是否可接受? | **Pre-authorized by user** via “apply/archive 直接授权” + prior design agreement on scheme A (document_id=evidenceKey). Recorded as accepted behavior change. |
| Q5 | 技术 | evidence-driven | SearchPort 是否本 change 必须? | Resolved: thin port required as foundation |
### Evidence-driven conclusions (reported)
1. Current collapse points: `KnowledgeEvidencePostProcessor.sourceKey` and `RagResultProjector` source fallback.
2. Metadata already has docId/chunkIndex on write path; not first-class on read path.
3. Existing main-spec still says source-level dedup — this change intentionally deltas that requirement.
### User-interview
- Q4 accepted under prior design alignment (scheme A) and explicit apply authorization for this sm-flow run. No remaining open product preference questions for Delivery 1.
## Audit
Module chain:
```text
LookupKnowledgeTool -> SearchPort -> Retriever -> PostProcessor -> Packer -> Assembler -> RagResultProjector
```
Risks:
1. Agent payload growth — mitigated by return-n + maxChunksPerDocument + projector budgets.
2. document_id semantic shift — documented L3 behavior change; tests updated.
3. Old data without chunkIndex — vector id fallback.
No ADR conflict with modular RAG L0/L1 boundary.
## Cross-artifact alignment
| From | To | Status |
|---|---|---|
| brief goals | proposal | 已对齐 |
| proposal scope | design decisions | 已对齐 |
| design identity/dedup/port | specs | 已对齐 |
| specs scenarios | tasks | 已对齐 |
## Interface impact
- L2 internal DTO
- L3 Agent `document_id` chunk-scoped
## Commit gate
- proposal/design/specs/tasks present
- no open user-interview blockers for Delivery 1
- apply authorized by user at sm-flow start
## Pre-apply research
Reference files:
- `LookupKnowledgeTool.java`
- `KnowledgeDocumentRetriever.java`
- `KnowledgeEvidencePostProcessor.java`
- `RagResultProjector.java`
- `LookupKnowledgeToolTest.java`
- `RagResultProjectorTest.java`
- `mvp/engineering/rag/Milvus-Hybrid接入清单.md`
Stack notes:
- No MQ/request envelope changes
- Spring `@Value` config pattern for rag.* keys
- Tests use ReflectionTestUtils + Mockito
@@ -0,0 +1,16 @@
# Evidence: rag-chunk-evidence-identity-dedup
## 代码证据(变更前)
- `KnowledgeEvidencePostProcessor.sourceKey` 使用 source/title 去重
- `RagResultProjector` 用 source 回退 document_id 并 HashSet 去重
- 写入路径 metadata 已有 docId/chunkIndex,读路径未一等化
## 规格证据
- 旧 `openspec/specs/rag-knowledge-retrieval` 要求 source 级 dedup(本 change 以 delta 修正)
- `mvp/engineering/rag/Milvus-Hybrid接入清单.md` §1.1 定义 Delivery 1 地基
## 验证证据
- 单测覆盖 multi-chunk keep / true-dup merge / maxChunksPerDocument / projector same-source multi-chunk
@@ -0,0 +1,23 @@
# Acceptance: rag-hybrid-search-rrf
## Result
Hybrid mode implemented on KnowledgeSearchPort:
- dense unfiltered + dense filtered + lexical rank over union
- RRF fusion by evidenceKey
- dense-compatible score preserved for thresholds
- default mode remains dense
## Verification
```text
mvn -q "-Dtest=RrfFusionTest,VectorKnowledgeSearchAdapterHybridTest,LookupKnowledgeToolTest" test
```
Pass.
## Residual
- True Milvus BM25/sparse schema + reindex still follow-up
- Lexical path only ranks dense-recalled candidates (does not expand pure-term misses outside dense topK)
@@ -0,0 +1,3 @@
# Brief: rag-hybrid-search-rrf
Delivery 2 after chunk identity. Enable hybrid multi-path + RRF on KnowledgeSearchPort without legacy SDK hybrid API. True BM25 schema rebuild is staged follow-up; this change ships sparse-lite lexical ranking over dense candidate union + filtered/unfiltered dense fusion.
@@ -0,0 +1,19 @@
# Decisions: rag-hybrid-search-rrf
## Capability
sm-flow + OpenSpec fallback; apply pre-authorized.
## Depends
Delivery 1 archived.
## Grill (compressed, pre-authorized)
- Q: Full BM25 schema now? A: No — sparse-lite + RRF first; schema rebuild follow-up.
- Q: Default mode? A: dense default; hybrid opt-in.
- Q: Threshold score? A: keep dense-compatible L2 mapping.
## Design
Hybrid paths: dense unfiltered + dense filtered + lexical rank over union; RRF fuse by evidenceKey.
@@ -0,0 +1,5 @@
# Evidence
- Delivery 1 identity/port foundation required
- RRF utility and hybrid adapter unit tests green
- Lexical sparse-lite intentionally intermediate until BM25 schema
@@ -0,0 +1,39 @@
# Acceptance: rag-eval-hybrid-baseline
## Tasks
All tasks in OpenSpec `tasks.md` checked, including apply-discovered 6.x quality-gate fix.
## 静态验证
- Snapshot generator path: no required `retrieval.vector-store.mode`.
- README documents hybrid generation and offline/live split.
## 脚本验证
```text
.\scripts\prepare_rag_eval_seed.ps1
.\scripts\generate_rag_lookup_snapshots.ps1 -SearchMode hybrid -SkipEval
python scripts\eval_rag_retrieval.py --json-report eval/rag-retrieval/reports/baseline.json --markdown-report eval/rag-retrieval/reports/baseline.md
# Result: Evaluated 7 cases: passRate=1.0, recall@5=1.0, failed=0
mvn -Dtest=RetrievalScoreNormalizerTest,KnowledgeEvidencePostProcessorTest,LookupKnowledgeToolTest,VectorSearchServiceTest,VectorKnowledgeSearchAdapterHybridTest test
# exit 0
```
Fixture sample meta: `searchMode=hybrid`, `kbScope=rag-eval`.
Fallback case: `UNFILTERED_VECTOR_RETRY` + `filtered_vector_low_quality`.
## 浏览器/人工
- 未做 UI 验证。
## 未验证 / 后续
- Dense vs hybrid dual-directory comparison report (knife-2).
- CI wiring of offline eval as required gate (optional process).
- Long-term calibration of hybrid PRECISE distribution under denseDistance quality.
## Specs
Main spec synced: `openspec/specs/rag-eval-offline-baseline/spec.md`.
@@ -0,0 +1,16 @@
# Brief: rag-eval-hybrid-baseline
## Background
Offline RAG eval (golden × fixture × key-field baseline) existed but generator/docs still used dead `retrieval.vector-store.mode=spring`. Fixtures lacked search meta and did not reflect hybrid main path.
## Goals (knife-1 only)
- Snapshot generation uses `retrieval.search.mode` (default hybrid; dense override).
- Fixtures record `searchMode` / `kbScope`.
- README documents hybrid-era offline vs live loop.
- Best-effort live seed + regenerate fixtures + update baseline.
## Non-goals
Dense/hybrid dual fixture trees; golden mustNot/chunk/level hard gates; new eval frameworks.
@@ -0,0 +1,18 @@
# Decisions: rag-eval-hybrid-baseline(最终版)
## Process
sm-flow standard-lean: Discover → Commit → Apply → Archive.
## Key decisions
1. Replace eval generator `vector-store.mode` with `retrieval.search.mode` (default hybrid).
2. Fixture meta: `searchMode`, `kbScope` when set.
3. Knife-2 (dual fixtures / mustNot golden) deferred.
4. Live refresh succeeded in apply env; baseline updated to hybrid snapshots.
5. **Quality gate refinement (apply-found):** hybrid absolute quality for `isLowQuality` / relevance uses optional dense L2 (`denseDistance`); does not overwrite hybrid scoreLabel or RRF order. Rank mapping remains fallback when dense missing.
## Trade-offs
- Extra dense ANN on hybrid path for gate calibration (latency) vs correct filter-fallback behavior.
- relevance_level still not a hard golden assertion (ordinal vs absolute mix).
@@ -0,0 +1,17 @@
# Evidence: rag-eval-hybrid-baseline
## Pre-change
- `generate_rag_lookup_snapshots.ps1` passed `-Dretrieval.vector-store.mode=spring`.
- Fixtures had `caseId/query/retrievedAt/lookupResult` only.
- Offline eval already supported Hit levels, recall@K, baseline diff.
## User decisions
- Scope: knife-1 only (no dual fixture dirs).
- Acceptance: wiring required; fixture refresh best-effort (env allowed full refresh).
## Apply-discovered
- After hybrid refresh, `chat-l0-filter-fallback` failed: pure rank→quality made topSimilarity=1.0 on decoy-only filtered hits → no unfiltered retry.
- Fix: optional `denseDistance` on hybrid hits; quality gate uses L2 when present; sort order remains RRF.
@@ -0,0 +1,41 @@
# Acceptance: rag-quality-score-unify
## Tasks
OpenSpec `tasks.md` 全部 `[x]`(1.1–6.2)。
## 静态验证
- 生产路径 grep:无 `bm25_only_no_dense` 发射、无 hybrid L2 enrichment(仅 Labels canonicalize 兼容旧串)。
- 架构文档 §6 与 `application.yml` 注释已对齐 quality 契约。
## 脚本验证
```text
mvn -Dtest=RetrievalScoreNormalizerTest,KnowledgeEvidencePostProcessorTest,LookupKnowledgeToolTest,VectorSearchServiceTest,VectorKnowledgeSearchAdapterHybridTest test
```
| 套件 | 结果 |
|---|---|
| RetrievalScoreNormalizerTest | 4 passed |
| KnowledgeEvidencePostProcessorTest | 6 passed |
| LookupKnowledgeToolTest | 7 passed |
| VectorSearchServiceTest | 2 passed |
| VectorKnowledgeSearchAdapterHybridTest | 1 passed |
(PowerShell 可能将 JVM warning 标为 exit 1;日志中为 BUILD SUCCESS / Failures: 0。)
## 浏览器 / 人工验证
- 未跑:live `lookup_knowledge` hybrid vs dense 对照、生产阈值标定。
## 未验证
| 项 | 风险 | 建议 |
|---|---|---|
| 真实 Milvus hybrid 联调 | 序/质量分布与单测 mock 有差 | 启动服务后固定 query 集切 mode 对比 |
| 阈值 0.75/0.5 在 hybrid rank 分下的标定 | retry/PRECISE 偏多或偏少 | 看 trace topSimilarity 再调 yml |
## Specs 同步
- 主规格新增:`openspec/specs/rag-retrieval-quality-score/spec.md`(archive 时从 delta 同步)。
@@ -0,0 +1,21 @@
# Brief: rag-quality-score-unify
## Background
真 BM25 hybrid(dense + BM25 + RRF)已上线,但后处理仍把 hybrid 结果伪装成 L2 做 `normalizeL2`,并用 L0 domain/entity/keyword contains 加分改序。排序权威与质量闸门分裂,词面信号被 BM25 与后处理双重计分。
## Goals
- 一级 `scoreLabel` 仅 `dense` | `hybrid`
- 唯一 `toQualityScore`;后处理 label-agnostic
- 排序主序 = 检索 `originalRank`;去掉关键词 boost 改序
- hybrid quality = 本轮 rank 纯映射(不做 max(rank, denseSim)、不为闸门回填 L2)
- 保留 `mode=dense` 作同库召回对照;线上默认 hybrid
## Scope
内部 RAG:store 发射、normalizer、evidence post-process、单测、架构文档 §6。
## Non-goals
精排 / query rewrite / 邻块、schema rebuild、改 Agent ACI 字段名、删除 dense 对照 mode。
@@ -0,0 +1,25 @@
# Decisions: rag-quality-score-unify(最终版)
## Scale / process
- sm-flow standard:Discover → Commit → Apply → Archive
- Committed OpenSpec:`openspec/changes/rag-quality-score-unify/`(归档后见 archive 目录)
- 废止:`rag-bm25-hybrid-drop-sdk` 中「dense L2 enrichment for threshold compatibility」
## Key decisions
1. **Label**:仅 `dense` | `hybrid`;旧别名 canonicalize。
2. **Normalizer**:唯一 `toQualityScore`;dense=L2 公式;hybrid=rank 线性映射(batchSize)。
3. **Store**:hybrid 不回填 L2、不发 `bm25_only_*`;返回序即 RRF 序。
4. **Post-process**:`originalRank` ASC;L0 重叠只写 hitReasons;relevance/low-quality 只看 qualityScore;PRECISE 不要求 hint support。
5. **Mode**:hybrid 主路径;dense 同库对照(架构 §6.0)。
## Trade-offs
- hybrid quality 为序数分,跨 query 绝对值不可比;阈值可能需后续标定。
- 去掉 boost 改序后,「词面热语义冷」不再被后处理抬升;词面交给 BM25+RRF。
## Risks accepted
- `relevance_level` / unfiltered retry 分布变化(产品已接受)。
- 未做 live E2E / 人工 hybrid 对照评测(见 acceptance 未验证项)。
@@ -0,0 +1,25 @@
# Evidence: rag-quality-score-unify
## Code (pre-change)
- `MilvusHybridKnowledgeStore.searchHybrid`:RRF 后并行 dense 回填 L2;BM25-only → `bm25_only_no_dense` + maxL2。
- `KnowledgeEvidencePostProcessor`:一律 `normalizeL2(score)` + domain/entity/keyword/source_type 加分,按 `finalScore` 降序;PRECISE 需 `hasHintSupport`。
## User decisions (grill)
| ID | 结论 |
|---|---|
| Q1 | 一级 label 仅 dense/hybrid;bm25_only 不作正式 label |
| Q2 | 后处理去掉 contains 加分改序,保 originalRank |
| Q3 | 唯一 toQualityScore;后处理统一 |
| Q4 | dense mode 保留作对照 |
| Q7 | 接受 relevance_level / retry 分布变化 |
| Q8 | hybrid quality = **纯 rank 映射** |
## Post-change anchors
- `RetrievalScoreLabels` / `RetrievalScoreNormalizer`
- `MilvusHybridKnowledgeStore`(无 L2 overwrite / 无 bm25_only 发射)
- `KnowledgeEvidencePostProcessor`(rank sort + explain-only L0 overlap)
- OpenSpec delta:`rag-retrieval-quality-score`
- 架构:`mvp/architecture/RAG知识检索架构.md` §6
+2 -58
View File
@@ -40,68 +40,12 @@ services:
timeout: 5s
retries: 5
# Milvus 向量数据库(Standalone 模式)
# 注意:生产环境建议使用 Zilliz Cloud 或 Milvus 集群
etcd:
image: quay.io/coreos/etcd:v3.5.5
container_name: superbiz-etcd
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- etcd-data:/etcd
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
container_name: superbiz-minio
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- minio-data:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
milvus:
image: milvusdb/milvus:v2.3.3
container_name: superbiz-milvus
depends_on:
- etcd
- minio
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- milvus-data:/var/lib/milvus
ports:
- "19530:19530"
- "9091:9091"
command: ["milvus", "run", "standalone"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
# 向量检索与知识入库由独立的 py-rag 服务承担(见 py-rag 仓库),
# 其依赖的 Milvus/etcd/MinIO 随 py-rag 部署,不再由本 compose 管理。
volumes:
mysql-data:
redis-data:
etcd-data:
minio-data:
milvus-data:
networks:
default:
+58 -91
View File
@@ -1,108 +1,75 @@
# 文档索引
## 📂 目录结构
**更新日期**:2026-07-29
```
## 目录结构
```text
docs/
├── README.md # 项目文档总览
├── INDEX.md # 本索引文件
│
├── learning/ # 📚 学习笔记(个人学习理解)
│ ├── 00-项目学习路径.md
│ ├── 01~08-*.md # 按学习顺序编号
│ └── README.md
│
├── analysis/ # 🔍 分析笔记(代码/问题分析)
│ ├── essence-report-*.md
│ ├── explore-report.md
│ ├── chunking-issues-analysis.md
│ └── 功能分析报告.md
│
├── reports/ # 📝 临时报告(修复/验证报告)
│ ├── 修复报告-*.md
│ ├── 验证报告-*.md
│ └── 日志配置完成总结.md
│
└── guides/ # 📖 指南文档
└── 日志配置与分析指南.md
├── INDEX.md # 本索引
├── learning/ # 早期学习笔记(可能过时)
├── analysis/ # 早期代码/问题分析
├── reports/ # 历史修复/验证报告
└── guides/ # 操作指南
mvp/ # 现行 MVP 文档(主入口)
├── architecture/ # 现行架构:系统现在怎么跑
├── engineering/ # 工程纪要:问题 / 决策 / E2E
├── issues/ # 未完成事项
├── tables/ # 表结构
├── demo/ # Demo
└── eval/ # 诊断评测材料
```
**⚠️ 注意:MVP 架构设计文档已移至项目根目录 `../mvp/`**
查看 [mvp/README.md](../mvp/README.md) 了解 MVP 架构、数据库设计、实施计划等。
**现行架构、工程纪要、表结构、Issue 均以 [mvp/README.md](../mvp/README.md) 为准。**
本目录 `learning/` / `analysis/` / `reports/` 偏早期学习与历史记录,**可能与当前实现不一致**。
---
## 🚀 快速导航
## 快速导航
### 我是新人/学习者
1. [项目学习路径](learning/00-项目学习路径.md) - 从这里开始
2. [learning/README.md](learning/README.md) - 学习笔记索引
3. 按编号顺序阅读 `learning/` 目录下的文档
### 我是开发者(优先)
### 我是开发者
👉 **MVP 架构设计文档已移至 `../mvp/`**
1. [mvp/README.md](../mvp/README.md) — MVP 总入口
2. [mvp/architecture/](../mvp/architecture/) — 现行架构
3. [mvp/engineering/](../mvp/engineering/) — 工程纪要(RAG / 诊断 E2E)
4. [mvp/issues/](../mvp/issues/) — 活跃 Issue
请查看 [mvp/README.md](../mvp/README.md) 了解:
- MVP 架构设计
- 数据库设计和表结构
- 实施规划(Phase 1/2/3)
- 会话管理设计
### RAG 工程纪要(已迁入 mvp)
### 我要查看分析报告
1. [分析笔记目录](analysis/) - 代码分析和问题分析
2. [临时报告目录](reports/) - 修复和验证报告
1. [RAG 排序:多路召回与 RRF](../mvp/engineering/rag/RAG排序-多路召回与RRF.md)
2. [Hybrid 之后的 qualityScore 与后处理](../mvp/engineering/rag/RAG-Hybrid质量分与后处理.md)
3. [Agent 如何读 relevance_level](../mvp/engineering/rag/RAG-Agent如何读relevance_level.md)
4. [RAG 离线评测:基线设计](../mvp/engineering/rag/RAG离线评测-基线设计.md)
5. [Milvus hybrid 接入清单](../mvp/engineering/rag/Milvus-Hybrid接入清单.md)
6. [RAG 审计补丁 E2E](../mvp/engineering/rag/RAG审计补丁-stepid-query-E2E验收.md)
7. 架构对照:[RAG 知识检索架构](../mvp/architecture/RAG知识检索架构.md)、[RAG 检索可观测性与审计](../mvp/architecture/RAG检索可观测性与审计.md)
### 诊断全流程(已迁入 mvp)
1. [一次诊断到底发生了什么](../mvp/engineering/diagnosis/一次诊断全流程-E2E导读.md)
### 我是新人 / 想看早期学习笔记
1. [项目学习路径](learning/00-项目学习路径.md)
2. [learning/README.md](learning/README.md)
3. 注意:内容可能过时,实现以 `mvp/architecture` 为准
### 分析 / 历史报告 / 指南
- [analysis/](analysis/) — 早期分析
- [reports/](reports/) — 历史修复与验证报告
- [guides/日志配置与分析指南.md](guides/日志配置与分析指南.md)
---
## 📚 学习笔记 (learning/)
## 文档维护
按学习顺序编号,建议按顺序阅读:
1. [00-项目学习路径](learning/00-项目学习路径.md)
2. [01-AI-Ops-核心设计-Essence报告](learning/01-AI-Ops-核心设计-Essence报告.md)
3. [02-outputKey-深度解析](learning/02-outputKey-深度解析.md)
4. [03-核心疑问解答](learning/03-核心疑问解答.md)
5. [04-RAG-分块策略-Essence报告](learning/04-RAG-分块策略-Essence报告.md)
6. [05-文件上传自动索引-Essence报告](learning/05-文件上传自动索引-Essence报告.md)
7. [06-RAG查询流程-Essence报告](learning/06-RAG查询流程-Essence报告.md)
8. [07-Tool定义方式对比与优化](learning/07-Tool定义方式对比与优化.md)
9. [08-MethodToolCallback-vs-ToolCallingManager深度分析](learning/08-MethodToolCallback-vs-ToolCallingManager深度分析.md)
---
## 🔍 分析笔记 (analysis/)
代码分析和问题分析文档:
- [essence-report-rag.md](analysis/essence-report-rag.md)
- [essence-report-rag-chunking.md](analysis/essence-report-rag-chunking.md)
- [explore-report.md](analysis/explore-report.md)
- [chunking-issues-analysis.md](analysis/chunking-issues-analysis.md)
- [功能分析报告.md](analysis/功能分析报告.md)
---
## 📝 临时报告 (reports/)
修复报告和验证报告:
- [修复报告-多轮对话时间查询缓存问题](reports/修复报告-多轮对话时间查询缓存问题.md)
- [验证报告-时间查询问题](reports/验证报告-时间查询问题.md)
- [日志配置完成总结](reports/日志配置完成总结.md)
---
## 📖 指南文档 (guides/)
- [日志配置与分析指南](guides/日志配置与分析指南.md)
---
## 🔄 文档维护
- **学习笔记** 放在 `learning/` 目录,按编号顺序命名
- **分析笔记** 放在 `analysis/` 目录
- **临时报告** 放在 `reports/` 目录
- **指南文档** 放在 `guides/` 目录
- **MVP 架构设计** 已移至项目根目录 `../mvp/`(包含架构、数据库、实施计划)
| 类型 | 位置 |
|---|---|
| 现行架构 | `mvp/architecture/` |
| 工程纪要(问题/决策/E2E) | `mvp/engineering/` |
| Issue | `mvp/issues/` |
| 表结构 | `mvp/tables/` |
| 早期学习 | `docs/learning/`(归档向,不充当现行规范) |
| 操作指南 | `docs/guides/` |
+89 -143
View File
@@ -1,38 +1,65 @@
# RAG Retrieval Baseline
This directory contains the offline retrieval baseline for the RAG refactor.
Offline regression harness for `lookup_knowledge` **after** hybrid retrieval + qualityScore post-process.
The baseline is intentionally narrower than full diagnosis evaluation. It checks
whether fixed retrieval queries can recover expected documents, breadcrumbs, and
evidence keywords before changing L0 behavior, query augmentation, evidence
post-processing, or Spring AI VectorStore integration.
It checks whether fixed queries still recover expected documents, breadcrumbs, keywords, and pipeline behaviors (filter / unfiltered retry). It is **not** a full diagnosis-agent E2E.
Production knowledge path: `MilvusHybridKnowledgeStore` with `retrieval.search.mode=hybrid` (dense+BM25+RRF).
`mode=dense` remains a same-collection baseline for recall comparison (not a second index).
Related design notes:
- `mvp/engineering/rag/RAG-Hybrid质量分与后处理.md`
- `mvp/engineering/rag/RAG-Agent如何读relevance_level.md`
- `mvp/architecture/RAG知识检索架构.md` §6
## Offline vs live
| Layer | What | Needs live stack? |
|-------|------|-------------------|
| **Offline** | `fixtures/*.json` × `golden-cases.json` → pass/fail + baseline diff | **No** (no Milvus/LLM/Boot) |
| **Snapshot generate** | Real `LookupKnowledgeTool` writes fixtures | **Yes** (embedding + Milvus + DB/L0 as configured) |
| **Live smoke** | optional `eval_rag_live_acceptance.py` | Yes (running app) |
Daily CI / local quick check: **offline only**.
After changing retrieval, indexing, or search mode: **regenerate fixtures**, then offline eval, then update baseline if the diff is intentional.
## Layout
```text
eval/rag-retrieval/
cases/golden-cases.json Fixed retrieval golden cases
seed-docs/*.md Canonical docs imported into the live KB for real-tool eval
fixtures/*.json Saved retrieval fixtures for each case
reports/baseline.json Machine-readable baseline report
reports/baseline.md Human-readable baseline report
reports/baseline-diff.* Optional diff reports
reports/live-post-reindex.* Optional live acceptance reports
cases/golden-cases.json Fixed queries + expectations
seed-docs/*.md Canonical docs for live snapshot (kb_scope: rag-eval)
fixtures/*.json Frozen lookupResult snapshots (+ searchMode meta)
reports/baseline.json|md Last accepted offline report
reports/baseline-diff.* Optional diff vs previous report
```
## Seed Docs + Import/Reindex
## Fixture shape (minimum)
The live-tool eval uses canonical seed documents so the real
`LookupKnowledgeTool` can retrieve stable evidence from MySQL/Milvus instead of
whatever ad hoc documents happen to exist in the local knowledge base.
```text
caseId
query
retrievedAt
searchMode # hybrid | dense (required on newly generated fixtures)
kbScope # e.g. rag-eval when generation used a scope
lookupResult # found, evidenceBlocks, contextPack, retrievalTrace, rerankTrace, …
```
Seed documents live in:
Offline eval **ignores unknown top-level meta** and does **not** full-JSON-compare.
It asserts golden key fields only (doc/source, keywords, attempt, fallback, …). Raw scores are not pass criteria.
Older fixtures may omit `searchMode`; regenerate to attach meta.
## Seed docs + import
Live snapshot generation should use seed docs so results do not depend on ad-hoc local KB junk:
```text
eval/rag-retrieval/seed-docs/*.md
```
Each seed doc uses frontmatter fields that are propagated into vector metadata:
Frontmatter example:
```yaml
source: mysql-connection-pool
@@ -40,40 +67,29 @@ breadcrumb: Database > MySQL > Connection Pool
kb_scope: rag-eval
```
Import or reindex the seed docs through the real upload pipeline:
Import via real upload pipeline:
```powershell
.\scripts\prepare_rag_eval_seed.ps1
```
The script runs `RagEvalSeedImporterTest` with `rag.seed.enabled=true`. It
deletes the existing document with the same `source`/`docId`, uploads the seed
doc through `DocumentManagementService`, updates DB metadata and L0, and rebuilds
Milvus chunks.
Isolation:
`kb_scope` isolates eval data:
- App default may leave `retrieval.kb-scope` empty (all docs).
- Eval generation passes `-Dretrieval.kb-scope=rag-eval`.
- Category-filter fallback retries without L0 category filter only; **kb_scope still applies**.
- default application config leaves `retrieval.kb-scope` empty, so legacy docs
without `kb_scope` remain searchable;
- eval scripts pass `-Dretrieval.kb-scope=rag-eval`, so L0 query hints and L1
vector retrieval both use only the canonical eval seed docs;
- the fallback retry skips only the L0 category filter, not the `kb_scope`
boundary.
Body is chunked/embedded; frontmatter feeds metadata/L0 (decoy keywords in frontmatter alone should not become dense content).
Frontmatter is not embedded as chunk content during upload. It feeds metadata,
L0, and document enrichment; only the Markdown body is chunked and embedded.
This keeps controlled L0 decoys from becoming semantically relevant just because
their frontmatter keywords matched the query.
Seeds must live in the **current hybrid collection schema** (`milvus.collection`, default `biz`). If the collection was recreated for BM25 hybrid, re-import seeds after rebuild.
## Run
From the repository root:
## Offline run (no live stack)
```bash
python scripts/eval_rag_retrieval.py
```
Custom paths are also supported:
Custom paths:
```bash
python scripts/eval_rag_retrieval.py \
@@ -83,83 +99,53 @@ python scripts/eval_rag_retrieval.py \
--markdown-report eval/rag-retrieval/reports/baseline.md
```
## Generate Fixtures From LookupKnowledgeTool
Use the snapshot generator when fixtures should reflect the real
`LookupKnowledgeTool` pipeline:
```powershell
.\scripts\generate_rag_lookup_snapshots.ps1
```
For the intended live loop, run seed import first:
## Generate fixtures (live stack)
```powershell
.\scripts\prepare_rag_eval_seed.ps1
.\scripts\generate_rag_lookup_snapshots.ps1
python scripts\eval_rag_retrieval.py
# default: SearchMode=hybrid, KbScope=rag-eval, then offline eval
```
The script runs a Spring test harness:
```text
mvn -q -Dtest=RagLookupSnapshotGeneratorTest -Drag.snapshot.enabled=true -Dretrieval.kb-scope=rag-eval -Dretrieval.vector-store.mode=spring test
```
The generator reads `golden-cases.json`, injects the real `LookupKnowledgeTool`
bean, calls `lookupKnowledge(query)` for each case, writes
`fixtures/{caseId}.json`, and then runs `eval_rag_retrieval.py` unless
`-SkipEval` is provided. It defaults to Spring AI VectorStore mode; pass
`-VectorStoreMode sdk` only when intentionally comparing the legacy SDK path.
Custom paths are supported:
Dense baseline snapshot (same seed, comparison only):
```powershell
.\scripts\generate_rag_lookup_snapshots.ps1 `
-Cases eval\rag-retrieval\cases\golden-cases.json `
-Fixtures eval\rag-retrieval\fixtures `
-RetrievedAt 2026-07-06T00:00:00Z
.\scripts\generate_rag_lookup_snapshots.ps1 -SearchMode dense -Fixtures eval\rag-retrieval\fixtures-dense -SkipEval
```
The generator is disabled in normal test runs. It only executes when
`rag.snapshot.enabled=true` is provided because it writes repository files and
depends on the configured runtime retrieval stack.
(Dual-directory comparison reports are optional / future; knife-1 only documents the override.)
If generated fixtures fail the offline baseline, treat that as a real alignment
signal: either the golden expectations need to be adjusted to the current
knowledge base, or the knowledge base/indexing path needs to be fixed.
## Modular RAG Contract
Fixtures must use the current `lookupResult` shape, which mirrors the
`lookup_knowledge` output:
Maven equivalent:
```text
lookupResult.evidenceBlocks
lookupResult.contextPack
lookupResult.retrievalTrace
lookupResult.rerankTrace
mvn -q -Dtest=RagLookupSnapshotGeneratorTest \
-Drag.snapshot.enabled=true \
-Dretrieval.kb-scope=rag-eval \
-Dretrieval.search.mode=hybrid \
test
```
Golden cases can assert both retrieval quality and pipeline behavior:
Generator is **off** in normal tests; only runs when `rag.snapshot.enabled=true` (writes files).
If generated fixtures fail offline golden checks: either fix retrieval/index, or update golden/baseline **with an explicit reason** — do not silently overwrite.
## Golden assertions
Supported expectation fields include:
- `expectedSources` / `expectedDocIds`
- `expectedBreadcrumbs`
- `expectedKeywords`
- `expectedBreadcrumbs` / `expectedKeywords`
- `expectedSelectedAttempt`
- `expectedFallbackReason`
- `expectedFallbackReasons`
- `expectedFallbackReason` / `expectedFallbackReasons`
- `expectedEvidenceStatus`
- `expectedContextSources`
- `expectedRerankTopSource`
This lets the baseline catch regressions such as losing the expected evidence
source, skipping context packing, changing the selected retrieval attempt, or
breaking the filtered-vector to unfiltered-retry fallback.
Catch regressions such as missing expected source, broken context pack sources, wrong selected attempt, or broken filtered → unfiltered retry.
## Baseline Diff
**Note:** `relevance_level` is not a hard golden gate here (hybrid quality is rank-ordinal; see agent relevance-level doc).
To compare a freshly generated report against an existing baseline:
## Baseline diff
```bash
python scripts/eval_rag_retrieval.py \
@@ -170,64 +156,24 @@ python scripts/eval_rag_retrieval.py \
--diff-markdown-report eval/rag-retrieval/reports/baseline-diff.md
```
The diff reports aggregate regressions and case-level changes for:
Diff covers pass rate, recall@K, hit level, first expected rank, attempt, fallback, evidence status, rerank top source.
Non-zero exit on case failure or regression in diff mode.
- pass rate, recall@K, strong hit rate, miss count
- pass state
- hit level
- first expected rank
- selected attempt
- fallback reason
- evidence status
- rerank top source
## Hit levels
The command exits non-zero when a case fails or the diff contains a regression.
- `strong`: expected document found **and** breadcrumb or keyword coverage OK
- `medium`: expected document found, coverage incomplete
- `weak`: keyword hit without expected document
- `miss`: neither
## Hit Levels
`Recall@K` counts `strong` + `medium`.
- `strong`: expected document is found and breadcrumb or evidence keyword coverage is satisfied.
- `medium`: expected document is found, but breadcrumb or keyword coverage is incomplete.
- `weak`: expected evidence keyword is found, but expected document is missing.
- `miss`: expected document and expected evidence are not found.
## Optional live smoke (post-reindex)
`Recall@K` counts `strong` and `medium` as retrieved.
## Scope
This baseline runs fully offline and does not call MySQL, Redis, Milvus, an LLM,
or the Spring Boot application. It is a regression harness for retrieval behavior,
not a claim that live production retrieval accuracy is complete.
## Live Post-Reindex Acceptance
When embedding input changes, existing vectors do not update by themselves. For
example, after adding `title` and `breadcrumb` to the embedding text, the live
Milvus/Zilliz collection must be reindexed before retrieval can reflect that new
semantic signal.
Use this optional live acceptance flow after the application is running and the
knowledge base has been reindexed:
After reindex, with app up:
```bash
python scripts/eval_rag_live_acceptance.py
python scripts/eval_rag_live_acceptance.py --base-url http://127.0.0.1:9900
```
Custom service URL and output paths are supported:
```bash
python scripts/eval_rag_live_acceptance.py \
--base-url http://127.0.0.1:9900 \
--json-report eval/rag-retrieval/reports/live-post-reindex.json \
--markdown-report eval/rag-retrieval/reports/live-post-reindex.md
```
The script calls:
```text
GET /api/search/similar
```
It writes JSON and Markdown reports with query, topK, result count, top
results, breadcrumb, score labels, and raw response fields. This is a live
smoke check for environment readiness and post-reindex behavior; it does not
replace the deterministic offline baseline above.
Calls `GET /api/search/similar`. Environment smoke only — does **not** replace offline baseline.
@@ -1,81 +1,122 @@
{
"caseId": "aiops-payment-latency-alert",
"query": "Alert HighLatency on payment-service with p95 latency above threshold",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "payment-service-latency",
"title": "Payment Service Latency Alert Playbook",
"breadcrumb": "AIOps > Service Alerts > Payment Latency",
"retrievalLayer": "L1",
"content": "For payment-service p95 latency alerts, check downstream dependency latency, thread pool saturation, gateway retries, and recent deployment changes.",
"score": 0.84,
"hitReasons": ["domain_match:+0.15", "entity_match:+0.20", "keyword_match:+0.10"]
"caseId" : "aiops-payment-latency-alert",
"query" : "Alert HighLatency on payment-service with p95 latency above threshold",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "payment-service-latency",
"chunkIndex" : 2,
"evidenceKey" : "payment-service-latency#chunk-2",
"source" : "payment-service-latency",
"title" : "Payment Latency",
"breadcrumb" : "AIOps > Service Alerts > Payment Latency",
"retrievalLayer" : "L1",
"content" : "### Payment Latency\n\nFor `HighLatency` alerts on `payment-service`, treat p95 latency as the primary symptom.\n\nDiagnosis steps:\n\n1. Confirm whether p95 latency is isolated to payment-service or shared across upstream callers.\n2. Compare payment-service latency with downstream dependency latency for gateway, risk, and order services.\n3. Check connection pool wait time, retry spikes, and timeout rates.\n4. If downstream dependency latency increased first, classify payment-service as affected rather than root cause.\n\nThe expected evidence terms are p95 latency, payment-service, and downstream dependency.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "aiops-alert-scope-control",
"chunkIndex" : 1,
"evidenceKey" : "aiops-alert-scope-control#chunk-1",
"source" : "aiops-alert-scope-control",
"title" : "Alert Scope Control",
"breadcrumb" : "AIOps > Alert Scope Control",
"retrievalLayer" : "L1",
"content" : "## Alert Scope Control\n\nWhen an AIOps request already includes an alert payload, the agent should diagnose that payload first.\nIt must not expand the task into unrelated active alerts unless the user asks for broad alert triage.\n\nScope rules:\n\n1. Treat the provided payload as the primary incident boundary.\n2. Use unrelated active alerts only as correlation evidence when they share service, dependency, time window, or trace context.\n3. Do not replace the requested alert with a louder but unrelated alert.\n\nThis runbook anchors payload, unrelated active alerts, and scope behavior.",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "payment-service-latency",
"chunkIndex" : 1,
"evidenceKey" : "payment-service-latency#chunk-1",
"source" : "payment-service-latency",
"title" : "Service Alerts",
"breadcrumb" : "AIOps > Service Alerts > Payment Latency",
"retrievalLayer" : "L1",
"content" : "## Service Alerts",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:3", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "aiops-alert-scope-control",
"chunkIndex" : 0,
"evidenceKey" : "aiops-alert-scope-control#chunk-0",
"source" : "aiops-alert-scope-control",
"title" : "AIOps",
"breadcrumb" : "AIOps > Alert Scope Control",
"retrievalLayer" : "L1",
"content" : "# AIOps",
"score" : 0.015384615398943424,
"hitReasons" : [ "semantic_rank:5", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: payment-service-latency\ntitle: Payment Latency\nbreadcrumb: AIOps > Service Alerts > Payment Latency\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n### Payment Latency\n\nFor `HighLatency` alerts on `payment-service`, treat p95 latency as the primary symptom.\n\nDiagnosis steps:\n\n1. Confirm whether p95 latency is isolated to payment-service or shared across upstream callers.\n2. Compare payment-service latency with downstream dependency latency for gateway, risk, and order services.\n3. Check connection pool wait time, retry spikes, and timeout rates.\n4. If downstream dependency latency increased first, classify payment-service as affected rather than root cause.\n\nThe expected evidence terms are p95 latency, payment-service, and downstream dependency.\n\n[Evidence 2]\nsource: aiops-alert-scope-control\ntitle: Alert Scope Control\nbreadcrumb: AIOps > Alert Scope Control\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## Alert Scope Control\n\nWhen an AIOps request already includes an alert payload, the agent should diagnose that payload first.\nIt must not expand the task into unrelated active alerts unless the user asks for broad alert triage.\n\nScope rules:\n\n1. Treat the provided payload as the primary incident boundary.\n2. Use unrelated active alerts only as correlation evidence when they share service, dependency, time window, or trace context.\n3. Do not replace the requested alert with a louder but unrelated alert.\n\nThis runbook anchors payload, unrelated active alerts, and scope behavior.\n\n[Evidence 3]\nsource: payment-service-latency\ntitle: Service Alerts\nbreadcrumb: AIOps > Service Alerts > Payment Latency\nlayer: L1\nreasons: semantic_rank:3, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n## Service Alerts\n\n[Evidence 4]\nsource: aiops-alert-scope-control\ntitle: AIOps\nbreadcrumb: AIOps > Alert Scope Control\nlayer: L1\nreasons: semantic_rank:5, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n# AIOps",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 2108,
"includedSources" : [ "payment-service-latency", "aiops-alert-scope-control", "payment-service-latency", "aiops-alert-scope-control" ],
"omittedSources" : [ ]
},
{
"source": "mysql-connection-pool",
"title": "MySQL Connection Pool Troubleshooting",
"breadcrumb": "Database > MySQL > Connection Pool",
"retrievalLayer": "L1",
"content": "Database connection pool saturation can increase payment latency when checkout paths wait for connections.",
"score": 0.68,
"hitReasons": ["keyword_match:+0.10"]
}
],
"contextPack": {
"packedText": "[1] Payment Service Latency Alert Playbook\nAIOps > Service Alerts > Payment Latency\nFor payment-service p95 latency alerts, check downstream dependency latency, thread pool saturation, gateway retries, and recent deployment changes.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 236,
"includedSources": ["payment-service-latency", "mysql-connection-pool"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "Alert HighLatency on payment-service with p95 latency above threshold",
"rewrittenQuery" : "Alert HighLatency on payment-service with p95 latency above threshold",
"categoryFilter" : "aiops",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "aiops" ],
"matched_keywords" : [ "HighLatency", "payment-service", "p95 latency" ],
"entities" : [ "HighLatency", "payment-service", "p95 latency" ],
"l0_titles" : [ "Payment Service Latency Alert" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "Alert HighLatency on payment-service with p95 latency above threshold",
"rewrittenQuery": "HighLatency payment-service p95 latency alert downstream dependency diagnosis",
"categoryFilter": "AIOps",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["AIOps"],
"matched_keywords": ["p95 latency", "payment-service", "downstream dependency"],
"entities": ["payment-service", "HighLatency"],
"l0_titles": ["Payment Service Latency Alert Playbook"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "Alert HighLatency on payment-service with p95 latency above threshold",
"categoryFilter" : "aiops",
"candidateCount" : 5,
"usable" : true,
"errorMessage" : null,
"durationMs" : 969,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.7736010700464249
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "HighLatency payment-service p95 latency alert downstream dependency diagnosis",
"categoryFilter": "AIOps",
"candidateCount": 2,
"usable": true,
"durationMs": 11,
"topScore": 0.84,
"topSimilarity": 0.84
}
]
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "payment-service-latency",
"baseScore" : 0.7736010700464249,
"finalScore" : 0.7736010700464249,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "aiops-alert-scope-control",
"baseScore" : 0.4683566689491272,
"finalScore" : 0.4683566689491272,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 3,
"source" : "payment-service-latency",
"baseScore" : 0.4981400966644287,
"finalScore" : 0.4981400966644287,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 4,
"source" : "aiops-alert-scope-control",
"baseScore" : 0.3814886808395386,
"finalScore" : 0.3814886808395386,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "payment-service-latency",
"baseScore": 0.84,
"finalScore": 1.29,
"boostReasons": ["domain_match:+0.15", "entity_match:+0.20", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "mysql-connection-pool",
"baseScore": 0.68,
"finalScore": 0.78,
"boostReasons": ["keyword_match:+0.10"]
}
]
}
"evidenceCandidateCount" : 5,
"evidenceBlockCount" : 4,
"relevanceLevel" : "PRECISE",
"completenessHint" : "知识库中不存在比上述结果更精准的文档",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,65 +1,122 @@
{
"caseId": "aiops-prometheus-alert-scope",
"query": "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "aiops-alert-scope-control",
"title": "AIOps Alert Scope Control",
"breadcrumb": "AIOps > Alert Scope Control",
"retrievalLayer": "L1",
"content": "When payload mode is used, diagnose the input alert payload and do not expand unrelated active alerts into the main diagnosis scope.",
"score": 0.88,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
}
],
"contextPack": {
"packedText": "[1] AIOps Alert Scope Control\nAIOps > Alert Scope Control\nWhen payload mode is used, diagnose the input alert payload and do not expand unrelated active alerts into the main diagnosis scope.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 188,
"includedSources": ["aiops-alert-scope-control"],
"omittedSources": []
"caseId" : "aiops-prometheus-alert-scope",
"query" : "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "aiops-alert-scope-control",
"chunkIndex" : 1,
"evidenceKey" : "aiops-alert-scope-control#chunk-1",
"source" : "aiops-alert-scope-control",
"title" : "Alert Scope Control",
"breadcrumb" : "AIOps > Alert Scope Control",
"retrievalLayer" : "L1",
"content" : "## Alert Scope Control\n\nWhen an AIOps request already includes an alert payload, the agent should diagnose that payload first.\nIt must not expand the task into unrelated active alerts unless the user asks for broad alert triage.\n\nScope rules:\n\n1. Treat the provided payload as the primary incident boundary.\n2. Use unrelated active alerts only as correlation evidence when they share service, dependency, time window, or trace context.\n3. Do not replace the requested alert with a louder but unrelated alert.\n\nThis runbook anchors payload, unrelated active alerts, and scope behavior.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "payment-service-latency",
"chunkIndex" : 1,
"evidenceKey" : "payment-service-latency#chunk-1",
"source" : "payment-service-latency",
"title" : "Service Alerts",
"breadcrumb" : "AIOps > Service Alerts > Payment Latency",
"retrievalLayer" : "L1",
"content" : "## Service Alerts",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "payment-service-latency",
"chunkIndex" : 2,
"evidenceKey" : "payment-service-latency#chunk-2",
"source" : "payment-service-latency",
"title" : "Payment Latency",
"breadcrumb" : "AIOps > Service Alerts > Payment Latency",
"retrievalLayer" : "L1",
"content" : "### Payment Latency\n\nFor `HighLatency` alerts on `payment-service`, treat p95 latency as the primary symptom.\n\nDiagnosis steps:\n\n1. Confirm whether p95 latency is isolated to payment-service or shared across upstream callers.\n2. Compare payment-service latency with downstream dependency latency for gateway, risk, and order services.\n3. Check connection pool wait time, retry spikes, and timeout rates.\n4. If downstream dependency latency increased first, classify payment-service as affected rather than root cause.\n\nThe expected evidence terms are p95 latency, payment-service, and downstream dependency.",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:3", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "aiops-alert-scope-control",
"chunkIndex" : 0,
"evidenceKey" : "aiops-alert-scope-control#chunk-0",
"source" : "aiops-alert-scope-control",
"title" : "AIOps",
"breadcrumb" : "AIOps > Alert Scope Control",
"retrievalLayer" : "L1",
"content" : "# AIOps",
"score" : 0.03076923079788685,
"hitReasons" : [ "semantic_rank:5", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: aiops-alert-scope-control\ntitle: Alert Scope Control\nbreadcrumb: AIOps > Alert Scope Control\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n## Alert Scope Control\n\nWhen an AIOps request already includes an alert payload, the agent should diagnose that payload first.\nIt must not expand the task into unrelated active alerts unless the user asks for broad alert triage.\n\nScope rules:\n\n1. Treat the provided payload as the primary incident boundary.\n2. Use unrelated active alerts only as correlation evidence when they share service, dependency, time window, or trace context.\n3. Do not replace the requested alert with a louder but unrelated alert.\n\nThis runbook anchors payload, unrelated active alerts, and scope behavior.\n\n[Evidence 2]\nsource: payment-service-latency\ntitle: Service Alerts\nbreadcrumb: AIOps > Service Alerts > Payment Latency\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## Service Alerts\n\n[Evidence 3]\nsource: payment-service-latency\ntitle: Payment Latency\nbreadcrumb: AIOps > Service Alerts > Payment Latency\nlayer: L1\nreasons: semantic_rank:3, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n### Payment Latency\n\nFor `HighLatency` alerts on `payment-service`, treat p95 latency as the primary symptom.\n\nDiagnosis steps:\n\n1. Confirm whether p95 latency is isolated to payment-service or shared across upstream callers.\n2. Compare payment-service latency with downstream dependency latency for gateway, risk, and order services.\n3. Check connection pool wait time, retry spikes, and timeout rates.\n4. If downstream dependency latency increased first, classify payment-service as affected rather than root cause.\n\nThe expected evidence terms are p95 latency, payment-service, and downstream dependency.\n\n[Evidence 4]\nsource: aiops-alert-scope-control\ntitle: AIOps\nbreadcrumb: AIOps > Alert Scope Control\nlayer: L1\nreasons: semantic_rank:5, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n# AIOps",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 2069,
"includedSources" : [ "aiops-alert-scope-control", "payment-service-latency", "payment-service-latency", "aiops-alert-scope-control" ],
"omittedSources" : [ ]
},
"retrievalTrace": {
"originalQuery": "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"rewrittenQuery": "AIOps alert payload scope unrelated active alerts diagnosis",
"categoryFilter": "AIOps",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["AIOps"],
"matched_keywords": ["payload", "unrelated active alerts", "scope"],
"entities": ["alert payload"],
"l0_titles": ["AIOps Alert Scope Control"],
"l0_match_count": 1
"retrievalTrace" : {
"originalQuery" : "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"rewrittenQuery" : "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"categoryFilter" : "aiops",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "aiops" ],
"matched_keywords" : [ "alert payload", "unrelated active alerts" ],
"entities" : [ "alert payload", "unrelated active alerts" ],
"l0_titles" : [ "AIOps Alert Scope Control" ],
"l0_match_count" : 1
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "AIOps alert payload scope unrelated active alerts diagnosis",
"categoryFilter": "AIOps",
"candidateCount": 1,
"usable": true,
"durationMs": 8,
"topScore": 0.88,
"topSimilarity": 0.88
}
]
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"categoryFilter" : "aiops",
"candidateCount" : 5,
"usable" : true,
"errorMessage" : null,
"durationMs" : 1623,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.7561411112546921
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "aiops-alert-scope-control",
"baseScore": 0.88,
"finalScore": 1.13,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
}
]
}
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "aiops-alert-scope-control",
"baseScore" : 0.7561411112546921,
"finalScore" : 0.7561411112546921,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "payment-service-latency",
"baseScore" : 0.503810703754425,
"finalScore" : 0.503810703754425,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 3,
"source" : "payment-service-latency",
"baseScore" : 0.5772626996040344,
"finalScore" : 0.5772626996040344,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 4,
"source" : "aiops-alert-scope-control",
"baseScore" : 0.36770421266555786,
"finalScore" : 0.36770421266555786,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"evidenceCandidateCount" : 5,
"evidenceBlockCount" : 4,
"relevanceLevel" : "PRECISE",
"completenessHint" : "知识库中不存在比上述结果更精准的文档",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,81 +1,88 @@
{
"caseId": "chat-diagnosis-flow",
"query": "What is the standard troubleshooting flow for an application incident?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "incident-diagnosis-flow",
"title": "Incident Diagnosis Flow",
"breadcrumb": "AIOps > Diagnosis Flow",
"retrievalLayer": "L1",
"content": "The standard flow is to collect evidence, identify the suspected fault domain, verify the hypothesis, apply remediation, and confirm recovery.",
"score": 0.82,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
"caseId" : "chat-diagnosis-flow",
"query" : "What is the standard troubleshooting flow for an application incident?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "incident-diagnosis-flow",
"chunkIndex" : 1,
"evidenceKey" : "incident-diagnosis-flow#chunk-1",
"source" : "incident-diagnosis-flow",
"title" : "Diagnosis Flow",
"breadcrumb" : "AIOps > Diagnosis Flow",
"retrievalLayer" : "L1",
"content" : "## Diagnosis Flow\n\nThe standard troubleshooting flow is evidence first, hypothesis second, remediation last.\n\nRecommended sequence:\n\n1. Collect evidence from alerts, metrics, logs, traces, deployments, and recent configuration changes.\n2. Define a small hypothesis that explains the observed symptoms.\n3. Verify the hypothesis with a targeted metric, log query, or reproduction step.\n4. Choose remediation that directly addresses the verified cause.\n5. Record the outcome and the evidence used to make the decision.\n\nDo not skip collect evidence, verify, and remediation ordering during an application incident.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "incident-diagnosis-flow",
"chunkIndex" : 0,
"evidenceKey" : "incident-diagnosis-flow#chunk-0",
"source" : "incident-diagnosis-flow",
"title" : "AIOps",
"breadcrumb" : "AIOps > Diagnosis Flow",
"retrievalLayer" : "L1",
"content" : "# AIOps",
"score" : 0.016129031777381897,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: incident-diagnosis-flow\ntitle: Diagnosis Flow\nbreadcrumb: AIOps > Diagnosis Flow\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n## Diagnosis Flow\n\nThe standard troubleshooting flow is evidence first, hypothesis second, remediation last.\n\nRecommended sequence:\n\n1. Collect evidence from alerts, metrics, logs, traces, deployments, and recent configuration changes.\n2. Define a small hypothesis that explains the observed symptoms.\n3. Verify the hypothesis with a targeted metric, log query, or reproduction step.\n4. Choose remediation that directly addresses the verified cause.\n5. Record the outcome and the evidence used to make the decision.\n\nDo not skip collect evidence, verify, and remediation ordering during an application incident.\n\n[Evidence 2]\nsource: incident-diagnosis-flow\ntitle: AIOps\nbreadcrumb: AIOps > Diagnosis Flow\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n# AIOps",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 1032,
"includedSources" : [ "incident-diagnosis-flow", "incident-diagnosis-flow" ],
"omittedSources" : [ ]
},
{
"source": "rag-chunk-context-reconstruction",
"title": "RAG Chunk Context Reconstruction",
"breadcrumb": "RAG > Chunking > Context Reconstruction",
"retrievalLayer": "L1",
"content": "Long sections may require neighbor chunk expansion and breadcrumb-aware packing.",
"score": 0.55,
"hitReasons": []
}
],
"contextPack": {
"packedText": "[1] Incident Diagnosis Flow\nAIOps > Diagnosis Flow\nThe standard flow is to collect evidence, identify the suspected fault domain, verify the hypothesis, apply remediation, and confirm recovery.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 192,
"includedSources": ["incident-diagnosis-flow", "rag-chunk-context-reconstruction"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "What is the standard troubleshooting flow for an application incident?",
"rewrittenQuery" : "What is the standard troubleshooting flow for an application incident?",
"categoryFilter" : "ops",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "ops" ],
"matched_keywords" : [ "standard troubleshooting flow", "application incident" ],
"entities" : [ "standard troubleshooting flow", "application incident" ],
"l0_titles" : [ "Incident Diagnosis Flow" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "What is the standard troubleshooting flow for an application incident?",
"rewrittenQuery": "standard application incident troubleshooting flow collect evidence verify remediation",
"categoryFilter": "AIOps",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["AIOps"],
"matched_keywords": ["collect evidence", "verify", "remediation"],
"entities": ["application incident"],
"l0_titles": ["Incident Diagnosis Flow"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "What is the standard troubleshooting flow for an application incident?",
"categoryFilter" : "ops",
"candidateCount" : 2,
"usable" : true,
"errorMessage" : null,
"durationMs" : 1540,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.6828859150409698
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "standard application incident troubleshooting flow collect evidence verify remediation",
"categoryFilter": "AIOps",
"candidateCount": 2,
"usable": true,
"durationMs": 10,
"topScore": 0.82,
"topSimilarity": 0.82
}
]
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "incident-diagnosis-flow",
"baseScore" : 0.6828859150409698,
"finalScore" : 0.6828859150409698,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "incident-diagnosis-flow",
"baseScore" : 0.3166210651397705,
"finalScore" : 0.3166210651397705,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "incident-diagnosis-flow",
"baseScore": 0.82,
"finalScore": 1.07,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "rag-chunk-context-reconstruction",
"baseScore": 0.55,
"finalScore": 0.55,
"boostReasons": []
}
]
}
"evidenceCandidateCount" : 2,
"evidenceBlockCount" : 2,
"relevanceLevel" : "REFERENCE",
"completenessHint" : "当前结果为相关参考,如需更精准信息请明确缺少的具体维度",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,81 +1,122 @@
{
"caseId": "chat-l0-domain-hint",
"query": "Should L0 keyword matching decide the final retrieval result?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "rag-l0-domain-entity-hint",
"title": "RAG L0 Domain Entity Hint",
"breadcrumb": "RAG > L0 > Domain Entity Hint",
"retrievalLayer": "L1",
"content": "L0 should be retained as a domain detector, entity extractor, metadata filter generator, and explainability signal, not as the final retrieval decision.",
"score": 0.88,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
"caseId" : "chat-l0-domain-hint",
"query" : "Should L0 keyword matching decide the final retrieval result?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 2,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-2",
"source" : "rag-l0-domain-entity-hint",
"title" : "Domain Entity Hint",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "rag-chunk-context-reconstruction",
"chunkIndex" : 2,
"evidenceKey" : "rag-chunk-context-reconstruction#chunk-2",
"source" : "rag-chunk-context-reconstruction",
"title" : "Context Reconstruction",
"breadcrumb" : "RAG > Chunking > Context Reconstruction",
"retrievalLayer" : "L1",
"content" : "### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.",
"score" : 0.032258063554763794,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 1,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-1",
"source" : "rag-l0-domain-entity-hint",
"title" : "L0",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "## L0",
"score" : 0.0317460335791111,
"hitReasons" : [ "semantic_rank:3", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "rag-chunk-context-reconstruction",
"chunkIndex" : 1,
"evidenceKey" : "rag-chunk-context-reconstruction#chunk-1",
"source" : "rag-chunk-context-reconstruction",
"title" : "Chunking",
"breadcrumb" : "RAG > Chunking > Context Reconstruction",
"retrievalLayer" : "L1",
"content" : "## Chunking",
"score" : 0.015625,
"hitReasons" : [ "semantic_rank:4", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: rag-l0-domain-entity-hint\ntitle: Domain Entity Hint\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.\n\n[Evidence 2]\nsource: rag-chunk-context-reconstruction\ntitle: Context Reconstruction\nbreadcrumb: RAG > Chunking > Context Reconstruction\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.\n\n[Evidence 3]\nsource: rag-l0-domain-entity-hint\ntitle: L0\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:3, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## L0\n\n[Evidence 4]\nsource: rag-chunk-context-reconstruction\ntitle: Chunking\nbreadcrumb: RAG > Chunking > Context Reconstruction\nlayer: L1\nreasons: semantic_rank:4, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## Chunking",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 1965,
"includedSources" : [ "rag-l0-domain-entity-hint", "rag-chunk-context-reconstruction", "rag-l0-domain-entity-hint", "rag-chunk-context-reconstruction" ],
"omittedSources" : [ ]
},
{
"source": "rag-l0-l1-fusion-ranking",
"title": "RAG L0 L1 Fusion Ranking",
"breadcrumb": "RAG > Ranking > Fusion",
"retrievalLayer": "L1",
"content": "L0 and L1 candidates should eventually be fused rather than handled as an early-return branch.",
"score": 0.75,
"hitReasons": ["domain_match:+0.15"]
}
],
"contextPack": {
"packedText": "[1] RAG L0 Domain Entity Hint\nRAG > L0 > Domain Entity Hint\nL0 should be retained as a domain detector, entity extractor, metadata filter generator, and explainability signal, not as the final retrieval decision.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 219,
"includedSources": ["rag-l0-domain-entity-hint", "rag-l0-l1-fusion-ranking"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "Should L0 keyword matching decide the final retrieval result?",
"rewrittenQuery" : "Should L0 keyword matching decide the final retrieval result?",
"categoryFilter" : "rag",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "rag" ],
"matched_keywords" : [ "L0 keyword matching", "final retrieval result" ],
"entities" : [ "L0 keyword matching", "final retrieval result" ],
"l0_titles" : [ "RAG L0 Domain Entity Hint" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "Should L0 keyword matching decide the final retrieval result?",
"rewrittenQuery": "RAG L0 keyword matching domain entity hint final retrieval decision",
"categoryFilter": "RAG",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["RAG"],
"matched_keywords": ["domain detector", "entity extractor", "metadata filter"],
"entities": ["L0"],
"l0_titles": ["RAG L0 Domain Entity Hint"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "Should L0 keyword matching decide the final retrieval result?",
"categoryFilter" : "rag",
"candidateCount" : 6,
"usable" : true,
"errorMessage" : null,
"durationMs" : 850,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.6438122987747192
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "RAG L0 keyword matching domain entity hint final retrieval decision",
"categoryFilter": "RAG",
"candidateCount": 2,
"usable": true,
"durationMs": 9,
"topScore": 0.88,
"topSimilarity": 0.88
}
]
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.6438122987747192,
"finalScore" : 0.6438122987747192,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "rag-chunk-context-reconstruction",
"baseScore" : 0.41426247358322144,
"finalScore" : 0.41426247358322144,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 3,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.3964804410934448,
"finalScore" : 0.3964804410934448,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 4,
"source" : "rag-chunk-context-reconstruction",
"baseScore" : 0.28678786754608154,
"finalScore" : 0.28678786754608154,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "rag-l0-domain-entity-hint",
"baseScore": 0.88,
"finalScore": 1.13,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "rag-l0-l1-fusion-ranking",
"baseScore": 0.75,
"finalScore": 0.9,
"boostReasons": ["domain_match:+0.15"]
}
]
}
"evidenceCandidateCount" : 6,
"evidenceBlockCount" : 4,
"relevanceLevel" : "REFERENCE",
"completenessHint" : "当前结果为相关参考,如需更精准信息请明确缺少的具体维度",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,91 +1,149 @@
{
"caseId": "chat-l0-filter-fallback",
"query": "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "rag-l0-filter-fallback",
"title": "RAG L0 Filter Fallback",
"breadcrumb": "RAG > Fallback > Unfiltered Retry",
"retrievalLayer": "L1",
"content": "When filtered vector retrieval is low quality, skip the L0 filter and run an unfiltered vector retry with the raw query before returning no evidence.",
"score": 0.83,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
"caseId" : "chat-l0-filter-fallback",
"query" : "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "rag-l0-filter-fallback",
"chunkIndex" : 2,
"evidenceKey" : "rag-l0-filter-fallback#chunk-2",
"source" : "rag-l0-filter-fallback",
"title" : "Unfiltered Retry",
"breadcrumb" : "RAG > Fallback > Unfiltered Retry",
"retrievalLayer" : "L1",
"content" : "### Unfiltered Retry\n\nIf the first vector search is over-constrained by an L0 metadata filter and returns low quality evidence,\nthe retriever should skip the L0 filter and run an unfiltered vector retry with the original query.\n\nThe fallback reason should be `filtered_vector_low_quality` when the filtered candidate exists but is below the\nreference threshold. If there is no usable evidence at all, use `filtered_vector_no_evidence`.\n\nThis document is the expected evidence for skip the L0 filter, unfiltered vector retry, and low quality behavior.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:UNFILTERED_VECTOR_RETRY", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "rag-l0-filter-decoy",
"chunkIndex" : 1,
"evidenceKey" : "rag-l0-filter-decoy#chunk-1",
"source" : "rag-l0-filter-decoy",
"title" : "Approval Window",
"breadcrumb" : "RAG > Fallback > Decoy",
"retrievalLayer" : "L1",
"content" : "## Approval Window\n\nThis document describes an unrelated release calendar approval window.\nIt intentionally avoids the real fallback instructions so the filtered retrieval\nattempt is low quality and the retriever must retry without the L0 category filter.",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:2", "attempt:UNFILTERED_VECTOR_RETRY", "l0_domain_overlap" ]
}, {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 2,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-2",
"source" : "rag-l0-domain-entity-hint",
"title" : "Domain Entity Hint",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.",
"score" : 0.0320020467042923,
"hitReasons" : [ "semantic_rank:3", "attempt:UNFILTERED_VECTOR_RETRY" ]
}, {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 1,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-1",
"source" : "rag-l0-domain-entity-hint",
"title" : "L0",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "## L0",
"score" : 0.03125,
"hitReasons" : [ "semantic_rank:4", "attempt:UNFILTERED_VECTOR_RETRY" ]
}, {
"docId" : "rag-chunk-context-reconstruction",
"chunkIndex" : 2,
"evidenceKey" : "rag-chunk-context-reconstruction#chunk-2",
"source" : "rag-chunk-context-reconstruction",
"title" : "Context Reconstruction",
"breadcrumb" : "RAG > Chunking > Context Reconstruction",
"retrievalLayer" : "L1",
"content" : "### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.",
"score" : 0.03053613007068634,
"hitReasons" : [ "semantic_rank:5", "attempt:UNFILTERED_VECTOR_RETRY" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: rag-l0-filter-fallback\ntitle: Unfiltered Retry\nbreadcrumb: RAG > Fallback > Unfiltered Retry\nlayer: L1\nreasons: semantic_rank:1, attempt:UNFILTERED_VECTOR_RETRY, l0_entity_overlap, l0_keyword_overlap\ncontent:\n### Unfiltered Retry\n\nIf the first vector search is over-constrained by an L0 metadata filter and returns low quality evidence,\nthe retriever should skip the L0 filter and run an unfiltered vector retry with the original query.\n\nThe fallback reason should be `filtered_vector_low_quality` when the filtered candidate exists but is below the\nreference threshold. If there is no usable evidence at all, use `filtered_vector_no_evidence`.\n\nThis document is the expected evidence for skip the L0 filter, unfiltered vector retry, and low quality behavior.\n\n[Evidence 2]\nsource: rag-l0-filter-decoy\ntitle: Approval Window\nbreadcrumb: RAG > Fallback > Decoy\nlayer: L1\nreasons: semantic_rank:2, attempt:UNFILTERED_VECTOR_RETRY, l0_domain_overlap\ncontent:\n## Approval Window\n\nThis document describes an unrelated release calendar approval window.\nIt intentionally avoids the real fallback instructions so the filtered retrieval\nattempt is low quality and the retriever must retry without the L0 category filter.\n\n[Evidence 3]\nsource: rag-l0-domain-entity-hint\ntitle: Domain Entity Hint\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:3, attempt:UNFILTERED_VECTOR_RETRY\ncontent:\n### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.\n\n[Evidence 4]\nsource: rag-l0-domain-entity-hint\ntitle: L0\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:4, attempt:UNFILTERED_VECTOR_RETRY\ncontent:\n## L0\n\n[Evidence 5]\nsource: rag-chunk-context-reconstruction\ntitle: Context Reconstruction\nbreadcrumb: RAG > Chunking > Context Reconstruction\nlayer: L1\nreasons: semantic_rank:5, attempt:UNFILTERED_VECTOR_RETRY\ncontent:\n### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 2904,
"includedSources" : [ "rag-l0-filter-fallback", "rag-l0-filter-decoy", "rag-l0-domain-entity-hint", "rag-l0-domain-entity-hint", "rag-chunk-context-reconstruction" ],
"omittedSources" : [ ]
},
{
"source": "rag-l0-domain-entity-hint",
"title": "RAG L0 Domain Entity Hint",
"breadcrumb": "RAG > L0 > Domain Entity Hint",
"retrievalLayer": "L1",
"content": "L0 supplies hints for metadata filtering and explanation, but it should not be treated as final fact evidence.",
"score": 0.66,
"hitReasons": ["domain_match:+0.15"]
}
],
"contextPack": {
"packedText": "[1] RAG L0 Filter Fallback\nRAG > Fallback > Unfiltered Retry\nWhen filtered vector retrieval is low quality, skip the L0 filter and run an unfiltered vector retry with the raw query before returning no evidence.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 214,
"includedSources": ["rag-l0-filter-fallback", "rag-l0-domain-entity-hint"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"rewrittenQuery" : "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"categoryFilter" : "overfilter-decoy",
"selectedAttempt" : "UNFILTERED_VECTOR_RETRY",
"fallbackReason" : "filtered_vector_low_quality",
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "overfilter-decoy" ],
"matched_keywords" : [ "over-filtered by L0", "filtered vector search", "low quality evidence" ],
"entities" : [ "over-filtered by L0", "filtered vector search", "low quality evidence" ],
"l0_titles" : [ "RAG L0 Filter Decoy" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"rewrittenQuery": "RAG L0 filtered vector low quality fallback unfiltered retry",
"categoryFilter": "RAG",
"selectedAttempt": "UNFILTERED_VECTOR_RETRY",
"fallbackReason": "filtered_vector_low_quality",
"evidenceStatus": "supported",
"queryHints": {
"domains": ["RAG"],
"matched_keywords": ["L0", "low quality", "unfiltered vector retry"],
"entities": ["L0"],
"l0_titles": ["RAG L0 Domain Entity Hint"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"categoryFilter" : "overfilter-decoy",
"candidateCount" : 2,
"usable" : false,
"errorMessage" : null,
"durationMs" : 722,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.47391992807388306
}, {
"name" : "UNFILTERED_VECTOR_RETRY",
"query" : "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"categoryFilter" : null,
"candidateCount" : 20,
"usable" : true,
"errorMessage" : null,
"durationMs" : 2606,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.7571567445993423
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "RAG L0 filtered vector low quality fallback unfiltered retry",
"categoryFilter": "RAG",
"candidateCount": 1,
"usable": false,
"durationMs": 7,
"topScore": 1.35,
"topSimilarity": 0.325
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "rag-l0-filter-fallback",
"baseScore" : 0.7571567445993423,
"finalScore" : 0.7571567445993423,
"boostReasons" : [ "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "rag-l0-filter-decoy",
"baseScore" : 0.47391992807388306,
"finalScore" : 0.47391992807388306,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 3,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.5912361443042755,
"finalScore" : 0.5912361443042755,
"boostReasons" : [ ]
}, {
"finalRank" : 4,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.473749577999115,
"finalScore" : 0.473749577999115,
"boostReasons" : [ ]
}, {
"finalRank" : 5,
"source" : "rag-chunk-context-reconstruction",
"baseScore" : 0.4492502808570862,
"finalScore" : 0.4492502808570862,
"boostReasons" : [ ]
} ]
},
{
"name": "UNFILTERED_VECTOR_RETRY",
"query": "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"categoryFilter": null,
"candidateCount": 2,
"usable": true,
"durationMs": 13,
"topScore": 0.83,
"topSimilarity": 0.83
}
]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "rag-l0-filter-fallback",
"baseScore": 0.83,
"finalScore": 1.08,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "rag-l0-domain-entity-hint",
"baseScore": 0.66,
"finalScore": 0.81,
"boostReasons": ["domain_match:+0.15"]
}
]
}
"evidenceCandidateCount" : 20,
"evidenceBlockCount" : 5,
"relevanceLevel" : "PRECISE",
"completenessHint" : "知识库中不存在比上述结果更精准的文档",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,81 +1,88 @@
{
"caseId": "chat-mysql-connection-pool",
"query": "MySQL connection pool is exhausted. How should I diagnose it?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "mysql-connection-pool",
"title": "MySQL Connection Pool Troubleshooting",
"breadcrumb": "Database > MySQL > Connection Pool",
"retrievalLayer": "L1",
"content": "When the connection pool is exhausted, inspect HikariCP active connections, max_connections, slow SQL, leak detection, and database wait events.",
"score": 0.86,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
"caseId" : "chat-mysql-connection-pool",
"query" : "MySQL connection pool is exhausted. How should I diagnose it?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "mysql-connection-pool",
"chunkIndex" : 2,
"evidenceKey" : "mysql-connection-pool#chunk-2",
"source" : "mysql-connection-pool",
"title" : "Connection Pool",
"breadcrumb" : "Database > MySQL > Connection Pool",
"retrievalLayer" : "L1",
"content" : "### Connection Pool\n\nWhen MySQL connection pool is exhausted, first compare application pool usage with database `max_connections`.\nFor HikariCP, check `active`, `idle`, `pending`, and connection acquisition timeout metrics.\n\nRecommended diagnosis:\n\n1. Verify whether HikariCP active connections stay near maximum while pending threads grow.\n2. Check MySQL `Threads_connected`, `Threads_running`, and `max_connections`.\n3. Inspect slow SQL and long transactions that keep connections checked out.\n4. If the database is healthy, look for application connection leaks or missing transaction boundaries.\n\nUse this runbook as evidence for connection pool, max_connections, and HikariCP incidents.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "mysql-connection-pool",
"chunkIndex" : 1,
"evidenceKey" : "mysql-connection-pool#chunk-1",
"source" : "mysql-connection-pool",
"title" : "MySQL",
"breadcrumb" : "Database > MySQL > Connection Pool",
"retrievalLayer" : "L1",
"content" : "## MySQL",
"score" : 0.032258063554763794,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: mysql-connection-pool\ntitle: Connection Pool\nbreadcrumb: Database > MySQL > Connection Pool\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n### Connection Pool\n\nWhen MySQL connection pool is exhausted, first compare application pool usage with database `max_connections`.\nFor HikariCP, check `active`, `idle`, `pending`, and connection acquisition timeout metrics.\n\nRecommended diagnosis:\n\n1. Verify whether HikariCP active connections stay near maximum while pending threads grow.\n2. Check MySQL `Threads_connected`, `Threads_running`, and `max_connections`.\n3. Inspect slow SQL and long transactions that keep connections checked out.\n4. If the database is healthy, look for application connection leaks or missing transaction boundaries.\n\nUse this runbook as evidence for connection pool, max_connections, and HikariCP incidents.\n\n[Evidence 2]\nsource: mysql-connection-pool\ntitle: MySQL\nbreadcrumb: Database > MySQL > Connection Pool\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## MySQL",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 1135,
"includedSources" : [ "mysql-connection-pool", "mysql-connection-pool" ],
"omittedSources" : [ ]
},
{
"source": "incident-diagnosis-flow",
"title": "Incident Diagnosis Flow",
"breadcrumb": "AIOps > Diagnosis Flow",
"retrievalLayer": "L1",
"content": "Collect evidence, compare metrics and logs, then verify remediation before closing the incident.",
"score": 0.61,
"hitReasons": []
}
],
"contextPack": {
"packedText": "[1] MySQL Connection Pool Troubleshooting\nDatabase > MySQL > Connection Pool\nWhen the connection pool is exhausted, inspect HikariCP active connections, max_connections, slow SQL, leak detection, and database wait events.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 216,
"includedSources": ["mysql-connection-pool", "incident-diagnosis-flow"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "MySQL connection pool is exhausted. How should I diagnose it?",
"rewrittenQuery" : "MySQL connection pool is exhausted. How should I diagnose it?",
"categoryFilter" : "database",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "database" ],
"matched_keywords" : [ "MySQL connection pool" ],
"entities" : [ "MySQL connection pool" ],
"l0_titles" : [ "MySQL Connection Pool Runbook" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "MySQL connection pool is exhausted. How should I diagnose it?",
"rewrittenQuery": "MySQL connection pool exhausted HikariCP max_connections diagnosis",
"categoryFilter": "Database",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["Database", "MySQL"],
"matched_keywords": ["connection pool", "HikariCP", "max_connections"],
"entities": ["MySQL", "HikariCP"],
"l0_titles": ["MySQL Connection Pool Troubleshooting"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "MySQL connection pool is exhausted. How should I diagnose it?",
"categoryFilter" : "database",
"candidateCount" : 3,
"usable" : true,
"errorMessage" : null,
"durationMs" : 5267,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.8114794194698334
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "MySQL connection pool exhausted HikariCP max_connections diagnosis",
"categoryFilter": "Database",
"candidateCount": 2,
"usable": true,
"durationMs": 12,
"topScore": 0.86,
"topSimilarity": 0.86
}
]
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "mysql-connection-pool",
"baseScore" : 0.8114794194698334,
"finalScore" : 0.8114794194698334,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "mysql-connection-pool",
"baseScore" : 0.49219560623168945,
"finalScore" : 0.49219560623168945,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "mysql-connection-pool",
"baseScore": 0.86,
"finalScore": 1.11,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "incident-diagnosis-flow",
"baseScore": 0.61,
"finalScore": 0.61,
"boostReasons": []
}
]
}
"evidenceCandidateCount" : 3,
"evidenceBlockCount" : 2,
"relevanceLevel" : "PRECISE",
"completenessHint" : "知识库中不存在比上述结果更精准的文档",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -1,81 +1,122 @@
{
"caseId": "chat-rag-chunk-context",
"query": "If a long section is split into multiple chunks, how do we keep retrieval context?",
"retrievedAt": "2026-07-06T00:00:00Z",
"lookupResult": {
"found": true,
"evidenceBlocks": [
{
"source": "rag-chunk-context-reconstruction",
"title": "RAG Chunk Context Reconstruction",
"breadcrumb": "RAG > Chunking > Context Reconstruction",
"retrievalLayer": "L1",
"content": "After a chunk hit, expand to neighbor chunk candidates from the same section and preserve breadcrumb metadata in the evidence pack.",
"score": 0.79,
"hitReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
"caseId" : "chat-rag-chunk-context",
"query" : "If a long section is split into multiple chunks, how do we keep retrieval context?",
"retrievedAt" : "2026-07-28T06:54:51.843450400Z",
"searchMode" : "hybrid",
"kbScope" : "rag-eval",
"lookupResult" : {
"found" : true,
"evidenceBlocks" : [ {
"docId" : "rag-chunk-context-reconstruction",
"chunkIndex" : 2,
"evidenceKey" : "rag-chunk-context-reconstruction#chunk-2",
"source" : "rag-chunk-context-reconstruction",
"title" : "Context Reconstruction",
"breadcrumb" : "RAG > Chunking > Context Reconstruction",
"retrievalLayer" : "L1",
"content" : "### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.",
"score" : 0.032786883413791656,
"hitReasons" : [ "semantic_rank:1", "attempt:FILTERED_VECTOR", "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 2,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-2",
"source" : "rag-l0-domain-entity-hint",
"title" : "Domain Entity Hint",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.",
"score" : 0.032258063554763794,
"hitReasons" : [ "semantic_rank:2", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "rag-chunk-context-reconstruction",
"chunkIndex" : 1,
"evidenceKey" : "rag-chunk-context-reconstruction#chunk-1",
"source" : "rag-chunk-context-reconstruction",
"title" : "Chunking",
"breadcrumb" : "RAG > Chunking > Context Reconstruction",
"retrievalLayer" : "L1",
"content" : "## Chunking",
"score" : 0.01587301678955555,
"hitReasons" : [ "semantic_rank:3", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
}, {
"docId" : "rag-l0-domain-entity-hint",
"chunkIndex" : 0,
"evidenceKey" : "rag-l0-domain-entity-hint#chunk-0",
"source" : "rag-l0-domain-entity-hint",
"title" : "RAG",
"breadcrumb" : "RAG > L0 > Domain Entity Hint",
"retrievalLayer" : "L1",
"content" : "# RAG",
"score" : 0.015625,
"hitReasons" : [ "semantic_rank:4", "attempt:FILTERED_VECTOR", "l0_domain_overlap" ]
} ],
"contextPack" : {
"packedText" : "[Evidence 1]\nsource: rag-chunk-context-reconstruction\ntitle: Context Reconstruction\nbreadcrumb: RAG > Chunking > Context Reconstruction\nlayer: L1\nreasons: semantic_rank:1, attempt:FILTERED_VECTOR, l0_domain_overlap, l0_entity_overlap, l0_keyword_overlap\ncontent:\n### Context Reconstruction\n\nWhen a long section is split into multiple chunks, retrieval should keep enough local structure for the answer.\n\nRecommended behavior:\n\n1. Store the breadcrumb with every chunk.\n2. Preserve the same section identity across adjacent chunks.\n3. During context packing, include a neighbor chunk when the selected chunk depends on nearby setup or definitions.\n4. Prefer concise evidence blocks that show the breadcrumb and the relevant content span.\n\nThe key concepts are neighbor chunk, same section, and breadcrumb.\n\n[Evidence 2]\nsource: rag-l0-domain-entity-hint\ntitle: Domain Entity Hint\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:2, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n### Domain Entity Hint\n\nL0 keyword matching should not decide the final retrieval result.\nIn the modular RAG pipeline, L0 behaves like a lightweight domain detector and entity extractor.\n\nThe output can provide:\n\n1. Candidate domain hints.\n2. Matched entities and keywords.\n3. An optional metadata filter for the first vector retrieval attempt.\n\nFinal evidence still comes from L1 vector retrieval, post-retrieval normalization, rerank, and context packing.\nThe important terms are domain detector, entity extractor, and metadata filter.\n\n[Evidence 3]\nsource: rag-chunk-context-reconstruction\ntitle: Chunking\nbreadcrumb: RAG > Chunking > Context Reconstruction\nlayer: L1\nreasons: semantic_rank:3, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n## Chunking\n\n[Evidence 4]\nsource: rag-l0-domain-entity-hint\ntitle: RAG\nbreadcrumb: RAG > L0 > Domain Entity Hint\nlayer: L1\nreasons: semantic_rank:4, attempt:FILTERED_VECTOR, l0_domain_overlap\ncontent:\n# RAG",
"strategy" : "ranked_evidence_char_budget",
"charBudget" : 4000,
"usedChars" : 1966,
"includedSources" : [ "rag-chunk-context-reconstruction", "rag-l0-domain-entity-hint", "rag-chunk-context-reconstruction", "rag-l0-domain-entity-hint" ],
"omittedSources" : [ ]
},
{
"source": "rag-breadcrumb-embedding-gap",
"title": "RAG Breadcrumb Embedding Gap",
"breadcrumb": "RAG > Embedding > Breadcrumb",
"retrievalLayer": "L1",
"content": "Embedding title and breadcrumb with content helps recover section semantics.",
"score": 0.72,
"hitReasons": ["domain_match:+0.15"]
}
],
"contextPack": {
"packedText": "[1] RAG Chunk Context Reconstruction\nRAG > Chunking > Context Reconstruction\nAfter a chunk hit, expand to neighbor chunk candidates from the same section and preserve breadcrumb metadata in the evidence pack.",
"strategy": "top_evidence_blocks",
"charBudget": 3500,
"usedChars": 203,
"includedSources": ["rag-chunk-context-reconstruction", "rag-breadcrumb-embedding-gap"],
"omittedSources": []
"retrievalTrace" : {
"originalQuery" : "If a long section is split into multiple chunks, how do we keep retrieval context?",
"rewrittenQuery" : "If a long section is split into multiple chunks, how do we keep retrieval context?",
"categoryFilter" : "rag",
"selectedAttempt" : "FILTERED_VECTOR",
"fallbackReason" : null,
"evidenceStatus" : "supported",
"queryHints" : {
"domains" : [ "rag" ],
"matched_keywords" : [ "split into multiple chunks", "retrieval context" ],
"entities" : [ "split into multiple chunks", "retrieval context" ],
"l0_titles" : [ "RAG Chunk Context Reconstruction" ],
"l0_match_count" : 1
},
"retrievalTrace": {
"originalQuery": "If a long section is split into multiple chunks, how do we keep retrieval context?",
"rewrittenQuery": "RAG chunk context reconstruction neighbor chunk same section breadcrumb",
"categoryFilter": "RAG",
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"queryHints": {
"domains": ["RAG"],
"matched_keywords": ["neighbor chunk", "same section", "breadcrumb"],
"entities": ["chunk", "breadcrumb"],
"l0_titles": ["RAG Chunk Context Reconstruction"],
"l0_match_count": 1
"attempts" : [ {
"name" : "FILTERED_VECTOR",
"query" : "If a long section is split into multiple chunks, how do we keep retrieval context?",
"categoryFilter" : "rag",
"candidateCount" : 6,
"usable" : true,
"errorMessage" : null,
"durationMs" : 743,
"topScore" : 0.032786883413791656,
"topSimilarity" : 0.7487991750240326
} ]
},
"attempts": [
{
"name": "FILTERED_VECTOR",
"query": "RAG chunk context reconstruction neighbor chunk same section breadcrumb",
"categoryFilter": "RAG",
"candidateCount": 2,
"usable": true,
"durationMs": 9,
"topScore": 0.79,
"topSimilarity": 0.79
}
]
"rerankTrace" : {
"items" : [ {
"finalRank" : 1,
"source" : "rag-chunk-context-reconstruction",
"baseScore" : 0.7487991750240326,
"finalScore" : 0.7487991750240326,
"boostReasons" : [ "l0_domain_overlap", "l0_entity_overlap", "l0_keyword_overlap" ]
}, {
"finalRank" : 2,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.4840593934059143,
"finalScore" : 0.4840593934059143,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 3,
"source" : "rag-chunk-context-reconstruction",
"baseScore" : 0.42978107929229736,
"finalScore" : 0.42978107929229736,
"boostReasons" : [ "l0_domain_overlap" ]
}, {
"finalRank" : 4,
"source" : "rag-l0-domain-entity-hint",
"baseScore" : 0.3859822154045105,
"finalScore" : 0.3859822154045105,
"boostReasons" : [ "l0_domain_overlap" ]
} ]
},
"rerankTrace": {
"items": [
{
"finalRank": 1,
"source": "rag-chunk-context-reconstruction",
"baseScore": 0.79,
"finalScore": 1.04,
"boostReasons": ["domain_match:+0.15", "keyword_match:+0.10"]
},
{
"finalRank": 2,
"source": "rag-breadcrumb-embedding-gap",
"baseScore": 0.72,
"finalScore": 0.87,
"boostReasons": ["domain_match:+0.15"]
}
]
}
"evidenceCandidateCount" : 6,
"evidenceBlockCount" : 4,
"relevanceLevel" : "REFERENCE",
"completenessHint" : "当前结果为相关参考,如需更精准信息请明确缺少的具体维度",
"retrievedDomainsThisSession" : null,
"message" : null
}
}
@@ -0,0 +1,107 @@
{
"generatedAt": "2026-07-28T06:48:56.439554+00:00",
"baselineReport": "eval/rag-retrieval/cases/golden-cases.json",
"currentReport": "eval/rag-retrieval/cases/golden-cases.json",
"baselineCaseCount": 7,
"currentCaseCount": 7,
"baselinePassRate": 1.0,
"currentPassRate": 0.8571,
"baselineRecallAtK": 1.0,
"currentRecallAtK": 0.8571,
"regressionCount": 6,
"improvementCount": 0,
"changedCount": 3,
"hasRegression": true,
"items": [
{
"type": "REGRESSION",
"scope": "aggregate",
"caseId": null,
"metric": "passRate",
"baselineValue": "1.0",
"currentValue": "0.8571",
"delta": -0.14290000000000003,
"message": "aggregate passRate changed"
},
{
"type": "REGRESSION",
"scope": "aggregate",
"caseId": null,
"metric": "recallAtK",
"baselineValue": "1.0",
"currentValue": "0.8571",
"delta": -0.14290000000000003,
"message": "aggregate recallAtK changed"
},
{
"type": "REGRESSION",
"scope": "aggregate",
"caseId": null,
"metric": "strongHitRate",
"baselineValue": "1.0",
"currentValue": "0.8571",
"delta": -0.14290000000000003,
"message": "aggregate strongHitRate changed"
},
{
"type": "REGRESSION",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "passed",
"baselineValue": "True",
"currentValue": "False",
"delta": -1.0,
"message": "chat-l0-filter-fallback passed changed"
},
{
"type": "REGRESSION",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "hitLevel",
"baselineValue": "strong",
"currentValue": "weak",
"delta": -2.0,
"message": "chat-l0-filter-fallback hitLevel changed"
},
{
"type": "REGRESSION",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "firstExpectedRank",
"baselineValue": "1",
"currentValue": "-",
"delta": null,
"message": "chat-l0-filter-fallback firstExpectedRank changed"
},
{
"type": "CHANGED",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "selectedAttempt",
"baselineValue": "UNFILTERED_VECTOR_RETRY",
"currentValue": "FILTERED_VECTOR",
"delta": null,
"message": "chat-l0-filter-fallback selectedAttempt changed"
},
{
"type": "CHANGED",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "fallbackReason",
"baselineValue": "filtered_vector_low_quality",
"currentValue": "-",
"delta": null,
"message": "chat-l0-filter-fallback fallbackReason changed"
},
{
"type": "CHANGED",
"scope": "case",
"caseId": "chat-l0-filter-fallback",
"metric": "rerankTopSource",
"baselineValue": "rag-l0-filter-fallback",
"currentValue": "rag-l0-filter-decoy",
"delta": null,
"message": "chat-l0-filter-fallback rerankTopSource changed"
}
]
}
@@ -0,0 +1,31 @@
# RAG Retrieval Baseline Diff
Generated at: `2026-07-28T06:48:56.439554+00:00`
## Summary
| Metric | Value |
|---|---:|
| Baseline cases | 7 |
| Current cases | 7 |
| Baseline pass rate | 1.0 |
| Current pass rate | 0.8571 |
| Baseline recall@K | 1.0 |
| Current recall@K | 0.8571 |
| Regressions | 6 |
| Improvements | 0 |
| Changed | 3 |
## Items
| Type | Scope | Case | Metric | Baseline | Current | Delta | Message |
|---|---|---|---|---|---|---:|---|
| REGRESSION | aggregate | | passRate | 1.0 | 0.8571 | -0.14290000000000003 | aggregate passRate changed |
| REGRESSION | aggregate | | recallAtK | 1.0 | 0.8571 | -0.14290000000000003 | aggregate recallAtK changed |
| REGRESSION | aggregate | | strongHitRate | 1.0 | 0.8571 | -0.14290000000000003 | aggregate strongHitRate changed |
| REGRESSION | case | chat-l0-filter-fallback | passed | True | False | -1.0 | chat-l0-filter-fallback passed changed |
| REGRESSION | case | chat-l0-filter-fallback | hitLevel | strong | weak | -2.0 | chat-l0-filter-fallback hitLevel changed |
| REGRESSION | case | chat-l0-filter-fallback | firstExpectedRank | 1 | - | | chat-l0-filter-fallback firstExpectedRank changed |
| CHANGED | case | chat-l0-filter-fallback | selectedAttempt | UNFILTERED_VECTOR_RETRY | FILTERED_VECTOR | | chat-l0-filter-fallback selectedAttempt changed |
| CHANGED | case | chat-l0-filter-fallback | fallbackReason | filtered_vector_low_quality | - | | chat-l0-filter-fallback fallbackReason changed |
| CHANGED | case | chat-l0-filter-fallback | rerankTopSource | rag-l0-filter-fallback | rag-l0-filter-decoy | | chat-l0-filter-fallback rerankTopSource changed |
+38 -14
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-07-06T13:37:59.726351+00:00",
"generatedAt": "2026-07-28T06:55:09.379164+00:00",
"caseFile": "eval/rag-retrieval/cases/golden-cases.json",
"fixtureDir": "eval/rag-retrieval/fixtures",
"aggregate": {
@@ -28,7 +28,7 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:mysql-connection-pool",
"2:incident-diagnosis-flow"
"2:mysql-connection-pool"
],
"matchedKeywords": [
"connection pool",
@@ -41,7 +41,7 @@
"evidenceStatus": "supported",
"includedSources": [
"mysql-connection-pool",
"incident-diagnosis-flow"
"mysql-connection-pool"
],
"omittedSources": [],
"rerankTopSource": "mysql-connection-pool",
@@ -57,7 +57,7 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:incident-diagnosis-flow",
"2:rag-chunk-context-reconstruction"
"2:incident-diagnosis-flow"
],
"matchedKeywords": [
"collect evidence",
@@ -70,7 +70,7 @@
"evidenceStatus": "supported",
"includedSources": [
"incident-diagnosis-flow",
"rag-chunk-context-reconstruction"
"incident-diagnosis-flow"
],
"omittedSources": [],
"rerankTopSource": "incident-diagnosis-flow",
@@ -86,7 +86,9 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:payment-service-latency",
"2:mysql-connection-pool"
"2:aiops-alert-scope-control",
"3:payment-service-latency",
"4:aiops-alert-scope-control"
],
"matchedKeywords": [
"p95 latency",
@@ -99,7 +101,9 @@
"evidenceStatus": "supported",
"includedSources": [
"payment-service-latency",
"mysql-connection-pool"
"aiops-alert-scope-control",
"payment-service-latency",
"aiops-alert-scope-control"
],
"omittedSources": [],
"rerankTopSource": "payment-service-latency",
@@ -114,7 +118,10 @@
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:aiops-alert-scope-control"
"1:aiops-alert-scope-control",
"2:payment-service-latency",
"3:payment-service-latency",
"4:aiops-alert-scope-control"
],
"matchedKeywords": [
"payload",
@@ -126,6 +133,9 @@
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"aiops-alert-scope-control",
"payment-service-latency",
"payment-service-latency",
"aiops-alert-scope-control"
],
"omittedSources": [],
@@ -142,7 +152,9 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:rag-chunk-context-reconstruction",
"2:rag-breadcrumb-embedding-gap"
"2:rag-l0-domain-entity-hint",
"3:rag-chunk-context-reconstruction",
"4:rag-l0-domain-entity-hint"
],
"matchedKeywords": [
"neighbor chunk",
@@ -155,7 +167,9 @@
"evidenceStatus": "supported",
"includedSources": [
"rag-chunk-context-reconstruction",
"rag-breadcrumb-embedding-gap"
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction",
"rag-l0-domain-entity-hint"
],
"omittedSources": [],
"rerankTopSource": "rag-chunk-context-reconstruction",
@@ -171,7 +185,9 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:rag-l0-domain-entity-hint",
"2:rag-l0-l1-fusion-ranking"
"2:rag-chunk-context-reconstruction",
"3:rag-l0-domain-entity-hint",
"4:rag-chunk-context-reconstruction"
],
"matchedKeywords": [
"domain detector",
@@ -184,7 +200,9 @@
"evidenceStatus": "supported",
"includedSources": [
"rag-l0-domain-entity-hint",
"rag-l0-l1-fusion-ranking"
"rag-chunk-context-reconstruction",
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction"
],
"omittedSources": [],
"rerankTopSource": "rag-l0-domain-entity-hint",
@@ -200,7 +218,10 @@
"firstExpectedRank": 1,
"topCandidates": [
"1:rag-l0-filter-fallback",
"2:rag-l0-domain-entity-hint"
"2:rag-l0-filter-decoy",
"3:rag-l0-domain-entity-hint",
"4:rag-l0-domain-entity-hint",
"5:rag-chunk-context-reconstruction"
],
"matchedKeywords": [
"skip the l0 filter",
@@ -213,7 +234,10 @@
"evidenceStatus": "supported",
"includedSources": [
"rag-l0-filter-fallback",
"rag-l0-domain-entity-hint"
"rag-l0-filter-decoy",
"rag-l0-domain-entity-hint",
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction"
],
"omittedSources": [],
"rerankTopSource": "rag-l0-filter-fallback",
+8 -8
View File
@@ -1,6 +1,6 @@
# RAG Retrieval Baseline
Generated at: `2026-07-06T13:37:59.726351+00:00`
Generated at: `2026-07-28T06:55:09.379164+00:00`
## Aggregate
@@ -24,10 +24,10 @@ Generated at: `2026-07-06T13:37:59.726351+00:00`
| Case | Scenario | Pass | Hit | Attempt | Fallback | Evidence | First Expected Rank | Top Candidates | Failed Checks |
|---|---|---|---|---|---|---|---:|---|---|
| chat-mysql-connection-pool | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:mysql-connection-pool<br>2:incident-diagnosis-flow | |
| chat-diagnosis-flow | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:incident-diagnosis-flow<br>2:rag-chunk-context-reconstruction | |
| aiops-payment-latency-alert | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:payment-service-latency<br>2:mysql-connection-pool | |
| aiops-prometheus-alert-scope | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:aiops-alert-scope-control | |
| chat-rag-chunk-context | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-chunk-context-reconstruction<br>2:rag-breadcrumb-embedding-gap | |
| chat-l0-domain-hint | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-l0-domain-entity-hint<br>2:rag-l0-l1-fusion-ranking | |
| chat-l0-filter-fallback | chat | true | strong | UNFILTERED_VECTOR_RETRY | filtered_vector_low_quality | supported | 1 | 1:rag-l0-filter-fallback<br>2:rag-l0-domain-entity-hint | |
| chat-mysql-connection-pool | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:mysql-connection-pool<br>2:mysql-connection-pool | |
| chat-diagnosis-flow | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:incident-diagnosis-flow<br>2:incident-diagnosis-flow | |
| aiops-payment-latency-alert | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:payment-service-latency<br>2:aiops-alert-scope-control<br>3:payment-service-latency<br>4:aiops-alert-scope-control | |
| aiops-prometheus-alert-scope | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:aiops-alert-scope-control<br>2:payment-service-latency<br>3:payment-service-latency<br>4:aiops-alert-scope-control | |
| chat-rag-chunk-context | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-chunk-context-reconstruction<br>2:rag-l0-domain-entity-hint<br>3:rag-chunk-context-reconstruction<br>4:rag-l0-domain-entity-hint | |
| chat-l0-domain-hint | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-l0-domain-entity-hint<br>2:rag-chunk-context-reconstruction<br>3:rag-l0-domain-entity-hint<br>4:rag-chunk-context-reconstruction | |
| chat-l0-filter-fallback | chat | true | strong | UNFILTERED_VECTOR_RETRY | filtered_vector_low_quality | supported | 1 | 1:rag-l0-filter-fallback<br>2:rag-l0-filter-decoy<br>3:rag-l0-domain-entity-hint<br>4:rag-l0-domain-entity-hint<br>5:rag-chunk-context-reconstruction | |
+245
View File
@@ -0,0 +1,245 @@
{
"generatedAt": "2026-07-28T06:48:56.421877+00:00",
"caseFile": "eval/rag-retrieval/cases/golden-cases.json",
"fixtureDir": "eval/rag-retrieval/fixtures",
"aggregate": {
"caseCount": 7,
"topK": 5,
"passedCount": 6,
"failedCount": 1,
"passRate": 0.8571,
"lookupResultCaseCount": 7,
"strongHitCount": 6,
"mediumHitCount": 0,
"weakHitCount": 1,
"missCount": 0,
"recallAtK": 0.8571,
"strongHitRate": 0.8571,
"averageFirstHitRank": 1.0
},
"results": [
{
"caseId": "chat-mysql-connection-pool",
"scenario": "chat",
"query": "MySQL connection pool is exhausted. How should I diagnose it?",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:mysql-connection-pool",
"2:mysql-connection-pool"
],
"matchedKeywords": [
"connection pool",
"max_connections",
"hikaricp"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"mysql-connection-pool",
"mysql-connection-pool"
],
"omittedSources": [],
"rerankTopSource": "mysql-connection-pool",
"failedChecks": []
},
{
"caseId": "chat-diagnosis-flow",
"scenario": "chat",
"query": "What is the standard troubleshooting flow for an application incident?",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:incident-diagnosis-flow",
"2:incident-diagnosis-flow"
],
"matchedKeywords": [
"collect evidence",
"verify",
"remediation"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"incident-diagnosis-flow",
"incident-diagnosis-flow"
],
"omittedSources": [],
"rerankTopSource": "incident-diagnosis-flow",
"failedChecks": []
},
{
"caseId": "aiops-payment-latency-alert",
"scenario": "aiops",
"query": "Alert HighLatency on payment-service with p95 latency above threshold",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:payment-service-latency",
"2:aiops-alert-scope-control",
"3:payment-service-latency",
"4:aiops-alert-scope-control"
],
"matchedKeywords": [
"p95 latency",
"payment-service",
"downstream dependency"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"payment-service-latency",
"aiops-alert-scope-control",
"payment-service-latency",
"aiops-alert-scope-control"
],
"omittedSources": [],
"rerankTopSource": "payment-service-latency",
"failedChecks": []
},
{
"caseId": "aiops-prometheus-alert-scope",
"scenario": "aiops",
"query": "When an AIOps request already includes alert payload, should the agent diagnose unrelated active alerts?",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:aiops-alert-scope-control",
"2:payment-service-latency",
"3:payment-service-latency",
"4:aiops-alert-scope-control"
],
"matchedKeywords": [
"payload",
"unrelated active alerts",
"scope"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"aiops-alert-scope-control",
"payment-service-latency",
"payment-service-latency",
"aiops-alert-scope-control"
],
"omittedSources": [],
"rerankTopSource": "aiops-alert-scope-control",
"failedChecks": []
},
{
"caseId": "chat-rag-chunk-context",
"scenario": "chat",
"query": "If a long section is split into multiple chunks, how do we keep retrieval context?",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:rag-chunk-context-reconstruction",
"2:rag-l0-domain-entity-hint",
"3:rag-chunk-context-reconstruction",
"4:rag-l0-domain-entity-hint"
],
"matchedKeywords": [
"neighbor chunk",
"same section",
"breadcrumb"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"rag-chunk-context-reconstruction",
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction",
"rag-l0-domain-entity-hint"
],
"omittedSources": [],
"rerankTopSource": "rag-chunk-context-reconstruction",
"failedChecks": []
},
{
"caseId": "chat-l0-domain-hint",
"scenario": "chat",
"query": "Should L0 keyword matching decide the final retrieval result?",
"dataShape": "lookupResult",
"hitLevel": "strong",
"passed": true,
"firstExpectedRank": 1,
"topCandidates": [
"1:rag-l0-domain-entity-hint",
"2:rag-chunk-context-reconstruction",
"3:rag-l0-domain-entity-hint",
"4:rag-chunk-context-reconstruction"
],
"matchedKeywords": [
"domain detector",
"entity extractor",
"metadata filter"
],
"breadcrumbMatched": true,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction",
"rag-l0-domain-entity-hint",
"rag-chunk-context-reconstruction"
],
"omittedSources": [],
"rerankTopSource": "rag-l0-domain-entity-hint",
"failedChecks": []
},
{
"caseId": "chat-l0-filter-fallback",
"scenario": "chat",
"query": "RAG query was over-filtered by L0 and filtered vector search returned low quality evidence. What should happen?",
"dataShape": "lookupResult",
"hitLevel": "weak",
"passed": false,
"firstExpectedRank": null,
"topCandidates": [
"1:rag-l0-filter-decoy",
"2:rag-l0-filter-decoy"
],
"matchedKeywords": [
"low quality"
],
"breadcrumbMatched": false,
"selectedAttempt": "FILTERED_VECTOR",
"fallbackReason": null,
"evidenceStatus": "supported",
"includedSources": [
"rag-l0-filter-decoy",
"rag-l0-filter-decoy"
],
"omittedSources": [],
"rerankTopSource": "rag-l0-filter-decoy",
"failedChecks": [
"expected document not found",
"selected attempt mismatch: expected UNFILTERED_VECTOR_RETRY, got FILTERED_VECTOR",
"fallback reason mismatch: expected one of [filtered_vector_low_quality, filtered_vector_no_evidence], got <none>",
"rerank top source mismatch: expected rag-l0-filter-fallback, got rag-l0-filter-decoy",
"expected context sources missing: rag-l0-filter-fallback"
]
}
]
}
+33
View File
@@ -0,0 +1,33 @@
# RAG Retrieval Baseline
Generated at: `2026-07-28T06:48:56.421877+00:00`
## Aggregate
| Metric | Value |
|---|---:|
| Cases | 7 |
| Top K | 5 |
| Passed | 6 |
| Failed | 1 |
| Pass rate | 0.8571 |
| LookupResult fixtures | 7 |
| Recall@K | 0.8571 |
| Strong hit rate | 0.8571 |
| Strong hits | 6 |
| Medium hits | 0 |
| Weak hits | 1 |
| Misses | 0 |
| Average first hit rank | 1.0 |
## Cases
| Case | Scenario | Pass | Hit | Attempt | Fallback | Evidence | First Expected Rank | Top Candidates | Failed Checks |
|---|---|---|---|---|---|---|---:|---|---|
| chat-mysql-connection-pool | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:mysql-connection-pool<br>2:mysql-connection-pool | |
| chat-diagnosis-flow | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:incident-diagnosis-flow<br>2:incident-diagnosis-flow | |
| aiops-payment-latency-alert | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:payment-service-latency<br>2:aiops-alert-scope-control<br>3:payment-service-latency<br>4:aiops-alert-scope-control | |
| aiops-prometheus-alert-scope | aiops | true | strong | FILTERED_VECTOR | | supported | 1 | 1:aiops-alert-scope-control<br>2:payment-service-latency<br>3:payment-service-latency<br>4:aiops-alert-scope-control | |
| chat-rag-chunk-context | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-chunk-context-reconstruction<br>2:rag-l0-domain-entity-hint<br>3:rag-chunk-context-reconstruction<br>4:rag-l0-domain-entity-hint | |
| chat-l0-domain-hint | chat | true | strong | FILTERED_VECTOR | | supported | 1 | 1:rag-l0-domain-entity-hint<br>2:rag-chunk-context-reconstruction<br>3:rag-l0-domain-entity-hint<br>4:rag-chunk-context-reconstruction | |
| chat-l0-filter-fallback | chat | false | weak | FILTERED_VECTOR | | supported | | 1:rag-l0-filter-decoy<br>2:rag-l0-filter-decoy | expected document not found<br>selected attempt mismatch: expected UNFILTERED_VECTOR_RETRY, got FILTERED_VECTOR<br>fallback reason mismatch: expected one of [filtered_vector_low_quality, filtered_vector_no_evidence], got <none><br>rerank top source mismatch: expected rag-l0-filter-fallback, got rag-l0-filter-decoy<br>expected context sources missing: rag-l0-filter-fallback |
+31 -57
View File
@@ -1,66 +1,40 @@
# SuperBizAgent 面试资料包
# SuperBizAgent 面试资料
**更新日期**:2026-07-24
**当前主叙事**:单 Diagnosis ReAct Agent + 确定性 Harness(ISS-014 后)
## 当前入口
| 文档 | 用途 |
|---|---|
| [architecture-evolution-deep-dive.md](architecture-evolution-deep-dive.md) | **主材料**:架构演进深读(为什么设计 / 问题 / 重构 / 业界对比 / 图与白板) |
| [issues-interview-stories.md](issues-interview-stories.md) | **故事材料**:从已解决 Issues 提炼的可讲案例(STAR / 追问 / 挂载演进) |
| [topic-evidence-attribution-and-gates.md](topic-evidence-attribution-and-gates.md) | **主题深读**:证据归因幻觉 + 质量门禁(质量辨识度主故事) |
配套现行架构事实(非面试话术):
| 文档 | 用途 |
|---|---|
| [mvp/architecture/README.md](../mvp/architecture/README.md) | 当前架构文档入口 |
| [mvp/architecture/current-mvp-architecture.md](../mvp/architecture/current-mvp-architecture.md) | 分层、主链、API、安全边界 |
| [mvp/demo/README.md](../mvp/demo/README.md) | Demo 运行与输出 |
| [mvp/issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md](../mvp/issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md) | 架构冻结后的运行质量收敛 |
## 一句话定位
SuperBizAgent 是一个面向企业故障诊断场景的 Agent 工程项目。它把用户问题或 AIOps 告警转换成可追踪的 Agent 执行链路,并把工具证据、模型步骤、最终答案、自评估和用户反馈统一沉淀到诊断 Trace 中。
SuperBizAgent 是面向故障诊断的可追踪 Agent 系统:业务侧一个 Diagnosis Agent 负责推理与写 Draft;Harness 负责预算、工具边界、证据验真、语义审查与安全发布。每次执行用 `sessionId + runId` 回放统一 Timeline。
## 面试重点
## 建议使用方式
- **Agent 编排**:Chat 复杂问题走 `Planner -> Executor -> Verifier`;AIOps 告警入口走 `Supervisor -> Planner / Executor`。
- **工具证据链**:知识库、日志、指标、Prometheus 告警都通过显式工具调用进入链路,并记录到 `tool_invocation`。
- **可追踪诊断**:一次诊断对应一个 `sessionId`,可通过 `GET /api/diagnosis/{sessionId}/trace` 回放。
- **质量门禁**:Chat Verifier 校验 groundedness;AIOps 规则评估检查报告完整性、payload 聚焦和证据工具覆盖。
- **RAG 工程化**:`lookup_knowledge` 是显式 Agent Tool,底层通过 Spring AI VectorStore 主路径 + Milvus SDK fallback。
- **反馈闭环**:用户反馈 `useful` 会沉淀 `case_library`,`not_useful` 保留 bad case 信号。
1. 先读深读文档 §0–§6 + §10,建立演进骨架。
2. 按 §15 默画白板图 A/C/D(当前主链、职责迁移、数据三层)。
3. 需要事实核对时回到 `mvp/architecture/*`,不要用归档面试稿当现行口径。
4. 未完成项用 ISS-015 收尾,体现判断力而非完美叙事。
## 推荐阅读顺序
## 归档
1. `mvp/architecture/interview-one-pager.md`:一页式架构图和 2-5 分钟讲解。
2. `mvp/demo/ten-minute-interview-demo.md`:10 分钟现场演示脚本。
3. `interview/story-cases.md`:可复用的面试故事案例。
4. `interview/architecture.md`:面试版系统架构。
5. `interview/design-tradeoffs.md`:关键设计取舍。
6. `interview/demo-script.md`:更细的命令式演示脚本。
7. `interview/acceptance-checklist.md`:面试前验收清单。
8. RAG 专题文档:`rag-refactor-story.md`、`rag-vectorstore-interview-notes.md`、`rag-retrieval-quality-report.md`。
2026-07-24 之前的面试资料(多角色编排、双入口、旧 RAG/AIOps 讲解等)已移至:
## 核心演示链路
### Chat 诊断
```text
POST /api/chat
-> ChatService
-> Planner -> Executor -> Verifier
-> lookup_knowledge / query_logs / query_metrics
-> diagnosis_session + agent_step + tool_invocation
-> GET /api/diagnosis/{sessionId}/trace
-> POST /api/feedback
```
### AIOps 告警诊断
```text
POST /api/ai_ops
-> AiOpsService
-> PAYLOAD_TARGETED / AUTO_DISCOVERY
-> ai_ops_supervisor
-> planner_agent / executor_agent
-> queryPrometheusAlerts + logs + metrics + lookup_knowledge
-> alert report
-> aiops_rule_evaluation
-> GET /api/diagnosis/{sessionId}/trace
```
## 当前完成度
- Chat 诊断链路:可运行、可追踪、有 Verifier。
- AIOps 告警链路:可运行、可追踪、支持 payload scope control。
- RAG 检索链路:Spring AI VectorStore 主路径、Milvus SDK fallback、L0 hint、检索评测 baseline。
- Trace API:统一返回 session、agent steps、tool invocations 和 summary。
- Demo 材料:`mvp/demo/README.md`、`mvp/demo/ten-minute-interview-demo.md`。
## 主叙事
这个项目不是简单调用大模型,而是在做一个可审计、可验证、可回归的 Agent 诊断系统。模型可以规划和推理,但每一步工具证据、最终结论、Verifier 结果和用户反馈都能被 Trace API 回放。面试时重点展示“从问题到证据到答案到验证再到反馈”的闭环。
[archive/2026-07-24-legacy/](archive/2026-07-24-legacy/)
仅用于历史追溯,不代表当前 runtime、API 或验收口径。说明见同目录 `_ARCHIVE_NOTE.md`。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
# SuperBizAgent 面试资料包
## 一句话定位
SuperBizAgent 是一个面向企业故障诊断场景的 Agent 工程项目。它把用户问题或 AIOps 告警转换成可追踪的 Agent 执行链路,并把工具证据、模型步骤、最终答案、自评估和用户反馈统一沉淀到诊断 Trace 中。
## 面试重点
- **Agent 编排**:Chat 复杂问题走 `Planner -> Executor -> Verifier`;AIOps 告警入口走 `Supervisor -> Planner / Executor`。
- **工具证据链**:知识库、日志、指标、Prometheus 告警都通过显式工具调用进入链路,并记录到 `tool_invocation`。
- **可追踪诊断**:一次诊断对应一个 `sessionId`,可通过 `GET /api/diagnosis/{sessionId}/trace` 回放。
- **质量门禁**:Chat Verifier 校验 groundedness;AIOps 规则评估检查报告完整性、payload 聚焦和证据工具覆盖。
- **RAG 工程化**:`lookup_knowledge` 是显式 Agent Tool,底层通过 Spring AI VectorStore 主路径 + Milvus SDK fallback。
- **反馈闭环**:用户反馈 `useful` 会沉淀 `case_library`,`not_useful` 保留 bad case 信号。
## 推荐阅读顺序
1. `mvp/architecture/interview-one-pager.md`:一页式架构图和 2-5 分钟讲解。
2. `mvp/demo/ten-minute-interview-demo.md`:10 分钟现场演示脚本。
3. `interview/story-cases.md`:可复用的面试故事案例。
4. `interview/architecture.md`:面试版系统架构。
5. `interview/design-tradeoffs.md`:关键设计取舍。
6. `interview/demo-script.md`:更细的命令式演示脚本。
7. `interview/acceptance-checklist.md`:面试前验收清单。
8. RAG 专题文档:`rag-refactor-story.md`、`rag-vectorstore-interview-notes.md`、`rag-retrieval-quality-report.md`。
## 核心演示链路
### Chat 诊断
```text
POST /api/chat
-> ChatService
-> Planner -> Executor -> Verifier
-> lookup_knowledge / query_logs / query_metrics
-> diagnosis_session + agent_step + tool_invocation
-> GET /api/diagnosis/{sessionId}/trace
-> POST /api/feedback
```
### AIOps 告警诊断
```text
POST /api/ai_ops
-> AiOpsService
-> PAYLOAD_TARGETED / AUTO_DISCOVERY
-> ai_ops_supervisor
-> planner_agent / executor_agent
-> queryPrometheusAlerts + logs + metrics + lookup_knowledge
-> alert report
-> aiops_rule_evaluation
-> GET /api/diagnosis/{sessionId}/trace
```
## 当前完成度
- Chat 诊断链路:可运行、可追踪、有 Verifier。
- AIOps 告警链路:可运行、可追踪、支持 payload scope control。
- RAG 检索链路:Spring AI VectorStore 主路径、Milvus SDK fallback、L0 hint、检索评测 baseline。
- Trace API:统一返回 session、agent steps、tool invocations 和 summary。
- Demo 材料:`mvp/demo/README.md`、`mvp/demo/ten-minute-interview-demo.md`。
## 主叙事
这个项目不是简单调用大模型,而是在做一个可审计、可验证、可回归的 Agent 诊断系统。模型可以规划和推理,但每一步工具证据、最终结论、Verifier 结果和用户反馈都能被 Trace API 回放。面试时重点展示“从问题到证据到答案到验证再到反馈”的闭环。
@@ -0,0 +1,36 @@
# Archive Note
**归档日期**:2026-07-24
**状态**:历史面试材料,不代表当前 runtime / 架构口径
## 为何归档
本目录保存切换到「单 Diagnosis Agent + Harness」之前整理的面试资料包,内容仍以:
- Chat:`Planner -> Executor -> Verifier`(及后续五段 Gatekeeper/Composer)
- AIOps 独立入口与规则评估
- 旧 Trace / Demo 叙事
为主。现行可运行架构见:
- `mvp/architecture/`
- `interview/architecture-evolution-deep-dive.md`(当前面试深读主文档)
## 归档文件
| 文件 | 原用途 |
|---|---|
| `README.md` | 旧面试资料包入口 |
| `architecture.md` | 旧面试版系统架构 |
| `design-tradeoffs.md` | 旧设计取舍 |
| `demo-script.md` | 旧命令式演示脚本 |
| `acceptance-checklist.md` | 旧面试前验收清单 |
| `story-cases.md` | 旧故事案例 |
| `rag-*.md` | 旧 RAG 专题与验收笔记 |
| `aiops-*.md` | 旧 AIOps 讲解材料 |
## 使用边界
- 可作历史决策与旧 Demo 话术追溯。
- 不得当作当前 API、编排或验收标准。
- 若引用其中内容,须同时说明归档日期与现行替代文档。
+380
View File
@@ -0,0 +1,380 @@
# 从 Issues 提炼的面试故事
**更新日期**:2026-07-24
**用途**:从 `mvp/issues` 已解决问题中,筛出可讲、值得讲、能举一反三的案例
**配套**:[architecture-evolution-deep-dive.md](architecture-evolution-deep-dive.md)(讲演进骨架);本文讲**具体踩坑与决策**
---
## 0. 怎么用
| 场景 | 用法 |
|---|---|
| 行为面试 / 项目深挖 | 选 2–3 个 ★★★ 故事,用 STAR 讲 |
| 架构追问 | 把故事挂回演进阶段(Phase2 证据 / Phase3 重构 / 工程化) |
| 避免踩坑 | ★ 仅作补充,勿当主叙事;过时方案要说「后来被什么吸收」 |
**总原则**
> 面试官要的不是 Issue 编号,而是:**现象 → 根因分层 → 你选了什么杠杆 → 如何验证 → 后来边界怎么演进**。
---
## 1. 全景评分(先看这张表)
| Issue / 主题 | 面试价值 | 最适合回答的问题类型 | 一句话钩子 | 注意 |
|---|:---:|---|---|---|
| **证据归因幻觉** + Gatekeeper 链路 | ★★★ | 防幻觉 / 质量门禁 | 「工具调了,结论仍是编的」 | 旧角色名要映射到现在 Guard |
| **ISS-014** 单 Agent + Harness | ★★★ | 架构重构 / 为什么简化 | 「不是砍验证,是换装载层」 | 主故事,深读文档已覆盖 |
| **ISS-008** 窄范围越界 | ★★★ | Agent 约束 / Prompt 不够 | 「只问 CPU,却去查全家桶」 | 好举一反三 |
| **ISS-009** 负向证据 | ★★★ | 证据建模 / no-hit | 「没查到也是证据」 | 区分「无问题」vs「无数据」 |
| **ISS-001→002→004→015** 重复检索族 | ★★★ | 迭代加深 / Prompt vs 硬约束 | 「去重了仍狂调 20 次」 | 讲演进链,别只讲一次补丁 |
| **ISS-010** session/run 隔离 | ★★★ | 可观测 / 多轮正确性 | 「同会话多轮 Trace 串台」 | 和 runId 真理源强绑定 |
| **ISS-012** Token/上下文膨胀 | ★★★ | 成本 / ACI / 上下文工程 | 「工具返回把上下文撑爆」 | 接到 projection 三层数据 |
| **ISS-006 + fixtures + baseline diff** | ★★ | 工程化 / 回归 | 「改 Prompt 怎么知道没退步」 | 体现测试思维 |
| **ISS-007** 摘要失真 → 自证循环 | ★★★ | 信息通路设计 | 「证据在,摘要丢了关键句」 | 可与归因幻觉合并讲 |
| **ISS-013** SSE / 入口解耦 | ★★ | 后端工程 / 协议边界 | 「假流式 + Controller 过重」 | 偏工程,Agent 味稍淡 |
| **ISS-005** 证据状态契约 | ★★ | 契约 / 失败语义 | 「failed/no_evidence/deduped 语义乱」 | 作 001/007 的基础设施铺垫 |
| **RAG 子问题集** | ★★ | RAG 专题 | L0 降级、显式 Tool、breadcrumb | 合成一条 RAG 故事,勿逐条念 |
| **ISS-003** 总 Review | ★ | 过程 | 问题发现清单 | 不宜单独讲 |
| **ISS-015**(进行中) | ★★ | 诚实收尾 / 判断力 | 「架构冻了,策略还在收」 | 讲未完成,勿假装已完美 |
---
## 2. 推荐主故事包(面试只带这 5 个就够)
### 故事包怎么组合(15 分钟项目介绍)
```text
1) 开场架构(2 min) → 深读文档 / ISS-014 结论
2) 质量核心(4 min) → 证据归因幻觉 + Gatekeeper/EvidenceGuard
3) 约束演进(3 min) → 重复检索 001→002→硬预算 / 窄范围 008
4) 工程底座(3 min) → run 隔离 010 + eval harness 006
5) 重构与未完(3 min) → ISS-014 为什么合并 + ISS-015 诚实项
```
---
## 3. ★★★ 故事详解(可直接练口述)
### 3.1 证据归因幻觉(最高辨识度)
| 项 | 内容 |
|---|---|
| 来源 | `executor-evidence-attribution-hallucination` + ISS-007 + design-note 自证循环 |
| 完整深读 | [topic-evidence-attribution-and-gates.md](topic-evidence-attribution-and-gates.md) |
| 阶段 | Phase 2 正确性建设 |
| 现象 | 多次 E2E:工具已调 `lookup_knowledge/logs/metrics`,Verifier 仍大面积 `LOW_CONFID`;答案里出现 OOM、Full GC 次数等**工具返回里没有的「精确事实」** |
| 错误归因(你要主动否定) | 「是不是 Verifier 太严 / 只认 RAG?」——查 `evidence_refs` 后发现并非如此 |
| 真根因 | **Executor 证据归因幻觉**:把 runbook/历史模式/模型常识写进「当前已证实事实」,未区分 direct / reference / hypothesis / missing |
| 解法演进 | ① 结构化 claim + binding(invocation/path/excerpt)② **代码 Gatekeeper** 验引用 ③ Verifier 只判可推导 ④ Composer 控表达 → 后来迁到 EvidenceGuard + SemanticGuard + Release |
| 验证 | 固定 session 表(groundedness、no_evidence 计数);eval fixture;Trace 可指出「哪条 claim 无 binding」 |
| 现行映射 | Gatekeeper → **EvidenceGuard**;Verifier → **SemanticGuard**;最终出门 → **Release** |
**STAR 口述(约 90 秒)**
> 我们诊断链路经常 LOW_CONFID。第一反应像是验证器太狠,但拉 Trace 发现工具其实调用成功了。
> 对比答案和 tool raw 后定位到:模型会把知识库里的「常见故障模式」写成「这次故障已观测事实」。
> 所以我们把「有没有这句证据」从 LLM 判断里拆出来,做成代码级引用校验;模型只负责在已验真片段上做推导。
> 这直接把问题从「提示词求稳」升级成「证据所有权与类型系统」。
> 后来重构单 Agent 时,这层语义保留了,只是从流水线角色变成了 Harness 门禁。
**追问预备**
- Q: 为什么不靠更强模型?
A: 分布上仍会混用常识与观测;确定性校验可回归、可解释。
- Q: excerpt 子串匹配会不会太死?
A: 对防伪造必须偏严;表达层再允许归纳,但不允许无根引用。
- Q: 和 RAG 引用角标有何不同?
A: 角标常是生成时装饰;我们校验的是**当次 Run 的 tool_call 所有权**。
**举一反三**
- 客服:「政策规定 7 天」≠「本单已同意退款」
- 代码 Agent:「README 说应有测试」≠「本 PR 已有测试」
---
### 3.2 窄范围查询越界(ISS-008)
| 项 | 内容 |
|---|---|
| 现象 | 用户只要确认 `payment-service` 是否有 HighCPU 告警;Agent 却扩展到内存、日志、根因故事 |
| 根因 | ReAct 默认「有工具就多查」;Prompt 未区分 **observation 任务** vs **root-cause 任务**;claim 类型未收窄 |
| 解法 | 窄范围只允许 `observation` / `negative_observation`;禁止随手 root_cause;工具选择与输出 schema 双约束 |
| 验证 | narrow-highcpu 类 fixture:该 PASS 的 observation 不因「没讲根因」被打成失败 |
| 现行 | DiagnosisDraft 分析项仍强调证据绑定;范围控制在 Agent 指令 + Guard 语义审查 |
**金句**
> Agent 的能力上限往往不是「会不会查」,而是「知不知道何时停、查什么算完成」。
**举一反三**
- SQL Agent:用户要 `SELECT count` 时禁止顺手 `UPDATE`
- 调查 Agent:「只要时间线」时禁止输出处置工单
---
### 3.3 负向证据(ISS-009)
| 项 | 内容 |
|---|---|
| 现象 | 工具明确 no-hit 时,模型要么不说,要么说成「已排除该根因」,且引用对不上 |
| 根因 | 只建模「命中证据」,没有一等公民的 **no_evidence 路径**(如 `$.no_evidence`) |
| 解法 | `negative_observation` + 固定 raw_path/excerpt 契约;Gatekeeper 校验「无证据」也可以是合法引用 |
| 价值 | 排障里「查过没有」改变后验;也避免虚假排除 |
**金句**
> 在证据系统里,空结果若不可引用,模型就会用语言填补真空——那就是幻觉温床。
---
### 3.4 重复检索演进链(ISS-001 → 002 → 004 → 015)
这是**最好的「迭代加深」故事**:同主题三次升级约束强度。
```text
ISS-001 同文档内容重复进上下文
→ session 文档去重 / Prompt「别重复」
ISS-002 去重后仍 lookup 20+ 次(换 query 刷同一域)
→ 根因:约束只打在 Planner,Executor 不知情
→ Executor 注入 map + 每域一次等 Prompt 约束
ISS-004 Prompt 仍挡不住「再确认一次」
→ 设计域级水位 / 硬限制(后被架构变更吸收)
ISS-014/015 约束归属变化
→ 不再靠 Executor 旁路状态打补丁
→ Harness 预算 + Agent 硬停止 + 重复 lookup 策略(015 进行中)
```
**面试怎么讲这条链**
> 第一阶段我们以为是重复文档,做了内容去重。
> 第二阶段发现模型换关键词继续刷,说明**去重粒度错了**,且约束注入点错了(只告诉了 Planner)。
> 第三阶段承认 Prompt 约定不是安全边界,必须**预算/次数硬停止**。
> 架构重构后,这些不再散落在 Tool 旁路,而进入统一 Harness 控制面。
> 这说明我处理 Agent 问题的习惯是:先观测 → 分层根因 → 逐步把约束从「软」推到「硬」,并在架构变了以后迁移装载点,而不是叠补丁。
**对应业界概念**:Action masking / tool budget / circuit breaker;不是调参玄学。
---
### 3.5 Session / Run Trace 隔离(ISS-010)
| 项 | 内容 |
|---|---|
| 现象 | 同 `sessionId` 多轮诊断时,步骤/工具/评价串台或「取最新一条」导致回放错乱 |
| 根因 | 缺少把**一次执行**定为真理源的 `runId`;查询与写入未全程 exact id |
| 解法 | `diagnosis_run`;step/invocation/trace 均挂 run;API 强制 `runId`;禁止 latest 语义 |
| 价值 | 评测、排障、面试 Demo 都依赖可复现回放 |
**金句**
> 可观测性若不能精确到一次 Run,就只是日志堆,不是诊断系统的记忆。
**举一反三**
- 工作流引擎的 `workflowId` vs `runId`
- CI 的 pipeline vs job attempt
---
### 3.6 Token 与上下文膨胀(ISS-012)→ ACI / 三层数据
| 项 | 内容 |
|---|---|
| 现象 | Executor 上下文暴涨;工具结果含 debug/rerank/大段重复;成本与截断不可控 |
| 根因 | Tool 返回**面向开发者**而非 Agent;完整 raw 与模型可见视图未分离 |
| 解法方向 | Token 可观测;硬预算;结果投影;证据引用带稳定 id(后由 ISS-014 的 Redis canonical + projector 落地) |
| 现行 | canonical / projection / durable audit 三层 |
**金句**
> 上下文工程首先是接口设计问题:Agent 的观察通道必须有界,验真通道才能完整。
---
### 3.7 单 Agent + Harness 重构(ISS-014)
深读文档已写透,这里只留**Issue 视角的故事钩子**:
| 项 | 内容 |
|---|---|
| 触发 | 多角色 = 外层重复 ReAct;JSON 接力;Token;失败语义组合爆炸 |
| 保留 | 物理验真 → 语义审查 → 发布 的正确性模型 |
| 迁移 | 角色流水线 → 执行边界(Harness) |
| 证据 | 阶段 E2E:成功诊断 ~8k tokens / 1 次 tool;Knowledge Query 独立路径修复 invalid schema |
**和 3.1 的关系(必说清)**
> 014 不是推翻 007/归因幻觉的成果,而是避免用「五个 LLM 角色」去实现本该由一个 ReAct + 一层确定性门禁完成的事。
---
### 3.8 评测 Harness(ISS-006 + fixtures + baseline diff)
| 项 | 内容 |
|---|---|
| 动机 | 只有 Demo 无法判断改 Prompt/Tool 是否退步 |
| 做法 | 固定 case;expected tools/verdict/keywords;**确定性** trace 校验(非一上来 LLM-as-judge);baseline + diff |
| 价值 | 把 Agent 质量从「感觉」变成「回归」 |
**金句**
> 没有 baseline diff 的 Agent 迭代,只是在用生产用户当测试集。
**注意**:面试强调「先确定性检查,再考虑 LLM judge」,显得克制。
---
### 3.9 SSE 与入口解耦(ISS-013)
| 项 | 内容 |
|---|---|
| 现象 | `/api/chat` 与 `/api/chat_stream` 双入口;stream 实为整答后假分片;Controller 编排过重 |
| 解法 | 唯一 `POST /api/chat` named SSE;UseCase 拥有业务;Controller 只协议;disconnect → cancel Run |
| 适合 | 问到 Spring/API 设计、背压、职责边界时 |
---
## 4. ★★ 可合并讲的「专题束」
### 4.1 RAG 专题束(不要逐 Issue 报菜名)
把 `mvp/issues/rag/*` 收成 **一条 2 分钟故事**:
```text
问题簇:
L0 关键词当终局、breadcrumb 不进向量、切片丢层级、
无 packing/rerank、分数语义不清、Advisor 隐式注入 vs 显式 Tool
收敛原则:
1) 检索决策要对 Agent 可见 → lookup_knowledge 保持 Tool
2) L0 降级为 hint,不替代语义召回
3) 向量主路径可演进(VectorStore)+ 过渡期 fallback
4) 召回质量与「能否被引用验真」一起设计
```
**面试官若只问 RAG**:用这条;若问 Agent 质量:退回 3.1。
### 4.2 证据契约束(ISS-005 + 007 + 结构化输出设计笔记)
```text
统一 evidence 状态:supported / no_evidence / deduped / failed
摘要不可当唯一证据源
Executor 产出可绑定结构,Gatekeeper 验,Verifier 判
```
适合接在「你们怎么保证工具结果语义一致」类问题。
---
## 5. 不建议当主故事的
| 项 | 原因 | 若被问到怎么说 |
|---|---|---|
| ISS-003 总 Review | 清单型,缺单点冲突 | 「那是问题发现基线,具体落地看 005/006/014」 |
| ISS-004 原文方案细节 | 实现被 014/015 吸收,细节易过时 | 「方向是硬水位,装载点已迁到 Harness 预算/停止策略」 |
| 归因幻觉的旧 Prompt 补丁 alone | 不完整 | 必须接到 Gatekeeper/Guard |
| 未归档的「计划中」口吻 | 很多已 done | 统一用「已归档 / 被 014 吸收 / 015 进行中」三态 |
---
## 6. 问题类型 → Issue 速查
| 面试官问… | 优先故事 |
|---|---|
| 怎么防幻觉? | 3.1 归因幻觉 + Guard 映射 |
| Prompt 够不够? | 3.4 重复检索链 + 3.2 窄范围 |
| 多 Agent 为什么又合并? | 3.7 ISS-014(挂 3.1 证明没砍质量) |
| 成本 / Token? | 3.6 + 数据三层 |
| 如何回归? | 3.8 eval |
| 如何调试一次错误诊断? | 3.5 run 隔离 + Timeline |
| 没找到证据怎么办? | 3.3 负向证据 + Release fallback |
| SSE / 接口设计? | 3.9 |
| RAG 怎么做的? | 4.1 专题束 |
| 还有什么没做完? | ISS-015:硬停止、Repair schema、reasoning 治理、信息化 fallback |
---
## 7. 与架构演进的挂载图
```text
Phase1 能跑
└─ 暴露:重复检索 001/002
Phase2 能验
├─ 证据状态 005
├─ 归因幻觉 + 007 自证循环
├─ 窄范围 008 / 负向证据 009
├─ eval 006 / baseline
└─ run 隔离 010
Phase2 负债
└─ Token 012、入口 013、角色编排税
Phase3 能控
└─ 014 单 Agent + Harness(吸收 004/012/013 与证据门禁语义)
Phase4 治理中
└─ 015 停止策略 / Repair / reasoning / fallback 信息量
```
---
## 8. 建议你精炼的「个人贡献表述」模板
按真实参与度改主语,结构建议:
```text
我负责/主导了 ___(问题)。
通过 Trace 看到 ___(证据),排除了 ___(错误假设)。
方案上选择 ___ 而不是 ___,因为 ___。
用 ___(fixture/E2E/指标)验证。
后续在 014 重构中,该能力迁移为 ___,我学到 ___。
```
示例(归因幻觉):
```text
我负责排查 Chat 诊断大面积 LOW_CONFID。
通过对比 tool raw 与最终答案,确认是证据归因幻觉而非 Verifier 误杀。
推动「结构化 claim + 代码 Gatekeeper + Verifier 只做推导」而不是继续堆 Prompt。
用固定 E2E session 与 eval fixture 回归。
014 重构后该语义保留为 EvidenceGuard/SemanticGuard/Release。
```
---
## 9. 源文件索引
| 故事 | 路径 |
|---|---|
| 归因幻觉 | `mvp/issues/archived/executor-evidence-attribution-hallucination.md` |
| 自证/摘要 | `mvp/issues/archived/ISS-007-...` / `design-notes/executor-self-evidence-loop-design-note.md` |
| 窄范围 | `mvp/issues/archived/ISS-008-...` |
| 负向证据 | `mvp/issues/archived/ISS-009-...` |
| 重复检索 | `ISS-001` `ISS-002` `ISS-004` |
| Run 隔离 | `ISS-010` |
| Token | `ISS-012` |
| SSE | `ISS-013` |
| 重构 | `ISS-014-single-react-agent-harness-aci-ptk-refactor.md` |
| 评测 | `ISS-006` `expand-diagnosis-eval-fixtures` `diagnosis-eval-baseline-diff` |
| 进行中 | `mvp/issues/active/ISS-015-...` |
| RAG 簇 | `mvp/issues/rag/*` |
---
## 10. 自测
1. 不看文档,讲清「工具调用成功为何仍 LOW_CONFID」的根因与门禁分层。
2. 用 001→002→004→015 说明你如何升级约束强度。
3. 画旧 Gatekeeper 到新 EvidenceGuard 的映射,并说明 014 保留了什么。
4. 举一个负向证据防止的错误用户话术。
5. 用一句话说明 eval harness 为什么先做确定性检查。
能答 1–3,项目深挖通常已够用;4–5 用于区分「做过功能」和「有质量体系」。
@@ -0,0 +1,739 @@
# 主题深读:证据归因幻觉 + 现行质量门禁
**用途**:巩固知识 + 面试准备 + 举一反三
**不是**:逐字讲稿、旧 Issue 复述、过时五段流水线说明书
**材料日期**:2026-07-24
**叙事原则**:**以现行设计为主讲;早期 Issue 只说明「问题从哪来」**
**主题定位**:质量辨识度主故事——「工具调了,结论为何仍不能直接给用户」
**现行依据(面试默认口径)**
| 层级 | 路径 |
|---|---|
| 架构 | `mvp/architecture/current-mvp-architecture.md` |
| 编排 | `mvp/architecture/agent-orchestration.md` |
| 门禁 | `mvp/architecture/harness-quality-gates.md` |
| Draft 契约 | `.../harness/contract/DiagnosisDraft.java`、`AnalysisKind.java` |
| 物理验真 | `.../harness/guard/evidence/EvidenceGuard.java` |
| 语义审查 | `.../harness/guard/semantic/SemanticGuard.java`、`semantic-guard-prompt.md` |
| 发布 | `.../harness/release/DiagnosisReleaseUseCase.java`、`SafeFallbackFactory.java` |
| Agent 规则 | `src/main/resources/prompts/diagnosis-agent-prompt.md` |
**历史依据(只作起源,不代表 runtime)**
- `mvp/issues/archived/executor-evidence-attribution-hallucination.md`(2026-07-07,旧 Executor 链路)
- Phase2 Gatekeeper / `executor_evidence_v2` 归档文档
**配套**
- 演进骨架 → [architecture-evolution-deep-dive.md](architecture-evolution-deep-dive.md)
- 故事索引 → [issues-interview-stories.md](issues-interview-stories.md) §3.1
---
## 0. 怎么用 / 怎么讲
| 目标 | 用法 |
|---|---|
| 巩固 | 先背现行三道门与 `DiagnosisDraft` 引用闭包,再看历史问题为何逼出这套设计 |
| 面试 | **先讲现在怎么拦,再补一句早期怎么发现**;禁止把主链路讲成 Planner→Executor→Verifier |
| 举一反三 | 用现行组件名迁移到客服/代码/合规场景 |
**开场主线(先背这句,现行口径)**
> 当前系统里,Diagnosis Agent 是唯一报告作者,它在 ReAct 里查只读工具并写出结构化 `DiagnosisDraft`。
> 但 Draft **默认不能出门**:必须先过 **EvidenceGuard(0 LLM,验 tool_call 归属与投影)**,再过 **SemanticGuard(隔离单轮,判是否越证)**,最后由 **Release Policy** 决定公开报告还是 SAFE_FALLBACK。
> 这套门禁要防的核心失败,是早期线上已经见过的 **证据归因幻觉**:有工具调用,却把 runbook/常识写成「本 Run 已证实事实」。
**禁止的过时口径**
| 不要说 | 要说 |
|---|---|
| 我们主链路是 Planner→Executor→Gatekeeper→Verifier→Composer | 单 Diagnosis Agent + Harness 门禁 |
| Gatekeeper 验 `source_invocation_id + raw_path + excerpt` | EvidenceGuard 验 `tool_call_id` + 当前 Run canonical/projection |
| Verifier 输出 LOW_CONFID/PASS | SemanticGuard 输出 `SUPPORTED` / `UNSUPPORTED` |
| 工具有 metrics/Prometheus | 现行诊断 Tool:`lookup_knowledge` / `query_logs` / `query_mysql` |
| 引用主键是 DB `tool_invocation.id` | 框架 `tool_call_id`;完整调用在 Redis canonical |
**四轴(现行)**
1. **谁写报告**:仅 Diagnosis Agent
2. **谁保证引用真**:EvidenceGuard + Redis canonical(Harness only)
3. **谁保证语义不越界**:SemanticGuard(无 Tool、无记忆)
4. **谁决定用户看见什么**:Release Policy(Draft ≠ 公开 SSE)
**图目录**
| 图 | 位置 | 白板优先级 |
|---|---|---|
| 现行主链(质量视角) | §1 | ★★★ |
| DiagnosisDraft 引用闭包 | §2 | ★★★ |
| EvidenceGuard 校验步骤 | §3 | ★★★ |
| AnalysisKind × EvidenceStatus | §3.3 | ★★ |
| SemanticGuard 输入冻结 | §4 | ★★★ |
| Release / Repair / Fallback | §5 | ★★★ |
| 数据三层如何服务验真 | §6 | ★★ |
| 历史问题 → 现行映射(30 秒) | §8 | ★★ |
| 白板速画 | §13 | ★★★ |
---
## 1. 现行设计:质量门禁长什么样
### 1.1 在系统中的位置
```text
POST /api/chat (SSE)
→ ChatApplicationUseCase
→ Intent Router → DIAGNOSIS
→ Diagnosis ReAct Agent
只读 Tool × 3,观察的是 projection
产出 DiagnosisDraft
→ DiagnosisReleaseUseCase
EvidenceGuard →(可选 EvidenceRepair 一次)→ SemanticGuard → Release
→ 公开 content | SAFE_FALLBACK | failure
```
```mermaid
flowchart TB
Q["query + safe previous_turn"] --> A["Diagnosis Agent<br/>唯一报告作者 · ReAct"]
A <--> TB["ToolBoundary"]
TB --> Redis["Redis canonical<br/>Harness only"]
TB --> Proj["bounded agent_result"]
Proj --> A
A --> Draft["DiagnosisDraft<br/>内部制品"]
Draft --> RU["DiagnosisReleaseUseCase"]
RU --> EG["EvidenceGuard · 0 LLM"]
EG -->|invalid| RP["EvidenceRepair 最多一次"]
RP --> EG
EG -->|valid snapshot| SG["SemanticGuard · 隔离单轮"]
SG -->|SUPPORTED| Out["Release SUCCESS<br/>公开 typed report"]
SG -->|UNSUPPORTED / 不可用| FB["SAFE_FALLBACK"]
EG -->|仍 invalid| FB
```
**面试一句话**
> Agent 负责「尽量基于证据写对」;Harness 负责「写错了也不能当成功答案发出去」。
### 1.2 职责切分(现行,必背)
| 组件 | 做 | 不做 |
|---|---|---|
| **Diagnosis Agent** | 规划、调 Tool、写完整 Draft、证据不足时写 limitations | HTTP/SSE、预算、物理验真、语义终审、发布 |
| **ToolBoundary** | schema/只读/预算、写 canonical、projector、只回有界观察 | 业务推理 |
| **EvidenceGuard** | Draft 结构、analysis 闭包、`tool_call_id` 属本 Run、READY/可引用、kind↔status、投影可解析 | 调模型、改报告语义 |
| **EvidenceRepair** | 在证据失败时尝试一次结构化修复 | 无限重试、绕过验真 |
| **SemanticGuard** | 在 verified snapshot 上判断整份报告是否被支持 | Tool、记忆、Redis、改写报告、部分放行 |
| **Release** | SUPPORTED 才公开 Draft;否则固定 fallback/failure | 把未验证 Draft 流式出去 |
对应代码入口:`DiagnosisReleaseUseCase.execute(run, query, draft)`。
### 1.3 和「纯 ReAct Demo」的差(质量视角)
```text
纯 ReAct: Model ↔ Tools → 文本直接给用户
现行: Model ↔ ToolBoundary/projection → DiagnosisDraft
→ EvidenceGuard → SemanticGuard → Release → SSE
+ run 预算/取消 + Timeline(EVIDENCE/SEMANTIC/RELEASE 事件)
```
---
## 2. 现行契约:`DiagnosisDraft` 如何逼归因诚实
### 2.1 结构(代码事实)
```text
DiagnosisDraft
conclusion?
text
based_on_analysis_ids[] ← 必须指向本 Draft 内 analysis_id
analysis[]
analysis_id ← 唯一
kind: NORMAL | NEGATIVE_OBSERVATION
text
tool_call_ids[] ← 至少一个;必须是本 Run 真实 id
action_plan[]
action
based_on_analysis_ids[]
requires_human_confirmation
recommendations[]
text
based_on_analysis_ids[]
limitations ← 必填 scope(EvidenceGuard 校验)
scope
missing_info[]
```
```mermaid
flowchart TB
C["conclusion / actions / recommendations"] -->|"based_on_analysis_ids"| A["analysis_id"]
A -->|"tool_call_ids"| T["本 Run tool_call_id"]
T --> Canon["Redis canonical<br/>runId + toolCallId"]
Canon --> Status["evidence_status<br/>FOUND / NO_EVIDENCE"]
A --> Kind["kind NORMAL / NEGATIVE"]
Kind -.->|"must match"| Status
```
**设计意图(对着归因幻觉)**
| 约束 | 防什么 |
|---|---|
| 每条 analysis 必须带 `tool_call_ids` | 「凭空结论」「常识当观测」 |
| conclusion 不直接绑 tool,只绑 analysis | 结论必须落在已声明的分析链上,形成闭包 |
| `limitations` 强制存在 | 证据不足时不能装成完整结案 |
| `kind` 二分 | 负向观察与正向命中不能混用同一类证据状态 |
### 2.2 Agent Prompt 里的硬规则(现行)
`diagnosis-agent-prompt.md` 关键口径(面试可直接引用思想,不必背原文):
1. **唯一报告作者**;内部规划,不对外输出 CoT。
2. **PreviousTurn 不是本 Run 证据**,不能引用其 tool_call_id。
3. 只有 `evidence_status=EVIDENCE_FOUND|NO_EVIDENCE` 且带真实 `tool_call_id` 的观察才能当证据。
4. **NORMAL** 只能引 FOUND;**NEGATIVE_OBSERVATION** 只能引 NO_EVIDENCE。
5. **NO_EVIDENCE ≠ 系统健康 / 已排除根因**。
6. Tool **ERROR 不是证据**,禁止引用。
7. 证据不足:`conclusion=null`,写 scope/missing_info,**禁止编根因**。
8. 不暴露 raw、凭据、内部错误、hidden reasoning。
**金句**
> Prompt 负责教 Agent「怎么写才诚实」;EvidenceGuard 负责「写得不诚实就过不了」。两者缺一不可,但**安全边界在代码**。
### 2.3 和早期「分区文案」的关系(30 秒)
早期 Issue 要求 Executor 输出「已证实 / 推测 / 缺口 / 动作」分区。
现行不是同一套 JSON 字段名,但**语义被结构吸收了**:
| 早期分区意图 | 现行落点 |
|---|---|
| 已证实事实 | `analysis`(NORMAL + FOUND) |
| 负向观察 | `analysis`(NEGATIVE_OBSERVATION + NO_EVIDENCE) |
| 证据缺口 | `limitations.missing_info` + 可空 `conclusion` |
| 建议动作 | `action_plan` / `recommendations`(必须 based_on analysis) |
| 禁止常识当事实 | Guard 不认无 tool_call 的 analysis;Semantic 审越证 |
---
## 3. EvidenceGuard:现行物理验真(0 LLM)
### 3.1 它在防什么
不是「这句话好不好听」,而是:
> 报告里声明依赖的每一个 tool_call,是否真的在本 Run 发生过、状态是否可引用、投影是否自洽、analysis kind 是否与 evidence_status 匹配,以及 conclusion/action 是否只引用存在的 analysis。
### 3.2 校验流水(按代码路径讲)
`EvidenceGuard.validate(RunContext, DiagnosisDraft)` 大致两段:
**A. Draft 结构与报告闭包**
- draft / analysis 非空
- `analysis_id` 存在且唯一
- `kind`、`text` 必填
- 每条 analysis 的 `tool_call_ids` 非空
- conclusion / action_plan / recommendations:text 非空,且 `based_on_analysis_ids` 非空、id 都认识
- `limitations.scope` 必填
**B. 逐 tool_call 验真**
对每个 `tool_call_id`:
1. 用 `runId + toolCallId` 生成 key,查 **Redis canonical**
2. 找不到 → `INVOCATION_MISSING`(典型:编造 id)
3. id 不一致 → `INVOCATION_ID_MISMATCH`
4. `!isReferencableBy(runId)` 或 agent_result 空 → `INVOCATION_NOT_REFERENCABLE`
(跨 Run、未 READY、不可引用状态)
5. `analysis.kind.accepts(invocation.evidenceStatus)`
- NORMAL ↔ EVIDENCE_FOUND
- NEGATIVE_OBSERVATION ↔ NO_EVIDENCE
否则 `EVIDENCE_KIND_MISMATCH`
6. 按 tool 反序列化 **agent_result 投影**(不是让模型再读 raw 讲故事):
- `lookup_knowledge` / `query_logs` / `query_mysql`
7. 投影内 `tool_call_id`、`evidence_status` 与 canonical 一致
8. FOUND 必须有可展示证据条目;NO_EVIDENCE 必须空列表且 count=0
9. 通过则写入 `VerifiedEvidence`,汇总为 `VerifiedEvidenceSnapshot`
```mermaid
flowchart TB
Draft["DiagnosisDraft"] --> S["结构 + analysis 闭包"]
S --> Loop["foreach tool_call_id"]
Loop --> Key["key = runId + toolCallId"]
Key --> Redis["Canonical store"]
Redis -->|missing| V1["INVOCATION_MISSING"]
Redis -->|not referencable| V2["NOT_REFERENCABLE"]
Redis -->|ok| K["kind vs evidence_status"]
K -->|mismatch| V3["KIND_MISMATCH"]
K --> P["Parse projection"]
P -->|invalid| V4["PROJECTION_*"]
P --> Snap["VerifiedEvidenceSnapshot"]
```
### 3.3 `AnalysisKind` × `EvidenceStatus`(高频考点)
```text
NORMAL → 只能绑 EVIDENCE_FOUND
NEGATIVE_OBSERVATION → 只能绑 NO_EVIDENCE
ERROR → 根本不是证据(Agent Prompt + 边界)
```
**面试例子**
| Agent 想说 | 错误绑法 | Guard |
|---|---|---|
| 「CPU 告警 92%」 | 编造 tool_call_id | MISSING |
| 「未查到池耗尽日志」 | kind=NORMAL 却绑 NO_EVIDENCE | KIND_MISMATCH |
| 「已排除内存泄漏」 | 仅 NO_EVIDENCE 却写排除结论 | 物理可能过,**Semantic 应 UNSUPPORTED** |
| 引用上一轮 previous_turn 的 id | 非本 Run canonical | MISSING / NOT_REFERENCABLE |
### 3.4 为什么验的是 projection 且 canonical 在 Redis
| 设计 | 原因 |
|---|---|
| Agent 只见 projection | 有界 ACI;降低上下文里的 debug 噪声与胡拼素材 |
| Guard 读 canonical 元数据 + 校验投影 | 确认「Agent 引用的 id」对应真实调用,且投影自洽 |
| Agent 不能访问 Redis | 防止自己翻 raw 再编第二套故事 |
| MySQL `tool_invocation` 只 metadata | 长期审计 ≠ 验真主存;完整 raw 短 TTL |
**与早期 Gatekeeper 的差异(讲清楚就加分)**
| 早期 Gatekeeper | 现行 EvidenceGuard |
|---|---|
| 多角色流水线中的一环 | Harness Release 路径上的确定性步骤 |
| `source_invocation_id` + `raw_path` + `excerpt` 字符串闭合 | `tool_call_id` + Run ownership + 投影结构/状态闭合 |
| 面向 `executor_evidence_v2` claims | 面向 `DiagnosisDraft` analysis 闭包 |
| 工具集合含 metrics 等 | 现行三 Tool;投影类型 Rag/Logs/Mysql |
语义继承:**都是 0 LLM 的物理/契约验真**;协议与装载层已现代化。
### 3.5 失败码怎么用于口述
挑几个最能讲故事的 `EvidenceViolationCode`:
| Code | 一句话 |
|---|---|
| `TOOL_REFERENCE_MISSING` | analysis 根本没绑工具 |
| `INVOCATION_MISSING` | 引用了不存在的 tool_call(归因造假) |
| `INVOCATION_NOT_REFERENCABLE` | 调用存在但不可作为证据(错 Run/未就绪/空结果) |
| `EVIDENCE_KIND_MISMATCH` | 负向/正向证据用错 kind |
| `ANALYSIS_REFERENCE_UNKNOWN` | 结论引用了不存在的 analysis_id |
| `PROJECTION_INVALID` | 投影与契约不一致,不能当干净证据 |
---
## 4. SemanticGuard:现行语义保险丝
### 4.1 输入被故意冻死
`SemanticGuardInput.from(query, draft, verifiedSnapshot)`:
```text
只给:
原始 query
+ 完整 Draft 视图
+ EvidenceGuard 产出的 verified snapshot
明确没有:
Tool / 记忆 / Redis / 主 Agent 回调 / 诊断历史
```
Prompt(`semantic-guard-prompt.md`)要求:
- 查:evidence 是否支持各 analysis;analysis 是否支持 conclusion;action/recommendation 是否越界;limitations 是否如实
- **禁止**改写、纠正、摘要扩展、**部分批准**
- 只返回 `{"verdict":"SUPPORTED|UNSUPPORTED","reason":"..."}`
### 4.2 它专门接住 EvidenceGuard 接不住的归因幻觉
EvidenceGuard 通过只说明:
> 「你引用的调用是真的,投影也合法。」
仍可能:
| 漏洞 | 例子 | 谁拦 |
|---|---|---|
| 真日志推不出该根因 | 只有超时日志 → 写「确定是死锁」 | SemanticGuard |
| 负向观察说成排除 | NO_EVIDENCE → 「不可能是池耗尽」 | SemanticGuard + Agent 规则 |
| 结论超出 analysis 集合语义 | analysis 只谈 A,conclusion 谈 B | SemanticGuard |
| 建议动作无分析支撑 | 乱给变更建议 | SemanticGuard + 结构上 based_on |
```mermaid
flowchart LR
EG["EvidenceGuard<br/>物理真"] --> SG["SemanticGuard<br/>语义立"]
SG -->|SUPPORTED| R["可发布"]
SG -->|UNSUPPORTED| F["Fallback<br/>保留 observed_facts"]
```
### 4.3 为什么必须隔离、且无 Tool
| 若 SemanticGuard 能再查库 | 后果 |
|---|---|
| 自建第二证据世界 | 与主 Agent / snapshot 不一致 |
| 「审稿时补证」 | 绕过用户可见的排查过程 |
| 又变成带 Tool 的第二 Executor | 归因问题换个角色重演 |
**金句**
> SemanticGuard 是保险丝,不是第二名侦探。
### 4.4 技术失败策略(现行)
- 输入/输出字节上限(`SemanticGuardLimits`)
- 同输入有限重试;仍失败 → `semanticUnavailable` fallback(有 snapshot 时仍可带已验证事实)
- 不把内部异常原文甩给用户
---
## 5. Release:Draft 与公开通道切断
### 5.1 `DiagnosisReleaseUseCase` 决策序
```text
1) EvidenceGuard.validate
2) 若失败 → EvidenceRepair 一次 → 再 validate
3) 仍失败 → EVIDENCE_VALIDATION_FAILED fallback
4) SemanticGuard.review(query, draft, snapshot)
5) SUPPORTED → SUCCESS(公开 candidate Draft + snapshot 元数据路径)
6) UNSUPPORTED → SEMANTIC_UNSUPPORTED fallback(可带 observed_facts)
7) Semantic 技术不可用 → SEMANTIC_UNAVAILABLE fallback
```
Timeline 会记:`EVIDENCE_GUARD_INITIAL` / `RECHECK` / semantic / release 决策(普通 Trace 可回放阶段,不靠「感觉」)。
### 5.2 SAFE_FALLBACK 在防什么
不是空白 500,而是**可信的不完整**:
| Fallback 类型 | 用户侧含义(思想) |
|---|---|
| 证据校验失败 | 引用/结构没过,不能确认根因;可带 validation 问题方向 |
| 语义不支持 | 已有可验证事实,但撑不起当前根因结论 |
| 语义不可用 | 有事实,但审不过/审不了,暂不发根因 |
`SafeFallbackFactory` 会从 snapshot 抽取有界 `observed_facts` / sources(有上限),并给出 `failure_stage`、`next_steps` 等——**在不泄 Prompt/raw/内部 Draft 细节的前提下**尽量可操作。
**金句**
> 我们宁可发布「已经核实到什么、卡在哪」,也不发布「流畅但未过门禁的完整故事」。
### 5.3 PreviousTurn 与门禁的衔接
- 仅 **同 Session、最近一次 DIAGNOSIS + SUCCESS + published_result** 可进下一轮
- Fallback/失败/raw **不进** PreviousTurn
- Prompt 明确:previous_turn **不可当本 Run 证据**
防止「上一轮没过门禁的句子」在下一轮被当成已证实事实——这是归因幻觉的跨轮版本。
---
## 6. Tool 边界:归因幻觉的上游防线
门禁是下游闸门;上游仍要减少「胡拼素材」。
```text
framework tool_call_id
→ exact Run / schema / read-only / budget
→ 执行
→ Redis canonical(request/raw/agent_result/status)
→ projector → bounded agent_result
→ 只把 projection 给 Agent
→ MySQL 仅 metadata audit
```
现行 Agent 可见 Tool 固定三个:
- `lookup_knowledge`
- `query_logs`
- `query_mysql`
**与归因的关系**
| 机制 | 作用 |
|---|---|
| 投影有界 | 少把 rerank/debug 大字段留给模型拼案情 |
| 统一 evidence_status | FOUND/NO_EVIDENCE/ERROR 语义稳定,供 kind 匹配 |
| 每调必有 tool_call_id | Draft 绑定有稳定主键 |
| 禁止 Agent 见 Redis | 不能「翻完整 raw 再假装引用」 |
---
## 7. Reasoning 明确不是证据(现行安全边界)
架构硬约束:
- Provider reasoning 若存在,进独立 `agent_reasoning_audit`
- **不进**普通 SSE、Trace 正文、Evidence Snapshot、业务判断
- **不能**绕过 EvidenceGuard / SemanticGuard
- 未返回则记 unavailable,**禁止伪造**
面试若被问「你们保存思考过程吗」:
> 审计与事实分离。思考不是 tool evidence,更不能当发布依据。
---
## 8. 历史问题:只用来回答「为什么要这套现行设计」
### 8.1 早期现象(30–45 秒够)
2026-07 旧 Chat 链路(Planner/Executor/Verifier…)上:
- 多次 E2E:`lookup_* / logs / metrics` 有调用,仍大量 `LOW_CONFID`
- 答案出现工具 raw 中不存在的「精确事故事实」(OOM 次数、慢 SQL 秒数等)
- 根因命名:**证据归因幻觉**——把 runbook/常识/他服事实写成「本会话已证实」
- 曾伴随摘要失真、自证闭环(自己总结再自己绑引用)
**正确用法**
> 这段证明「只靠模型自觉 + 事后 LLM Verifier」不够,必须把物理引用做成确定性约束,并把发布权从生成模型手里拿走。
**错误用法**
> 把整场面试讲成旧五段角色和 `executor_evidence_v2` 字段细节,却说不清现在的类名与 API。
### 8.2 语义迁移表(历史 → 现行)
| 历史概念 | 现行概念 | 说明 |
|---|---|---|
| Executor 综合答案 | Diagnosis Agent 写 Draft | 仍是模型生成,但是唯一作者 |
| claim + excerpt binding | analysis + `tool_call_ids` | 主键协议变更 |
| Gatekeeper | EvidenceGuard | 仍 0 LLM;装入 Release 用例 |
| Verifier LOW_CONFID | SemanticGuard UNSUPPORTED | 隔离输入;二元 verdict |
| Composer 控表达 | Draft 结构 + Release/Fallback | 表达权在 Agent,发布权在 Harness |
| 调低阈值换 PASS | **明确不做** | 用 fallback 信息量换体验 |
```mermaid
flowchart LR
H1["早期:归因幻觉被发现"] --> H2["正确性模型:物理验真+语义审+控表达"]
H2 --> H3["ISS-014:装载到单 Agent + Harness"]
H3 --> Now["现行:Draft→EG→SG→Release"]
```
### 8.3 一句话定位两阶段
> 早期 Issue 解决的是 **「要什么正确性」**;
> 现行架构解决的是 **「正确性如何成为默认运行路径」**。
---
## 9. 和业界主流的区别(用现行组件说)
| 常见做法 | 缺口 | 本项目现行 |
|---|---|---|
| 纯 ReAct 直接吐最终答案 | 无发布闸 | Draft 默认内部,Release 才公开 |
| 答案末尾 sources 角标 | 角标可假 | `tool_call_id` 必须在本 Run canonical 可解析 |
| 单一 LLM-as-Judge | 真伪与语义混判、不可复现引用检查 | EG 代码 + SG 隔离模型 |
| 质检 Agent 再带 Tool | 第二证据世界 | SG 无 Tool,冻结 snapshot |
| 离线 RAGAS | 不挡单次错误出门 | 在线门禁 + Timeline + eval 夹具 |
| 只靠更强模型 | 无工程边界 | 与模型代际正交的 Harness |
**三个不一样(现行表述)**
1. **引用是 Run 级所有权问题**,不是文案装饰。
2. **物理与语义拆分**,失败阶段可进 Trace / fallback。
3. **公开通道与生成通道切断**,SUPPORTED 才是成功产品语义。
---
## 10. 决策环(填的是现行答案)
| # | 问题 | 现行答案 |
|---|---|---|
| 1 | 威胁? | 带 tool 外观的完整假案情进入 SSE |
| 2 | 谁强制? | EG 强制引用;SG 强制语义;Release 强制发布 |
| 3 | 数据面? | canonical / projection / metadata audit;reasoning 另表 |
| 4 | 失败用户看到? | SAFE_FALLBACK(阶段、有界事实、下一步),非假成功 |
| 5 | 如何证明? | 单元/契约测 violation;E2E release_outcome;Timeline 事件 |
| 6 | 演进? | 误杀先查投影与 schema/Repair;不给 SG 加 Tool;停止策略见 ISS-015 |
---
## 11. 面试题库(默认用现行答)
### 11.1 90 秒主叙述(推荐背这个版本)
> 故障诊断里最危险的不是完全不查工具,而是查了一点真实信号就补成完整事故——我们早期在旧链路上把它定义为证据归因幻觉。
> **现在**的做法是:唯一 Diagnosis Agent 用 ReAct 查三个只读工具,只看投影,输出结构化 DiagnosisDraft;每条分析必须绑定本 Run 的 tool_call_id。
> Draft 先过 EvidenceGuard:纯代码检查结构闭包、调用是否存在于当前 Run 的 Redis canonical、证据状态是否与 NORMAL/负向观察匹配、投影是否自洽。
> 通过后生成 verified snapshot,再交给无工具的 SemanticGuard 做整份语义是否越证的审查。
> 只有 SUPPORTED 才经 Release 进入 SSE;否则走 SAFE_FALLBACK,宁可告诉用户已核实事实和卡住的阶段,也不发未验证根因。
> 所以质量辨识度是三句话:**可调用 ≠ 可归因;可归因 ≠ 语义成立;语义成立才可发布。**
### 11.2 为什么题
| 问题 | 现行得分点 |
|---|---|
| 怎么防幻觉? | Draft 绑定 tool_call_id → EG → SG → Release |
| 为什么 EG 不用模型? | 归属与状态是确定性的;要可回归 |
| 为什么还要 SG? | 真调用推不出假根因;负向≠排除 |
| 为什么 SG 不能有 Tool? | 冻结证据集,防第二世界 |
| 引用主键为什么是 tool_call_id? | 框架协议 id;与当次调用一致;不靠「最新 DB 行」 |
| Agent 能看 raw 吗? | 不能;只看 projection;canonical Harness only |
| 证据不足怎么办? | conclusion 可空 + limitations;或 fallback;不编根因 |
| 和旧 Gatekeeper 啥关系? | 语义祖先;现装在 Harness,协议已换 |
### 11.3 对抗题
**Q:这不就是多 Agent 质检吗?**
A:不是。业务侧只有一个带 Tool 的 Diagnosis Agent。SG 是无 Tool 的隔离单轮审查,属于 Harness 控制面,不是协作同事。
**Q:你们重构掉多角色后质量是不是弱了?**
A:弱的是重复的 LLM 角色编排;强的是默认路径上的确定性 EG + 发布切断。正确性模型保留,装载点从流水线角色变成 Release 用例。
**Q:投影校验不看 raw 原文子串,会不会漏?**
A:现行 EG 强调 **调用所有权 + 状态 + 投影结构自洽 + kind 匹配**,再交给 SG 做语义。上游靠 projector 把可引用证据做成稳定结构。若追问 excerpt 级闭合,可承认协议从早期 raw_path/excerpt 演进到投影契约,并强调 **不能引用 ERROR/跨 Run/不可引用调用** 仍是硬的。
**Q:用户体验会不会总是 fallback?**
A:体验做在「信息化 fallback + 成功路径的 limitations」,不是放宽 Guard。ISS-015 继续收敛停止策略与 fallback 信息量。
### 11.4 现场设计题
1. 给「工单退款 Agent」设计等价于 `tool_call_ids` + EG 的字段。
2. 若增加第四个 Tool,EG 要补哪些分支?kind/status 如何扩展?
3. Knowledge Query 路径(非完整 DIAGNOSIS)如何复用「引用必须真实」而不照搬整份 SemanticGuard?
4. 如何用 Timeline 事件向面试官演示一次 UNSUPPORTED 的失败阶段?
---
## 12. 原则清单(现行)
1. **唯一报告作者,多个确定性关卡**
2. **Draft 是 staging,SSE 是 production**
3. **引用主键 = 本 Run 的 framework tool_call_id**
4. **NORMAL / NEGATIVE 与 FOUND / NO_EVIDENCE 强匹配**
5. **NO_EVIDENCE 不是健康证明**
6. **ERROR 与跨 Run id 绝不能当证据**
7. **PreviousTurn 不是证据**
8. **SemanticGuard 冻结 snapshot,无 Tool**
9. **Reasoning 不是证据**
10. **历史 Issue 论证问题,现行代码定义答案**
---
## 13. 白板默画(只画现行)
### 13.1 图 A · 60 秒主链
```text
Diagnosis Agent → DiagnosisDraft
↓
EvidenceGuard (0 LLM, tool_call_id)
↓ verified snapshot
SemanticGuard (no tools)
↓ SUPPORTED?
Release → SSE or SAFE_FALLBACK
```
### 13.2 图 B · 45 秒闭包
```text
conclusion.based_on → analysis_id → tool_call_ids
↓
Redis canonical (this runId)
↓
FOUND / NO_EVIDENCE
↓
kind must match
```
### 13.3 图 C · 30 秒历史锚点(可选)
```text
早期发现:有 tool 仍假案情
→ 要物理验真 + 语义审 + 发布权
→ 现装在 EG / SG / Release
```
### 13.4 红线
- 画出 Planner/Executor/Composer 当主路径
- 说 Verifier 输出 LOW_CONFID 当现行 API
- SG 带检索箭头
- Draft 直连用户
- 说 metrics Tool 仍是诊断三件套之一(现行是 knowledge/logs/mysql)
---
## 14. 复习路径(偏现行)
| 步骤 | 动作 |
|---|---|
| 1 | 读 `diagnosis-agent-prompt.md` + `DiagnosisDraft` / `AnalysisKind` |
| 2 | 通读 `EvidenceGuard.validate` 与 `EvidenceViolationCode` |
| 3 | 读 `DiagnosisReleaseUseCase` + `semantic-guard-prompt.md` |
| 4 | 对照 `harness-quality-gates.md` 默画 §13 图 A/B |
| 5 | 用 §11.1 录音;再花 20 秒提早期归因幻觉作动机 |
| 6 | 扫一眼归档 Issue 标题与现象表即可,不背旧字段 |
---
## 15. 源文件索引
### 现行(主)
| 内容 | 路径 |
|---|---|
| 架构总览 | `mvp/architecture/current-mvp-architecture.md` |
| 执行序列 | `mvp/architecture/agent-orchestration.md` |
| 门禁 | `mvp/architecture/harness-quality-gates.md` |
| Draft | `src/main/java/.../harness/contract/DiagnosisDraft.java` |
| Kind/Status | `AnalysisKind.java` / `EvidenceStatus.java` |
| EG | `.../guard/evidence/EvidenceGuard.java` |
| SG | `.../guard/semantic/SemanticGuard.java` |
| Release | `.../release/DiagnosisReleaseUseCase.java` |
| Fallback | `.../release/SafeFallbackFactory.java` |
| Agent Prompt | `src/main/resources/prompts/diagnosis-agent-prompt.md` |
| SG Prompt | `src/main/resources/prompts/semantic-guard-prompt.md` |
### 历史(辅)
| 内容 | 路径 |
|---|---|
| 归因幻觉发现 | `mvp/issues/archived/executor-evidence-attribution-hallucination.md` |
| 自证闭环笔记 | `mvp/issues/design-notes/executor-self-evidence-loop-design-note.md` |
| 旧证据契约 | `mvp/architecture/archive/2026-07-22-legacy/executor-evidence-pipeline-refactor.md` |
| 重构承接 | `mvp/issues/archived/ISS-014-...` |
| 运行质量后续 | `mvp/issues/active/ISS-015-...` |
---
## 16. 自测(必须能用现行组件名回答)
1. 画出 Draft 从产生到 SSE 的完整门禁序,并标出哪步 0 LLM。
2. `NORMAL` 与 `NEGATIVE_OBSERVATION` 分别能绑哪种 `evidence_status`?
3. EvidenceGuard 如何发现「编造 tool_call_id」?
4. 为什么 conclusion 要 `based_on_analysis_ids` 而不是直接绑 tool?
5. SemanticGuard 的输入有哪三样?为什么不能有 Tool?
6. Evidence 失败时 Repair 最多几次?仍失败用户看到什么产品语义?
7. PreviousTurn 为什么不能提供可引用的 tool_call_id?
8. Reasoning 能否帮助 Draft 过 EG/SG?
9. 用 20 秒说明早期归因幻觉与现行三道门的关系(动机 vs 实现)。
10. 举一个「EG 通过但 SG 应 UNSUPPORTED」的例子。
---
## 17. 和旧版材料的关系
若你曾按「五段流水线 + excerpt 外键」准备:
- **保留**:归因幻觉定义、物理/语义拆分、发布切断思想
- **替换**:所有主路径类名、Tool 列表、verdict 枚举、引用主键、API
- **降级**:Executor/Gatekeeper/Composer 仅出现在「历史动机」小节
**面试默认叠词顺序**
```text
1. 现行:Agent → Draft → EG → SG → Release
2. 机制:tool_call_id、kind/status、snapshot、fallback
3. 动机:早期归因幻觉(可选一句)
4. 演进:正确性模型保留,装载进 Harness(若追问重构)
```
+11 -30
View File
@@ -1,60 +1,41 @@
# SuperBizAgent MVP 文档
**更新日期**:2026-07-23
**更新日期**:2026-07-29
本目录保存 MVP 阶段的架构、问题、演示、评测和数据表说明。当前材料按“当前入口”和“历史归档”拆开,避免把早期设计稿当成当前实现。
本目录保存 MVP 阶段的架构、工程纪要、问题、演示、评测和数据表说明。当前材料按“当前入口”和“历史归档”拆开,避免把早期设计稿当成当前实现。
## 当前入口
| 目录/文档 | 用途 |
|---|---|
| [architecture/README.md](architecture/README.md) | 当前 MVP 架构入口 |
| [architecture/README.md](architecture/README.md) | **现行架构**(系统现在怎么跑) |
| [engineering/README.md](engineering/README.md) | **工程纪要**(问题 / 决策 / 思路 / E2E 导读) |
| [architecture/current-mvp-architecture.md](architecture/current-mvp-architecture.md) | 当前可运行系统架构 |
| [architecture/agent-orchestration.md](architecture/agent-orchestration.md) | Agent 编排架构 |
| [architecture/harness-quality-gates.md](architecture/harness-quality-gates.md) | Harness 与质量门禁 |
| [architecture/session-trace-lifecycle.md](architecture/session-trace-lifecycle.md) | 会话与 Trace 生命周期 |
| [architecture/RAG知识检索架构.md](architecture/RAG知识检索架构.md) | 当前检索架构(py-rag 知识服务接入) |
| [issues/README.md](issues/README.md) | MVP issue 索引 |
| [tables/README.md](tables/README.md) | 当前 MySQL 表说明 |
| [demo/README.md](demo/README.md) | Demo 运行和演示材料 |
| [eval/README.md](eval/README.md) | 诊断评测材料 |
当前架构、运行链路和后续规划分别以 `architecture/`、`issues/README.md`、`tables/README.md` 以及 OpenSpec/devflow 的最新记录为准。
**读法**:改行为先看 `architecture/`;要理解「为什么这样定、踩过什么坑」再看 `engineering/`。
早期个人学习笔记仍在仓库根目录 `docs/learning/` 等,**可能过时**,不以之为现行口径。
## 文档结构
```text
mvp/
architecture/
architecture/ # 现行架构规范
engineering/ # 工程纪要(问题/决策/E2E)
README.md
current-mvp-architecture.md
agent-orchestration.md
harness-quality-gates.md
session-trace-lifecycle.md
archive/
2026-07-05-legacy/
2026-07-22-legacy/
issues/
README.md
active/
archived/
design-notes/
rag/
diagnosis/
issues/
tables/
README.md
*表-*.md
archive/
demo/
README.md
ten-minute-interview-demo.md
requests/
scripts/
output/
eval/
README.md
schema.md
cases/
fixtures/
reports/
archive/
```
@@ -0,0 +1,391 @@
# RAG 检索可观测性、审计与 Trace(现行)
**更新日期**:2026-09-29
**状态**:当前可运行
**关联**:`lookup_knowledge`、Harness `ToolBoundary`、`tool_invocation`、`DiagnosisTraceService`、离线 eval
> **2026-09-29 RAG 抽离影响**:检索后端切换为 py-rag 服务(见 [RAG知识检索架构.md](./RAG知识检索架构.md))。
> Trace / 审计的三层边界与读写接口**不变**;变化仅在内容语义:
> L0 已下沉(`queryHints` 恒为空结构、`categoryFilter` 恒为 null、attempt 只剩 `UNFILTERED_VECTOR`),
> 质量分统一为 py-rag rerank 绝对分(scoreLabel=RERANK)。
> 文中涉及 FILTERED/RETRY attempt 的示例为历史数据读法,保留供回放旧 Run。
---
## 1. 三层边界
```mermaid
flowchart TB
subgraph A["A. 请求内 Trace"]
LR[LookupResult<br/>retrievalTrace / rerankTrace / relevanceLevel]
end
subgraph B["B. 持久化审计 + Trace API"]
TI[tool_invocation 表]
DT[diagnosis_trace 事件摘要]
API["GET /api/diagnosis/{sessionId}/trace"]
end
subgraph C["C. 质量回归"]
EV[eval/rag-retrieval offline baseline]
end
subgraph agent["Agent 可见(非审计)"]
RT[RagToolResult<br/>evidence + optional relevance_level]
end
LK[LookupKnowledgeTool] --> LR
LR --> PROJ[RagResultProjector]
PROJ --> RT
LR --> BOUND[ToolBoundary audit]
BOUND --> TI
BOUND --> DT
TI --> API
DT --> API
EV -.->|不替代运行时 Trace| LK
```
| 层 | 完善度 | 说明 |
|----|--------|------|
| A 请求内 | 高 | attempt / fallback / quality 齐全 |
| B 持久化 + Trace API | 中高 | RAG 富字段入 `tool_invocation`,经 Trace API 回放 |
| C 离线 eval | 高 | hybrid fixtures 回归 |
**Agent 看到的不是完整 Trace。** 完整检索轨迹在 A/B;Agent 只拿投影后的证据契约。
---
## 2. 端到端:从 lookup 到 Trace API
```mermaid
sequenceDiagram
participant Agent
participant Adapter as RagToolAdapter
participant Bound as ToolBoundary
participant Tool as LookupKnowledgeTool
participant Sink as JpaToolInvocationAuditSink
participant DB as tool_invocation
participant Trace as DiagnosisTraceService
participant API as GET .../trace
Agent->>Adapter: lookup_knowledge(query)
Adapter->>Bound: execute(legacy, projector)
Bound->>Tool: execute(query)
Tool-->>Bound: raw LookupResult JSON
Note over Tool: 内含 retrievalTrace / rerankTrace / evidenceBlocks
Bound->>Bound: project → RagToolResult
Bound->>Sink: AuditEvent + rawResultJson + agentResultJson
Sink->>Sink: RagLookupAuditEnricher
Sink->>DB: 富字段行
Bound-->>Agent: 投影后 agent_result(无完整 trace)
API->>Trace: sessionId + optional runId
Trace->>DB: find tool_invocation by run/session
Trace-->>API: DiagnosisTraceResponse.toolInvocations[]
```
---
## 3. A 层:请求内 Trace(`LookupResult`)
一次成功的 `lookup_knowledge` 内部出口是 **`LookupResult`**(比 Agent 契约更富)。
### 3.1 结构总览
```mermaid
flowchart TB
LR[LookupResult]
LR --> F[found]
LR --> EB[evidenceBlocks[]]
LR --> CP[contextPack]
LR --> RT[retrievalTrace]
LR --> RR[rerankTrace]
LR --> RL[relevanceLevel]
LR --> CH[completenessHint]
LR --> CNT[evidenceCandidateCount / evidenceBlockCount]
RT --> ATT[attempts[]]
RT --> SEL[selectedAttempt]
RT --> FB[fallbackReason]
RT --> HINT[queryHints L0]
```
| 字段 | 含义 |
|------|------|
| `found` | 是否有可用证据块 |
| `evidenceBlocks` | 后处理后的 chunk 级证据(含 evidenceKey、source、content…) |
| `contextPack` | 字符预算打包文本(内部/审计用) |
| `retrievalTrace` | **检索路径 Trace**(见下) |
| `rerankTrace` | 后处理排序/quality 痕迹(现多为保序后的 quality) |
| `relevanceLevel` | PRECISE / REFERENCE / null |
| `completenessHint` | 给模型的天花板提示文案 |
### 3.2 `retrievalTrace`(检索路径)
| 字段 | 含义 |
|------|------|
| `originalQuery` | 原始查询 |
| `rewrittenQuery` | 用于检索的 query(L0 下沉后恒等于 originalQuery) |
| `categoryFilter` | 首次过滤的 category(L0 下沉后恒为 null) |
| `selectedAttempt` | 最终采用的 attempt 名 |
| `fallbackReason` | 如 `filtered_vector_low_quality`;未降级为 null |
| `evidenceStatus` | 内部:`supported` / `no_evidence` 等 |
| `queryHints` | L0 提示(下沉后恒为空 domains/keywords/entities 与 l0_match_count=0) |
| `attempts[]` | 每次检索尝试快照 |
**`selectedAttempt` 取值:**
| 值 | 含义 |
|----|------|
| `UNFILTERED_VECTOR` | **当前唯一会出现**:无 category,直传 py-rag 检索 |
| `FILTERED_VECTOR` | (历史)带 category 的首次检索即采用;L0 下沉后不再产生 |
| `UNFILTERED_VECTOR_RETRY` | (历史)filtered 低质/无证据后去掉 category 重试;分支保留但不可达,仅见于旧 Run 回放 |
**单次 `attempts[]` 元素:**
| 字段 | 含义 |
|------|------|
| `name` | attempt 名 |
| `query` | 该次实际检索句 |
| `categoryFilter` | 该次 filter |
| `candidateCount` | 召回候选数 |
| `usable` | 后处理阈值后是否可用 |
| `topScore` / `topSimilarity` | 引擎分 / 归一化 quality(0~1) |
| `durationMs` | 耗时 |
| `errorMessage` | 失败时 |
### 3.3 一次典型路径(当前:单 attempt 直传)
```mermaid
flowchart TB
Q[query 原始句直传] --> A1[attempt UNFILTERED_VECTOR<br/>PyRagKnowledgeSearchAdapter → py-rag]
A1 --> POST[PostProcess · evidenceBlocks · relevanceLevel]
POST --> LR[LookupResult 完整 Trace]
```
> 历史 filter fallback 路径(L0 → FILTERED_VECTOR → 低质 → UNFILTERED_VECTOR_RETRY)的流程图已随 L0 下沉移除;
> 旧 Run 的 Trace 回放仍可见该结构,字段含义见 §3.2。
### 3.4 与 Agent 投影的关系
```mermaid
flowchart LR
LR[LookupResult 全量 Trace] --> PROJ[RagResultProjector]
PROJ --> AG[RagToolResult]
AG --> F1[evidence_status]
AG --> F2[evidence excerpt]
AG --> F3[relevance_level 可选]
AG --> F4[truncated / returned_count]
LR -.->|不投影| X1[retrievalTrace]
LR -.->|不投影| X2[rerankTrace]
LR -.->|不投影| X3[raw scores / contextPack 全文]
```
人/系统要「为什么这样检索」→ 看 **A 全量** 或 **B 落库摘要**,不要只看 Agent 字段。
---
## 4. B 层:持久化 + Trace API
### 4.1 写入路径
| 组件 | 职责 |
|------|------|
| `ToolBoundary` | 执行后发 `ToolInvocationAuditEvent`(含 raw LookupResult JSON + agent JSON) |
| `RagLookupAuditEnricher` | 从 LookupResult 抽有界 RAG 字段 |
| `JpaToolInvocationAuditSink` | 写入 `tool_invocation` |
| `TraceAuditEvents.toolInvocation` | 另写一条 diagnosis_trace 摘要事件(不含全文 LookupResult) |
### 4.2 `tool_invocation` 列(RAG)
| 列 | lookup_knowledge | 其它工具 |
|----|------------------|----------|
| `tool_name` | `lookup_knowledge` | 各自工具名 |
| `retrieval_layer` | 通常 `L1` | `HARNESS` |
| `relevance_level` | **PRECISE / REFERENCE / …** | **null**(不再写 evidence_status) |
| `l0_match_count` | queryHints | null |
| `l1_match_count` | evidence 块数等 | null |
| `is_truncated` | 投影 truncated | false |
| `retrieval_details` | JSON `rag_lookup_v1` | 通用 status 元数据 |
| `output_preview` | level/attempt 摘要 | status=… |
| `duration_ms` / `success` | 有 | 有 |
### 4.3 `retrieval_details`(rag_lookup_v1)示例(当前形态)
```json
{
"audit_schema": "rag_lookup_v1",
"search_mode": "hybrid",
"selected_attempt": "UNFILTERED_VECTOR",
"fallback_reason": null,
"category_filter": null,
"evidence_keys": ["e2e-gateway-b9c1fa12-md-34223174#chunk-1"],
"sources": ["e2e-gateway-b9c1fa12-md"],
"evidence_candidate_count": 5,
"evidence_block_count": 2,
"l0_hints": { "domains": [], "matched_keywords": [] },
"attempts": [
{
"name": "UNFILTERED_VECTOR",
"candidate_count": 5,
"usable": true,
"top_similarity": 0.91,
"duration_ms": 640
}
],
"truncated": false,
"returned_count": 2,
"evidence_status": "EVIDENCE_FOUND",
"invocation_status": "READY",
"tool_call_id": "call-…"
}
```
**默认不落库:** 原始 query 全文、chunk 正文 excerpt、完整 rerankTrace(体积与隐私)。
历史 Run 中 `selected_attempt=FILTERED_VECTOR` / `UNFILTERED_VECTOR_RETRY` 与非空 `l0_hints` 为 L0 下沉前的旧数据形态。
### 4.4 Trace API:人怎么读 RAG
**接口:**
```http
GET /api/diagnosis/{sessionId}/trace
GET /api/diagnosis/{sessionId}/trace?runId={runId}
```
**实现:** `DiagnosisTraceController` → `DiagnosisTraceService.getTrace`
按 `sessionId`(可选精确 `runId`)拉 run、steps、**toolInvocations**、摘要等。
**响应中与 RAG 相关的核心块:** `DiagnosisTraceResponse.toolInvocations[]`
| API 字段 | 来源列 | 读法 |
|----------|--------|------|
| `toolName` | `tool_name` | 是否为 `lookup_knowledge` |
| `retrievalLayer` | `retrieval_layer` | L1 / HARNESS |
| `relevanceLevel` | `relevance_level` | RAG 粗相关度(非 evidence_status) |
| `l0MatchCount` / `l1MatchCount` | 同名列 | L0/L1 规模提示 |
| `truncated` | `is_truncated` | 证据是否被投影截断 |
| `outputPreview` | `output_preview` | 一行摘要(level/attempt…) |
| `retrievalDetails` | 解析自 `retrieval_details` | **RAG Trace 主阵地** |
| `retrievalDetailsRaw` | 原始 JSON 字符串 | 调试 |
| `durationMs` / `success` / `errorMessage` | 同名列 | 耗时与成败 |
| `inputParams` | 通常仅 tool_call_id、request_bytes | **不含完整 query**(有意) |
```mermaid
flowchart TB
API["GET /api/diagnosis/{sessionId}/trace"] --> SVC[DiagnosisTraceService]
SVC --> ROW[tool_invocation 行]
ROW --> T1[列: relevanceLevel, L0/L1 count, layer…]
ROW --> T2[retrievalDetails Map]
T2 --> D1[search_mode]
T2 --> D2[selected_attempt / fallback_reason]
T2 --> D3[attempts[] / evidence_keys]
T2 --> D4[evidence_status 契约状态]
```
### 4.5 读 Trace 的推荐顺序(排查「这次知识库怎么检的」)
```mermaid
flowchart TB
S1[找到 toolName=lookup_knowledge 的 invocation] --> S2{success?}
S2 -->|否| E[看 errorMessage / evidence_status]
S2 -->|是| S3[看 retrievalDetails.search_mode]
S3 --> S4[看 selected_attempt + fallback_reason]
S4 --> S5[看 attempts[] 每次 candidate_count / top_similarity / usable]
S5 --> S6[看 evidence_keys / sources]
S6 --> S7[看 relevanceLevel 列]
S7 --> S8[需要原文?看 Agent 侧 evidence 或当时 canonical 存储 · 审计默认无 excerpt]
```
| 现象 | 优先看 |
|------|--------|
| 是否 hybrid | `search_mode`(hybrid/semantic 对应 py-rag 融合/纯向量) |
| 滤错域 | (历史)`category_filter` + L0 domains;L0 下沉后恒为 null |
| 相关度档 | 列 `relevanceLevel`(PRECISE/REFERENCE) |
| 返回了哪些块 | `evidence_keys` / `sources`(无正文) |
| Agent 是否被截断 | `truncated` / `returned_count` |
| 为何走了 retry | (历史)`fallback_reason` + 两次 `attempts`;L0 下沉后单 attempt,不再产生 |
### 4.6 diagnosis_trace 事件 vs tool_invocation 行
| 通道 | 内容 | 用途 |
|------|------|------|
| `tool_invocation` 行 | RAG 富字段完整摘要 | **主审计/回放** |
| `diagnosis_trace` 中 `TOOL_INVOCATION` | tool_call_id、status、字节数、`has_raw_result` 等薄摘要 | 时间线事件,**不含**完整 retrieval_details |
查 RAG 细节以 **`toolInvocations[].retrievalDetails`** 为准。
---
## 5. 与 Agent / Eval 的边界
```mermaid
flowchart LR
subgraph human["人 / 运维 / 评测"]
TRACE[Trace API]
EVAL[Offline eval]
end
subgraph model["模型"]
AGENT[RagToolResult only]
end
TI[(tool_invocation)] --> TRACE
FX[fixtures] --> EVAL
PROJ[Projector] --> AGENT
```
| 消费者 | 能看到 |
|--------|--------|
| Agent | evidence + 可选 relevance_level,无 attempt 细节 |
| Trace API | 落库摘要:mode/attempt/fallback/keys/level… |
| Offline eval | 冻结 fixture 全量 LookupResult(含 trace),与 golden 比对 |
---
## 6. 与旧文档差异
| 旧(archive `retrieval-observability`) | 现 |
|----------------------------------------|-----|
| `vector-store.mode` 多后端 | `search_mode` dense\|hybrid(现映射 py-rag semantic\|hybrid) |
| sink 理想化未落地 | `RagLookupAuditEnricher` + 列回填 |
| `relevance_level` 混用 evidence_status | **列仅 RAG 等级**;契约状态在 details |
| 未写清 Trace API 读法 | 本文 §4.4–4.5 |
| L0 hint / FILTERED-RETRY attempt(2026-07-28 形态) | 2026-09-29 L0 下沉 py-rag:单 attempt、queryHints 恒空、质量分 RERANK 直传 |
---
## 7. 代码锚点
| 职责 | 类 / 路径 |
|------|-----------|
| 内建 Trace | `LookupKnowledgeTool`、`RetrievalTrace`、`LookupResult` |
| 投影 | `RagResultProjector`、`RagToolResult` |
| 审计事件 | `ToolInvocationAuditEvent`、`ToolBoundary` |
| 富化 | `RagLookupAuditEnricher` |
| 落库 | `JpaToolInvocationAuditSink`、`ToolInvocation` |
| Trace API | `DiagnosisTraceController`、`DiagnosisTraceService`、`DiagnosisTraceResponse.ToolInvocationTrace` |
| 离线回归 | `eval/rag-retrieval/`、`scripts/eval_rag_retrieval.py` |
---
## 8. 已知限制
- 持久化 **不存** 完整 query/excerpt(有意);要正文需 Agent 侧证据或其它存储
- `dedup_reason` 列可能仍为空
- 非 `lookup_knowledge` 工具仍为薄审计
- **历史** `tool_invocation` 行可能仍把 evidence_status 写进 `relevance_level`(旧 sink)
- `diagnosis_trace` 时间线事件不替代 `retrieval_details`
---
## 9. 相关文档
| 文档 | 内容 |
|------|------|
| `mvp/architecture/RAG知识检索架构.md` | 检索主架构 |
| `../engineering/rag/RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
| `../engineering/rag/RAG-Hybrid质量分与后处理.md` | quality / 排序闸门 |
| `../engineering/rag/RAG离线评测-基线设计.md` | 离线评测(非运行时 Trace) |
| `mvp/architecture/session-trace-lifecycle.md` | 会话/run Trace 总览(若存在) |
+236
View File
@@ -0,0 +1,236 @@
# RAG 知识检索架构
**更新日期**:2026-09-29
**状态**:当前可运行架构
**关联实现**:`lookup_knowledge`、`KnowledgeSearchPort`、`PyRagKnowledgeSearchAdapter`、`PyRagClient`
**关联契约**:py-rag 仓库 `docs/Java接入文档.md`(API v1,冻结面)
**关联运维**:py-rag `/api/v1/collections:rebuild`(全量重建)、py-rag `/api/v1/documents:ingest`(单文档入库)
## 1. 定位
知识检索是 Diagnosis Agent 的显式证据工具,不是隐式 Advisor。
检索算法(dense+BM25 融合、rerank、判级)与文档入库(解析、frontmatter、分块、向量化)
**全部由独立的 py-rag 知识服务承担**;Java 侧只保留 harness 消费面与 HTTP 客户端。
```text
Diagnosis Agent
-> lookup_knowledge(query)
-> Harness ToolBoundary / ACI projection
-> EvidenceGuard 只认当前 Run 的 READY canonical 证据
```
目标:
- 保留 Agent 可见的工具调用与证据边界
- 检索/入库基础设施外置为独立服务,Java 侧不感知引擎细节
- 用 chunk 级证据身份保证同文档多片段可同时进入上下文
- 检索行为可配置、可重建、可审计
## 2. 稳定边界
```mermaid
flowchart LR
subgraph AgentBoundary["Agent boundary"]
Agent["Diagnosis Agent"]
Tool["lookup_knowledge"]
end
subgraph HarnessBoundary["Harness boundary"]
Adapter["RagToolAdapter"]
Projector["RagResultProjector"]
Canonical["Redis canonical invocation"]
end
subgraph RetrievalBoundary["Retrieval boundary"]
Backend["LookupKnowledgeTool"]
Port["KnowledgeSearchPort"]
Remote["PyRagKnowledgeSearchAdapter"]
Service["py-rag 知识服务 (HTTP /api/v1)"]
end
Agent --> Tool
Tool --> Adapter
Adapter --> Backend
Backend --> Port
Port --> Remote
Remote -->|HTTP| Service
Adapter --> Projector
Adapter --> Canonical
```
| 边界 | 职责 | 不负责 |
|---|---|---|
| Agent | 决定何时检索、如何用证据写报告 | 不直接访问 py-rag / MySQL 元数据表 |
| Harness | Tool 校验、投影裁剪、canonical 存证 | 不改写检索排序算法 |
| Retrieval | 请求映射、后处理、打包;py-rag 承担召回/融合/rerank/判级 | 不绕过 ACI 直接给 Agent 原始库响应 |
## 3. 当前主链路
```mermaid
flowchart TD
A["lookup_knowledge(query)"] --> B["原始 query 直传(L0 已下沉 py-rag)"]
B --> C["KnowledgeDocumentRetriever"]
C --> D["KnowledgeSearchPort"]
D --> E["PyRagKnowledgeSearchAdapter"]
E -->|POST /api/v1/search| F["py-rag: dense+BM25 融合 / rerank / 判级"]
F --> G["hits + evidenceKey(docId#chunk-N)"]
G --> H["KnowledgeEvidencePostProcessor"]
H --> I["evidenceKey dedup / maxChunksPerDocument / return-n"]
I --> J["KnowledgeContextPacker"]
J --> K["LookupResultAssembler"]
K --> L["RagResultProjector"]
L --> M["Agent-facing RagToolResult"]
```
对应代码:
| 阶段 | 类 | 职责 |
|---|---|---|
| Tool 编排 | `LookupKnowledgeTool` | 串联 retrieve / post / pack;UNFILTERED 单 attempt 主路径 |
| 检索端口 | `KnowledgeSearchPort` / `PyRagKnowledgeSearchAdapter` | 防腐层;请求映射 + 命中归一化 |
| HTTP 客户端 | `PyRagClient` | API v1 调用、错误信封(`E_*`)、`X-Request-ID`、分端点超时 |
| 后处理 | `KnowledgeEvidencePostProcessor` | qualityScore(RERANK 直传)、chunk 去重、相关度等级 |
| 打包 | `KnowledgeContextPacker` | 有界 context pack |
| 投影 | `RagResultProjector` | 只暴露 Agent 可见 evidence 字段 |
2026-09-29 抽离时删除的 Java 侧组件:`MilvusHybridKnowledgeStore`、`VectorSearchService`、
`VectorKnowledgeSearchAdapter`、`RrfFusion` / `LexicalRanker`(融合评分下沉)、
`KnowledgeIndexService` / `KnowledgeDomainService`(L0 索引)、
`DocumentChunkService` / `FrontmatterParser` / `TextExtractorService`(入库解析)、
`KnowledgeQueryTransformer`(L0 query 理解)。
## 4. py-rag 检索契约映射
请求映射(`PyRagKnowledgeSearchAdapter`):
| Java(KnowledgeSearchRequest) | py-rag(/api/v1/search) | 说明 |
|---|---|---|
| `query` | `query` | 原始检索句直传,服务端自行处理边界与精排 |
| `mode=DENSE` | `mode=semantic` | 纯向量,对照/排障用 |
| `mode=HYBRID` | `mode=hybrid` | dense+BM25 融合,线上主路径(`retrieval.search.mode: hybrid`) |
| `topK` | `retrieve_k` / `return_n` / `max_chunks_per_document` | 三者同置 topK:chunk 去重截断由 Java 后处理器统一负责,避免服务端预截断 |
| `categoryFilter` | `category` | L0 下沉后恒为 null(不过滤) |
| — | `kb_scope` | 不传,由 py-rag 部署配置决定 |
响应映射:
| py-rag | Java(KnowledgeSearchHit) | 说明 |
|---|---|---|
| `evidence_key` | `evidenceKey` / `docId` / `chunkIndex` | `docId#chunk-N`,与 EvidenceGuard 验真约定一致 |
| `excerpt` | `content` | 进入 context pack 的正文 |
| `quality_score` | `score` / `rawScore` | rerank 绝对相关分 [0,1],越大越好 |
| — | `scoreLabel=RERANK` | `RetrievalScoreNormalizer` 对 RERANK 分支 quality 原样 clamp,不走 L2/rank 归一化 |
| `relevance_level` | (参考值) | Java 后处理按同阈值(0.75/0.5)独立判级,语义一致 |
| `evidence_status=no_evidence` | 空列表 | 正常业务响应(服务端保证 hits=[]),Agent 侧按"无知识可用"处理 |
超时与重试矩阵见 py-rag 仓库 `docs/Java接入文档.md` 第 6 节;Java 侧由 `pyrag.*` 配置承载。
## 5. 入库与重建
### 5.1 日常写入
```text
业务上传(DocumentController /api/documents/upload)
-> MySQL api_document(业务元数据:faultSource 等)+ 本地原件保存
-> PyRagClient.ingest(multipart 透传原件 + category)
-> py-rag:解析 / frontmatter 校验 / 分块 / 向量化 / 索引
简单上传(FileUploadController /api/upload)
-> 本地保存 + PyRagClient.ingest(category 可选参数,缺省 default;入库失败不影响上传成功语义)
```
MySQL `api_document.docId` 取 py-rag 返回的 `doc_id`(路径 slug + 内容 SHA-256 前 8 位),
与检索 `evidence_key` 的 docId 段对齐;`chunk_count` 取 ingest 响应。
### 5.2 全量重建
```text
POST /api/v1/collections:rebuild?confirm=REBUILD (py-rag 服务端)
GET /api/v1/tasks/{task_id} (任务状态)
```
- rebuild 为 py-rag 异步任务;执行期间 ingest 返回 409(`E_REBUILD_IN_PROGRESS`),search 不受影响
- 原料为 py-rag 服务端 `data/knowledge_base/` 下历次 ingest 落盘的 md
- 旧 Java 侧 `POST /api/knowledge/rebuild-hybrid` 与 `scripts/rebuild_hybrid_knowledge.py` 已删除
### 5.3 单文档删除
py-rag API v1 没有单文档删除端点。`DELETE /api/documents/{docId}` 只删 MySQL 元数据与本地原件;
py-rag 侧已入库内容需全量重建后才会消失(见 `DocumentManagementService.deleteDocument` 注释)。
## 6. 部署与配置
```yaml
pyrag:
base-url: ${PYRAG_BASE_URL:http://localhost:8000}
connect-timeout-ms: 3000
search-read-timeout-ms: 5000 # 正常 300–800ms(含 rerank 外呼)
ingest-read-timeout-ms: 30000
default-read-timeout-ms: 10000
```
- Milvus / etcd / MinIO 随 py-rag 部署,不再由本仓库 `docker-compose.yml` 管理(compose 仅剩 MySQL/Redis)
- `vector-database.yml` 已删除;Makefile 的 up/down/status 只管 MySQL/Redis
- `retrieval.search.mode: hybrid` 语义保留:映射 py-rag 的 `hybrid` / `semantic`
## 7. 证据身份与去重(不变)
```text
evidenceKey = docId#chunk-{chunkIndex}
fallback: vector:{id}
fallback: rank:{n}
```
- 去重按 `evidenceKey`,同文档不同 chunk 可同时保留
- `rag.max-chunks-per-document` / `rag.return-n` 在 Java 后处理器生效
- Agent 投影中的 `document_id` 使用 chunk 级 evidenceKey,EvidenceGuard 据此验真
## 8. Agent 可见契约(不变)
Agent 只看到有界 `RagToolResult`:
- `evidence_status` / `tool_call_id` / `query`
- `evidence[]`:`document_id` / `source` / `title` / `breadcrumb` / `excerpt`
- `relevance_level`(PRECISE / REFERENCE;`RagRelevanceLevel.HIGHLY_RELEVANT` 为保留档)
- `truncated` / `returned_count`
不暴露:raw score、retrievalTrace / rerankTrace、contextPack 全文、py-rag 地址与凭据。
完整内部结果仍在 `LookupResult` 中,供审计与调试使用。Trace / 审计读法见
[RAG检索可观测性与审计.md](./RAG检索可观测性与审计.md)。
## 9. L0 下沉
L0 query 理解(domain/keyword hint → 可选 categoryFilter)随本次抽离**整体下沉 py-rag**:
- `KnowledgeQueryTransformer` / `KnowledgeIndexService.analyzeQuery` 已删除
- `lookup_knowledge` 直传原始 query;`categoryFilter` 恒为 null,走 UNFILTERED 单 attempt
- `LookupKnowledgeTool` 中 filtered→unfiltered 降级分支保留但不可达(作为未来 Java 侧过滤策略的兜底骨架)
- `RetrievalTrace.queryHints` 恒为空结构;`attempt` 命名只剩 `UNFILTERED_VECTOR`
## 10. 与旧文档的差异
| 旧描述(2026-07-28 版) | 当前实现(2026-09-29 抽离后) |
|---|---|
| 进程内 MilvusClientV2,dense+BM25+RRF | py-rag 服务端承担;Java 经 `KnowledgeSearchPort` → HTTP |
| `retrieval.search.mode` 切 Milvus 查询算法 | 同名配置映射 py-rag `hybrid` / `semantic` |
| scoreLabel 仅 dense \| hybrid,L2/rank 归一化 | 新增 `RERANK`:py-rag rerank 绝对分直传 |
| L0 hint + categoryFilter + filtered/unfiltered retry | L0 下沉;原始 query 直传,单 attempt |
| Java 侧 frontmatter 解析 / 分块 / embedding 入库 | py-rag `documents:ingest`;Java 只做 MySQL 元数据 + 原件保存 |
| `POST /api/knowledge/rebuild-hybrid` 重建 | py-rag `POST /api/v1/collections:rebuild` |
| `GET /milvus/health` 健康检查 | py-rag `GET /api/v1/health`(含 milvus/embedding/rerank 探针) |
| 删除文档同步删向量索引 | 仅删 MySQL+本地文件;py-rag 侧靠全量重建生效 |
历史材料见:
- `mvp/architecture/archive/2026-07-22-legacy/rag-architecture.md`(Milvus 前身)
- `mvp/engineering/rag/Milvus-Hybrid接入清单.md`(进程内 Milvus hybrid 接入纪要,已过时)
- 本仓库 git 历史:`refactor/extract-rag-module` 分支,71 文件 / 约 -8000 行
## 11. 当前已知边界
- py-rag 判级阈值(0.75/0.5/0.3)未校准,`quality_score` 仅供排序与展示参考(契约已知边界)
- frontmatter 的 keywords/summary/covers/when_to_retrieve 当前仅上传方提供;Java 侧 LLM 补全(原 `DocumentFieldEnricher`)已随抽离移除,待 py-rag 开放
- `knowledge_base/` 历史原料需在 py-rag 侧完成一次性 ingest 迁移后方可检索
- 无单文档删除;下线文档靠 py-rag 全量重建
- eval/rag-retrieval 离线基线基于旧 L0/scoreLabel 语义构建,抽离后需重新校准(见 `mvp/engineering/rag/RAG离线评测-基线设计.md` 顶部说明)
- Trace/审计细节与限制见 [RAG检索可观测性与审计.md](./RAG检索可观测性与审计.md)
+15 -2
View File
@@ -1,6 +1,6 @@
# MVP 架构文档
**更新日期**:2026-07-23
**更新日期**:2026-09-29
**状态**:当前单 Diagnosis Agent + Harness 架构
当前文档入口:
@@ -11,7 +11,20 @@
| [agent-orchestration.md](agent-orchestration.md) | 单 Diagnosis ReAct Agent 的职责、执行方式和 reasoning 采集边界 |
| [harness-quality-gates.md](harness-quality-gates.md) | Run、Tool、Evidence、Semantic、Release 与 Trace Recorder 门禁 |
| [session-trace-lifecycle.md](session-trace-lifecycle.md) | sessionId/runId、SSE、统一 Timeline 和 reasoning audit 生命周期 |
| [diagnosis-information-gain-stop-architecture.md](diagnosis-information-gain-stop-architecture.md) | 已实施的信息增益评价、Harness 饱和检测、Draft 合同失败降级与证据不足停止设计 |
| [RAG知识检索架构.md](RAG知识检索架构.md) | 当前 `lookup_knowledge` 检索:py-rag 知识服务接入、契约映射、chunk 证据身份、入库与重建运维 |
| [RAG检索可观测性与审计.md](RAG检索可观测性与审计.md) | RAG Trace / 审计:请求内 retrievalTrace、tool_invocation 富字段、Trace API 读法 |
**工程纪要**(问题 / 决策 / E2E,非架构规范正文)见 [../engineering/README.md](../engineering/README.md)。
2026-07-22 前的多角色编排、双入口和旧证据链文档已移动到 `archive/2026-07-22-legacy/`,仅用于历史决策追溯,不代表当前运行时。
当前普通 Trace 与 Provider reasoning 审计使用独立存储和独立接口。Reasoning 访问控制、保留期限、加密要求以及真实 Provider/V015 验证仍由 ISS-015 跟踪,不能把“数据已分表”理解为“治理已经完成”。
RAG 架构经历两次更替,均以 [RAG知识检索架构.md](RAG知识检索架构.md) 为准:
1. 2026-07-28:进程内 MilvusClientV2 hybrid(取代更早的 Spring AI VectorStore 主路径 + Milvus SDK fallback);
2. **2026-09-29(当前)**:RAG 模块抽离为独立 py-rag 知识服务,Java 经 `KnowledgeSearchPort` → `PyRagKnowledgeSearchAdapter` → HTTP `/api/v1` 调用;进程内 Milvus/embedding/L0/分块全部移除。
检索可观测与 Trace 以 [RAG检索可观测性与审计.md](RAG检索可观测性与审计.md) 为准(勿再依赖 archive 内旧 retrieval-observability)。
当前普通 Trace 与 LLM 步骤审计(`agent_reasoning_audit`:`reasoning_content` + `assistant_text`)使用独立存储和独立接口。
DeepSeek thinking 捕获路径与 V015–V017 字段已 live 验证(2026-07-28)。Reasoning 访问控制、保留期限、加密要求仍由 ISS-015 跟踪,不能把“数据已分表 + 能抓到 thinking”理解为“治理已经完成”。
+30 -8
View File
@@ -56,14 +56,36 @@ sequenceDiagram
PreviousTurn 只来自同一 Session 最近一个 `DIAGNOSIS + SUCCESS + published_result`。Fallback、失败、取消、raw evidence 和完整历史都不能进入下一轮;字段与字节上限由 Harness 配置控制。
## 5. Provider Reasoning 审计
## 5. Provider Reasoning 与 Assistant 正文审计
`HarnessAgentAuditHook` 在每次模型步骤结束后检查 `AssistantMessage` metadata。当前识别 `reasoning_content`、`reasoningContent`、`reasoning` 和 `thinking`,但只接受 Provider 实际返回的非空文本:
`HarnessAgentAuditHook` 在每次模型步骤结束后写入独立表 `agent_reasoning_audit`,**同时**尝试捕获:
- 有内容时写入 `agent_reasoning_audit`,单条最多保留 32000 个字符,并记录 UTF-8 `content_bytes`。
- 无内容时写入 `reasoning_available=false`、`reasoning_content=NULL`、`content_bytes=0`,不得根据最终回答反推或生成 reasoning。
- `agent_step.thought` 始终为空;步骤表只记录 message count、roles、是否有文本、Tool names、reasoning availability 和字节数等 metadata。
- 普通 `diagnosis_trace_event` 的 `AGENT_MODEL_STEP` 只记录 reasoning availability/bytes,不保存 reasoning 原文。
- Reasoning 只用于受限审计,不进入 Agent 后续上下文,不参与 EvidenceGuard、SemanticGuard 或 Release Policy 的事实判断。
| 列 | 含义 |
|---|---|
| `reasoning_content` | Provider thinking / CoT |
| `assistant_text` | 本步 assistant 可见正文,和/或 tool-call **计划**(不含 tool 结果) |
| `content_source` | `PROVIDER_REASONING+ASSISTANT_TEXT` 等组合标记 |
| `content_bytes` | 两列截断后 UTF-8 字节合计 |
当前查询隔离已经实现,完整访问治理和真实 Provider 行为验证仍属于 ISS-015。
### 捕获路径(DeepSeek 生产)
当前 Chat 为 Spring AI 原生 `DeepSeekChatModel`(`deepseek-v4-flash`)。API 返回的 `message.reasoning_content` 被映射到 **`DeepSeekAssistantMessage.getReasoningContent()`**,而不是普通 `AssistantMessage.metadata`。Hook 优先读该专用字段,再反射 `getReasoningContent()`,最后才回退 metadata 键(`reasoning_content` / `thinking` 等)。
只接受 Provider 实际返回的非空文本;不得根据最终回答反推或生成伪 reasoning。
### 边界
- 单字段最多保留 32000 字符;tool **结果**只在 `tool_invocation`。
- `agent_step.model_*` 仍为有界 metadata(含 `reasoning_available` / bytes / `content_source`)。
- `agent_step.thought` 为兼容镜像:优先 reasoning,否则 assistant 正文;完整双字段以 `agent_reasoning_audit` 为准。
- 普通 Timeline 的 `AGENT_MODEL_STEP` 不保存 reasoning/assistant 原文。
- Reasoning / assistant 审计原文只用于受限审计 API,不进入 Agent 后续上下文,不参与 EvidenceGuard、SemanticGuard 或 Release Policy 的事实判断。
### 运行级结论读出
Run 结束时 `JpaChatRunStore` 从安全发布 JSON 提取 `diagnosis_run.conclusion`(与 `query` 并列),便于 Trace/DB 直接读结论;它不是 Provider thinking。
### 验证与治理
- **已 live 验证(2026-07-28)**:DeepSeek thinking 模式下 `reasoning_available=true`,且 reasoning 与 assistant 可同时非空。
- 查询隔离已实现;访问控制、保留期限、加密等完整治理仍属 ISS-015。
+44 -10
View File
@@ -1,12 +1,14 @@
# 当前 MVP 架构
**更新日期**:2026-07-23
**更新日期**:2026-09-29
**状态**:当前可运行架构
## 1. 系统定位
SuperBizAgent 是面向故障诊断的可追踪 Agent 应用。当前系统只保留一个拥有 Tool loop 的 `Diagnosis Agent`;Harness 负责确定性的预算、取消、工具边界、证据验真、语义审查和安全发布。
知识检索当前为显式 `lookup_knowledge` 工具 + 独立 py-rag 知识服务(HTTP `/api/v1`);检索算法(dense+BM25 融合、rerank、判级)与文档入库全部在 py-rag 侧,Java 只保留 harness 消费面与 HTTP 客户端。详细链路见 [RAG知识检索架构.md](RAG知识检索架构.md)。
## 2. 分层
```mermaid
@@ -63,6 +65,29 @@ Agent 只看到三个固定 Tool:
每次调用由框架提供 `tool_call_id`,Harness 校验 exact run、只读、Schema、预算和容量。Redis 保存 TTL 内完整 canonical invocation;MySQL `tool_invocation` 只保存长期有界 metadata,不保存完整参数、SQL/日志正文、raw response 或 Agent projection。
### 4.1 `lookup_knowledge` 检索边界
```text
Agent
-> RagToolAdapter / ToolBoundary
-> LookupKnowledgeTool
-> 原始 query 直传(L0 已下沉 py-rag)
-> KnowledgeSearchPort
-> PyRagKnowledgeSearchAdapter # HTTP 客户端(PyRagClient)
-> py-rag 知识服务 /api/v1/search # 融合 / rerank / 判级
-> KnowledgeEvidencePostProcessor # chunk 去重 / return-n / 判级(Java 侧)
-> RagResultProjector # 有界 Agent 投影
```
要点:
- 默认 `retrieval.search.mode=hybrid`:映射 py-rag `hybrid`(dense+BM25 融合);`dense` 映射 `semantic` 作对照。
- 证据按 chunk 级 `evidenceKey` 去重;Agent 侧 `document_id` 为 chunk 级身份。
- py-rag `evidence_status=no_evidence` 按正常"无知识可用"处理,不是错误。
- 知识库全量重建:py-rag `POST /api/v1/collections:rebuild?confirm=REBUILD`(异步任务)。
完整契约映射、入库、重建与历史差异见 [RAG知识检索架构.md](RAG知识检索架构.md)。
## 5. Trace 与持久化
```text
@@ -75,26 +100,35 @@ chat_session(sessionId)
```
- `chat_session` 是 JPA Run 目录与多轮 metadata,不保存完整对话历史。
- `diagnosis_run` 是 Run 状态、intent、release outcome、安全发布结果和预算汇总真理源。
- `agent_step` 只保存模型步骤 metadata,不保存 Prompt、消息正文、模型正文或 Thought。
- `diagnosis_run` 是 Run 状态、intent、release outcome、安全发布结果、预算汇总真理源;另含与 `query` 并列的提取字段 `conclusion`(业务结论读出,非 thinking)。
- `agent_step` 保存模型步骤有界 metadata;`thought` 可为 reasoning/assistant 的兼容镜像,完整双字段不在此表。
- `tool_invocation` 只保存 Tool durable audit metadata;完整调用由 Redis canonical store 短期保存。
- `diagnosis_trace_event` 是追加式统一 Timeline,记录 Run、Routing、Agent、Tool、Evidence、Semantic 和 Release 生命周期事件;`details` 只能保存有界安全 metadata。
- `agent_reasoning_audit` 与普通 Trace 分表,只保存 Provider 实际返回的 reasoning 或明确的 unavailable 记录;reasoning 不属于事实证据。
- `agent_reasoning_audit` 与普通 Trace 分表,按模型步骤保存 Provider `reasoning_content` 与 `assistant_text`(及 `content_source`),或明确的 unavailable;二者均不属于事实证据。
## 6. 公开 API
当前诊断执行入口只有 `POST /api/chat`。诊断审计读取分为:
- `GET /api/diagnosis/{sessionId}/trace?runId={runId}`:普通 Trace,返回 Run、步骤、Tool metadata 和统一 Timeline,不返回 reasoning 原文。
- `GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}`:独立 reasoning 审计读取,`runId` 必填并校验其属于 path `sessionId`。
- `GET /api/diagnosis/{sessionId}/trace?runId={runId}`:普通 Trace,返回 Run(含 `query`/`conclusion`)、步骤、Tool metadata 和统一 Timeline,**不**返回 reasoning/assistant 原文。
- `GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}`:独立 LLM 步骤审计读取(`reasoningContent` + `assistantText`),`runId` 必填并校验其属于 path `sessionId`。
Reasoning endpoint 是敏感审计面,不属于普通业务 API。当前已完成数据和查询隔离;认证授权、保留期限、加密要求及真实 Provider 验证仍由 ISS-015 收敛。Feedback、文档与检索 API 保持独立;已删除的旧诊断和 Redis conversation Session endpoint 不提供兼容分支。
Reasoning endpoint 是敏感审计面,不属于普通业务 API。数据和查询隔离、DeepSeek thinking 捕获路径已 live 验证;认证授权、保留期限、加密要求仍由 ISS-015 收敛。Feedback、文档与检索 API 保持独立;已删除的旧诊断和 Redis conversation Session endpoint 不提供兼容分支。
与知识检索相关的独立 API:
- `POST /api/documents/upload`:文档上传(MySQL 业务元数据 + 本地原件 + py-rag ingest)
- `POST /api/upload`:简单上传(本地保存 + py-rag ingest,可选 `category`)
- `GET /api/documents/{docId}` / `GET /api/documents/status/{status}` / `GET /api/documents/faultSource/{faultSource}`:文档元数据查询
- `DELETE /api/documents/{docId}`:删除 MySQL 元数据与本地原件(py-rag 侧索引需全量重建生效)
知识库检索/入库/重建的服务端健康与统计由 py-rag 提供:`GET /api/v1/health`、`GET /api/v1/stats`(`{pyrag.base-url}`)。
## 7. 安全边界
- 普通 SSE、Trace、Evidence Snapshot、业务结果和应用日志不输出或保存 reasoning 原文。
- 仅当 Provider 在模型 metadata 中实际返回 reasoning 时,审计 Hook 才将其截断后写入独立表;Provider 未返回时不得伪造。
- Reasoning 不能作为事实证据,也不能绕过 EvidenceGuard 或 SemanticGuard。
- 普通 SSE、普通 Trace 的 steps/timeline、Evidence Snapshot、业务结果和应用日志不输出或保存 reasoning / assistant 审计原文。
- 审计 Hook 仅当 Provider **实际返回** thinking(DeepSeek:`DeepSeekAssistantMessage.reasoningContent`;其它:metadata 键)时写入 `reasoning_content`;未返回时不得伪造。`assistant_text` 来自本步 assistant 可见输出与 tool-call 计划,不含 tool 结果。
- Reasoning / assistant 审计原文不能作为事实证据,也不能绕过 EvidenceGuard 或 SemanticGuard。
- 不向 Agent 暴露 Redis、canonical key、完整 Tool 请求/响应或数据库凭据。
- EvidenceGuard 只接受当前 Run 的 READY canonical invocation。
- SemanticGuard 无 Tool、无记忆、无回调主 Agent 能力。
@@ -0,0 +1,608 @@
# Diagnosis Agent 信息增益与停止控制架构
**更新日期**:2026-07-27
**状态**:已实施,待归档
**关联 Issue**:[ISS-016](../issues/active/ISS-016-diagnosis-information-gain-stop-contract.md)
## 1. 背景
当前 Diagnosis ReAct Agent 在知识库没有直接答案、日志持续为空或查询条件不足时,可能继续改写查询并反复调用 Tool,直到资源预算耗尽。此时 Run 被发布为技术失败,用户只能看到通用错误,而不是“已完成有限排查,但证据不足”。
本设计解决两个不同问题:
1. 查询何时已经不再产生有效信息,必须停止继续调用 Tool。
2. 停止后如何避免模型在证据不足时生成无依据结论。
## 2. 核心原则
```text
Tool:返回客观数据
Harness:判断查询过程是否还允许继续
Model:判断数据是否推进了当前诊断,并通过实际 Tool Call 或 Draft 表达行为
SemanticGuard:判断最终结论是否有证据支撑
Release:统一把 Draft 或 Harness 停止投影为安全的 SUCCESS / FALLBACK
```
停止权属于 Harness。模型可以建议继续,但不能绕过 Harness 的饱和状态和硬资源边界。
“没有找到根因”是合法业务结果,不等于执行失败。模型被允许输出 `conclusion=null` 的 `DiagnosisDraft`;最终仍只使用 `SUCCESS / FALLBACK / FAILED / CANCELLED`,不增加第二套诊断终态。
## 3. 总体架构
```mermaid
flowchart TD
U["用户提出诊断问题"] --> M["Diagnosis Agent"]
P["ReAct Prompt<br/>允许合法放弃"] -.-> M
C["会话上下文<br/>有界历史和已检查范围"] -.-> M
M -->|"Tool Call Envelope"| B["Harness 调用门禁"]
B --> A["应用 previous_observation<br/>更新连续 NO_GAIN"]
A --> GATE{"是否允许继续"}
GATE -->|"INFORMATION_SATURATED"| STOP["拒绝调用<br/>注入 STOP_REQUIRED"]
GATE -->|"BUDGET_LIMIT_REACHED"| BSTOP["停止调用<br/>记录资源保护原因"]
GATE -->|"允许调用"| T["剥离控制字段<br/>执行业务 Tool"]
T -->|"执行失败"| ERR["Tool 状态:FAILED"]
ERR --> EP["技术故障策略<br/>有限重试或 FAILED 结束"]
T -->|"执行成功"| CR["Canonical Tool Result<br/>完整内部结果"]
CR --> CV["Harness Control View"]
CR --> AP["Agent Observation Projector"]
CR --> STORE["Canonical Store"]
AP --> MO["Model Observation<br/>白名单有界结果"]
MO --> SM["模型阅读返回内容"]
CV --> O{"Harness 客观判断"}
O -->|"evidence_status=NO_EVIDENCE"| NG1["Harness 标记 NO_GAIN"]
O -->|"重复 Tool + 相同 scope"| NG1
O -->|"其他成功非空结果<br/>包括 REFERENCE"| SM
SM --> SD{"语义信息增益"}
SD -->|"推进或排除诊断假设"| G["GAINED"]
SD -->|"没有可验证的新事实"| NG2["NO_GAIN"]
G --> M
NG1 --> M
NG2 --> M
M -->|"DiagnosisDraft"| SNAP["结束时投影 ProgressSnapshot"]
M -->|"空或非法 Draft"| DRAFTERR["丢弃非法内容<br/>投影当前进展"]
DRAFTERR -->|"有已验真 observed facts"| SNAP
DRAFTERR -->|"无安全进展"| EP
STOP -->|"给模型一次合法完成机会"| M
STOP -->|"仍无合法 Draft"| SNAP
BSTOP --> SNAP
STORE --> SNAP
SNAP --> RELEASE["DiagnosisReleaseUseCase"]
RELEASE -->|"conclusion 非空"| GUARD["EvidenceGuard / Repair / SemanticGuard"]
RELEASE -->|"conclusion 为空或无 Draft"| SAFE["确定性 SafeFallback<br/>不修复结论"]
GUARD -->|"证据支持"| SUCCESS["SUCCESS"]
GUARD -->|"无证据推断"| SAFE
SAFE --> FALLBACK["FALLBACK"]
EP --> FAILED["FAILED"]
SUCCESS --> APP["ChatApplicationUseCase<br/>持久化"]
FALLBACK --> APP
FAILED --> APP
APP --> SSE["ChatSseSession<br/>发送 content/failure + done"]
```
## 4. 状态模型
状态按职责分开,不使用一个枚举表达整个生命周期。
| 维度 | 状态 | 负责人 | 含义 |
|---|---|---|---|
| Tool 执行 | `SUCCEEDED / FAILED` | ToolBoundary | Tool 是否完成技术执行 |
| 信息增益 | `GAINED / NO_GAIN` | Harness 或模型 | 本次结果是否推进当前诊断 |
| 收集生命周期 | `COLLECTING / SATURATED` | Harness | 是否允许继续调用 Tool |
| 内部停止原因 | `INFORMATION_SATURATED / BUDGET_LIMIT_REACHED` | Harness | 为什么停止继续调用 Tool |
| Fallback 原因 | `SafeFallback.type` | DiagnosisReleaseUseCase | 为什么没有发布诊断结论 |
| 最终发布 | `SUCCESS / FALLBACK / FAILED / CANCELLED` | Release | 对外发布结果 |
错误码、停止原因、Fallback 原因和 RAG 相关度是附带字段,不提升为全局生命周期状态。`CONFIRMED / INSUFFICIENT_EVIDENCE / NEED_MORE_INFO` 不作为第二套诊断状态;其中后两种语义由 `SafeFallback.type` 表达。
当前实现中的兼容映射是:内部 `PROJECTING` 不对外暴露,`READY` 对应目标语义 `SUCCEEDED`,`ERROR` 对应目标语义 `FAILED`。
不定义独立的模型行动状态。模型发起 Tool Call 表示继续收集,输出正常 `DiagnosisDraft` 表示完成诊断,输出证据不足 Draft 表示主动停止;Harness 直接从实际输出推导行为。
## 5. Tool 结果与上下文边界
Tool 不判断业务结论,只提供事实和可机械计算的元信息。
```text
tool_call_id 本次调用的唯一标识
execution_status SUCCEEDED | FAILED
returned_count 本次实际返回的记录数量
scope 本次查询实际覆盖的结构化范围
metadata Tool 特有的有限元信息
result 提供给模型的有界结果
```
字段说明:
- `returned_count` 只表达“返回了多少条”,不表示这些内容有诊断价值。
- `scope` 用于识别相同 Tool、相同查询范围的重复调用,例如企业、时间窗、服务和过滤条件。
- `metadata` 保存 Tool 特有信息,例如 RAG 的 `relevance_level`、是否截断和分页信息。
- 第一版不引入 `new_count`。它要求为不同 Tool 建立稳定的结果指纹,而且“新数据”也不等于“有效数据”。
### 5.1 客观信号来源
这些信号不是模型推导的:
| 信号 | 来源 | 含义 |
|---|---|---|
| `PROJECTING / READY / ERROR` | ToolBoundary | 当前实现的调用生命周期;目标外部语义映射为 `SUCCEEDED / FAILED` |
| `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | Tool Result Projector / ToolBoundary | 是否存在候选结果或发生技术错误 |
| `PRECISE / HIGHLY_RELEVANT / REFERENCE / null` | RAG 后处理规则 | RAG 候选内容的相关度 |
| 重复 `tool + scope` | Harness | 本次查询范围是否与历史调用等价 |
`NO_EVIDENCE` 由各 Projector 根据结果集合是否为空确定。`REFERENCE` 由 RAG 根据归一化相似度及查询提示命中情况计算。两者都不是 LLM 生成的状态,但职责不同:`NO_EVIDENCE` 可由 Harness 机械判定为 `NO_GAIN`;`REFERENCE` 只表示候选内容相关度一般,仍由模型判断是否推进当前诊断。
改造前 `RagResultProjector` 只根据 `evidence` 是否为空生成 `EVIDENCE_FOUND / NO_EVIDENCE`,没有保留上游 `relevanceLevel`,因此会出现:
```text
relevanceLevel = REFERENCE
evidenceBlocks 非空
-> RagResultProjector
-> evidence_status = EVIDENCE_FOUND
```
这不是两个判断冲突,而是 Projector 丢失了相关度维度。当前实现已兼容读取上游 `relevanceLevel` 或 `relevance_level`,并统一保留为 `relevance_level`。
### 5.2 Tool 双视图架构
内部控制信息和模型观察不能继续共用一个无差别 JSON。标准化结果产生两个明确视图:
```mermaid
flowchart TD
A["Tool 原始返回"] --> B["结果标准化"]
B --> C["Canonical Tool Result<br/>完整内部结果"]
C --> D["Harness Control View"]
C --> E["Agent Observation Projector"]
D --> F["Harness<br/>重复检测、低收益计数、预算、饱和状态"]
D --> G["Canonical Store"]
E --> H["Model Observation<br/>最小必要字段"]
H --> I["ToolCallResponse.content"]
I --> J["模型上下文"]
```
改造前会把完整 `agent_result` 作为 `ToolCallResponse.content` 交给模型。当前实现已改为白名单投影,Harness 控制字段默认不进入模型上下文。
字段边界:
| 字段 | Harness | 模型 | 说明 |
|---|---:|---:|---|
| `tool_call_id` | 是 | 是 | 引用与归属校验 |
| 实际查询 `scope` | 是 | 是 | 重复检测与有界负向观察 |
| 有界 `evidence` | 是 | 是 | 模型语义判断所需内容 |
| `evidence_status` | 是 | 是 | 候选结果是否为空或失败 |
| `relevance_level` | 是 | 是 | 只暴露粗粒度标签,不暴露原始分数 |
| `truncated` | 是 | 是 | 提醒模型结果并不完整 |
| `returned_count` | 是 | 否 | Harness 客观统计 |
| 原始相似度和检索轨迹 | 是 | 否 | 内部质量与审计信息 |
| 规范化 scope、重复指纹 | 是 | 否 | Harness 控制信息 |
| 低收益计数、阈值、预算 | 是 | 否 | Run 内部状态 |
| 原始 Tool Response | 是 | 否 | 不进入模型上下文 |
只有当 Harness 必须改变模型行为时,才注入有界控制指令,例如:
```json
{
"stop_required": true,
"reason": "INFORMATION_SATURATED",
"checked_scopes": ["本 Run 已检查范围的有界摘要"]
}
```
计数器、阈值和剩余预算不随控制指令进入模型上下文。
### 5.3 Tool 调用与投影流程
```mermaid
sequenceDiagram
participant Model as Diagnosis Agent
participant Interceptor as Harness Tool Interceptor
participant Gate as Harness Gate
participant Adapter as Tool Adapter
participant Backend as Tool Backend
participant Projector as Result Normalizer / Projector
participant Progress as Progress Tracker
participant Store as Canonical Store
Model->>Interceptor: Tool Call Envelope
Interceptor->>Progress: 校验并应用 previous_observation(如有)
Progress-->>Interceptor: 更新后的收集状态
Interceptor->>Gate: 校验 Run、授权、预算和饱和状态
alt 已经 SATURATED
Gate-->>Interceptor: STOP_REQUIRED
Interceptor-->>Model: 有界停止指令
else 达到硬预算
Gate-->>Interceptor: BUDGET_LIMIT_REACHED
else 允许调用
Gate-->>Interceptor: ALLOW
Interceptor->>Interceptor: 剥离 previous_observation
Interceptor->>Adapter: input 中的 typed request
Adapter->>Backend: 执行只读查询
Backend-->>Adapter: raw response
Adapter->>Projector: 标准化原始结果
Projector-->>Progress: Harness Control View
Projector-->>Store: Canonical Tool Result
Projector-->>Interceptor: Model Observation
Interceptor-->>Model: 有界 Tool Observation
end
```
重复查询由 Harness 根据 `tool_name + normalized_scope` 判断,不属于 Tool 返回状态。规范化只处理确定性的业务参数,例如时间格式、无序数组和非业务字段;第一版不判断自然语言改写是否语义等价。
### 5.4 结束时进展投影
每次 Tool 调用完成后只追加 Canonical Tool Result,不在每轮维护另一份用户可见摘要。Tool Loop 因 Agent 输出 Draft、信息饱和或预算限制结束时,一次性生成内部 `ProgressSnapshot`:
```mermaid
flowchart LR
T["每次 Tool 完成"] --> C["Canonical Tool Result"]
C --> S["Canonical Store"]
S -->|"Tool Loop 结束时只读投影"| P["ProgressSnapshot"]
P --> R["DiagnosisReleaseUseCase"]
R --> O["SafeFallback.observed_facts"]
```
`ProgressSnapshot` 不是新的真理源,也不进入模型上下文。它只包含来源、实际 scope、客观结果摘要、截断标记和 Harness `stop_reason`;原始 Tool Response、Prompt、内部 thought、计数器和剩余预算不进入发布内容。
现有 `SafeFallback` 和前端已经支持 `observed_facts / verified_sources / limitations / next_steps`,因此不新增前端协议。`FallbackType` 增加 `INSUFFICIENT_EVIDENCE / MISSING_REQUIRED_CONTEXT`,分别表达有限排查后证据不足和缺少有效查询条件。
## 6. 信息增益契约
信息增益只保留两个值:
```text
information_gain = GAINED | NO_GAIN
```
### 6.1 GAINED
满足以下任一条件:
- 返回了与当前问题直接相关、可验证的新事实。
- 新事实确认了一个当前诊断假设。
- 新事实排除了一个当前诊断假设。
- 新事实实质性缩小了故障范围。
“排除假设”也是信息增益。信息增益不要求得到最终根因。
### 6.2 NO_GAIN
满足以下任一条件:
- 内容为空、重复或只有通用参考资料。
- 数据虽然非空,但与当前问题没有直接关系。
- 没有新增可验证事实,也没有改变任何诊断假设。
- 只是建议继续查询另一个位置,没有提供新的诊断事实。
- 模型无法判断结果是否推进诊断。
不增加 `UNKNOWN`。无法判断时归入 `NO_GAIN`,避免它成为无限继续查询的出口。
### 6.3 谁负责赋值
```text
FAILED
-> 不产生 information_gain,进入技术故障流程
SUCCEEDED + evidence_status=NO_EVIDENCE
或重复的 tool_name + normalized_scope
-> Harness 直接赋 NO_GAIN
其他 SUCCEEDED 非空结果,包括 relevance_level=REFERENCE
-> 模型必须赋 GAINED 或 NO_GAIN
```
模型只判断“本次结果是否推进当前诊断”,不评价 Tool 产品质量,也不输出 0 到 100 的主观分数。
## 7. 模型步骤契约
模型收到未被 Harness 客观标记为 `NO_GAIN` 的成功非空结果后,如果要继续调用 Tool,必须在下一次 Tool Call 中给出上一调用的信息增益。该设计不要求模型显式声明下一步行动。
```json
{
"previous_observation": {
"tool_call_id": "call-123",
"information_gain": "NO_GAIN"
},
"input": {
"query": "下一次查询参数"
}
}
```
约束:
1. `previous_observation` 只在上一轮存在待模型评价的 Tool Observation 时必填;首次调用以及上一轮已由 Harness 客观赋值时省略。
2. `tool_call_id` 必须指向当前 Run 中最后一个尚未评价的成功 Tool 调用。
3. Harness 必须先校验并应用 `information_gain`,再判断是否允许执行本次 Tool 调用。
4. Harness 消费并剥离 `previous_observation`,业务 Tool 只接收 `input` 中的原有业务参数。
5. 上一个待语义评价的结果未被评价时,Harness 不接受新的 Tool 调用。
6. Harness 已经进入 `SATURATED` 时,新的 Tool Call 被拒绝,并向模型注入 `STOP_REQUIRED`。
7. 模型主动判断没有合理查询方向时,应直接输出证据不足 Draft,不必等待 Harness 强制停止,也无需为放行下一次调用而回传最后一轮评价。
模型行为直接从实际输出推导:
```text
Tool Call -> 继续收集
正常 DiagnosisDraft -> 完成诊断
证据不足 DiagnosisDraft -> 主动停止
```
该 Envelope 是模型侧 Tool 调用协议。服务端为动态注册的 Tool Schema 统一增加 Envelope,Harness 在边界处消费控制字段,业务 Tool 的入参协议保持不变。这是一项有意的协议变更,影响模型 Tool Schema 生成、Tool Call 解析、Harness 拦截和相关测试,不影响 Tool Backend。
## 8. Harness 饱和规则
第一版使用连续低收益计数,不维护复杂进展账本。硬调用上限是独立资源保护,不作为信息饱和条件。
```text
evidence_status=NO_EVIDENCE -> consecutiveLowYield + 1
重复 Tool + 相同 scope -> consecutiveLowYield + 1
成功非空结果(包括 REFERENCE)+ 模型 NO_GAIN
-> consecutiveLowYield + 1
其他成功非空结果 + 模型 GAINED -> consecutiveLowYield = 0
FAILED -> 技术故障流程,不计入低收益
consecutiveLowYield >= stopAfterConsecutiveNoGain
-> SATURATED
-> stop_reason=INFORMATION_SATURATED
达到单 Tool 或 Run 硬调用上限 -> stop_reason=BUDGET_LIMIT_REACHED
```
配置项 `stop-after-consecutive-no-gain` 默认值为 `2`、必须大于等于 `1`,只在 Run 启动时读取并固定,不进入模型上下文。默认值需要通过固定 E2E 和评测集校准,不作为不可调整的业务真理。
伪代码:
```text
onToolResult(result):
if result.execution_status == FAILED:
handleTechnicalFailure(result)
return
if isEmpty(result) or isRepeatedScope(result):
applyInformationGain(NO_GAIN)
return
requireModelAssessment(result.tool_call_id)
applyInformationGain(gain):
if gain == GAINED:
consecutiveLowYield = 0
else:
consecutiveLowYield += 1
if consecutiveLowYield >= stopAfterConsecutiveNoGain:
collectionState = SATURATED
stopReason = INFORMATION_SATURATED
onHardLimitReached():
stopReason = BUDGET_LIMIT_REACHED
stopToolCollection()
```
`SATURATED` 是当前 Run 的 Tool 收集终态。模型不能在同一 Run 中把它恢复为 `COLLECTING`。用户补充新范围或新事实后,应开启新的诊断 Run。
## 9. 控制与发布边界
### 9.1 Harness 层:物理停止
Harness 使用 `NO_EVIDENCE`、重复的 `tool_name + normalized_scope` 和连续 `NO_GAIN` 检测信息饱和。RAG `REFERENCE` 与其他成功非空结果一样,由 Diagnosis Agent 通过下一次 Tool Call Envelope 回传语义评价。硬调用上限只产生 `BUDGET_LIMIT_REACHED`,不伪装成信息饱和。
### 9.2 SemanticGuard 层:无证据推断兜底
只有 `DiagnosisDraft.conclusion` 非空时才进入完整 EvidenceGuard、EvidenceRepair 和 SemanticGuard 链路。SemanticGuard 在最终发布前检查:
- 正常诊断中的关键结论是否绑定已验证证据。
- `REFERENCE` 和通用资料是否被错误当作当前故障事实。
- scoped `NO_EVIDENCE` 是否被错误解释为“故障不存在”。
- 证据不足时是否生成了确定性根因。
发现无证据推断时,不继续 Tool Loop,而是转换为有界的证据不足结果。
`conclusion=null` 是合法业务结果,不触发 EvidenceRepair,也不要求额外调用 SemanticGuard 来证明“没有结论”。已有 analysis 时只校验其 Tool 引用真实性;Release 从 `ProgressSnapshot` 和 `limitations` 生成安全 Fallback。零次 Tool 调用且 `limitations.missing_info` 非空时,直接生成 `MISSING_REQUIRED_CONTEXT` Fallback。
### 9.3 ReAct Prompt 层:合法放弃许可证
Prompt 必须明确:
- 不要求模型必须得出诊断结论或根因,但必须输出一个诚实、有界的完整 Draft。
- 无法得到足够证据时,`conclusion=null` 是成功完成,不是失败。
- Tool 调用次数可以为零。缺少企业、时间范围、服务或错误信息,且不存在预期能产生新诊断信息的明确查询时,直接在 `limitations.missing_info` 中列出缺口。
- 不得为了表现“已经排查”而调用没有明确目标的 Tool。
- Tool 返回内容事实正确、表述完整或结果非空,不代表它对当前诊断有信息增益。
- 通用知识、背景说明、重复内容或不能改变当前判断的内容属于 `NO_GAIN`。
- `NO_GAIN` 后不得通过改写相似关键词或重复相同 scope 继续尝试。
- 如果仍存在范围明确、可能产生新诊断信息的不同查询,可以继续;否则应主动停止。
- 不得为了表现“尽力”而重复或扩大无明确目标的查询。
- 收到 `STOP_REQUIRED` 后不得继续调用 Tool。
Prompt 不包含 Tool 名称、Tool Schema 或需要展开 Schema 的文本占位符。Tool 只通过服务端的模型原生 Tool Calling 通道注册;通用 Envelope 由服务端包装动态 Tool Schema。
### 9.4 会话上下文层:历史边界认知
每条 Tool 结果只以白名单 `Model Observation` 进入模型上下文一次。Harness 不在后续步骤中重复注入完整 Tool 结果、原始响应或内部进展账本。
只有 Harness 必须改变模型行为时,才注入有界停止指令:
```text
stop_required
stop_reason
已检查 Tool 和 scope 的有界摘要
```
不向模型注入 `consecutiveLowYield`、阈值、剩余预算、重复指纹、原始相似度、完整 Tool 原始载荷或内部 thought。
### 9.5 Release 层:统一安全发布
`DiagnosisReleaseUseCase` 是诊断业务发布的唯一决策入口:
```text
DiagnosisDraft.conclusion 非空
-> EvidenceGuard
-> 必要时 EvidenceRepair
-> SemanticGuard
-> SUCCESS 或 FALLBACK
DiagnosisDraft.conclusion 为空
-> 不执行 EvidenceRepair
-> 校验已有 Tool 引用(如有)
-> ProgressSnapshot + limitations
-> FALLBACK
Harness 已停止且没有 DiagnosisDraft
-> stop_reason + ProgressSnapshot
-> 确定性 FALLBACK
最终 Draft 为空或违反严格 JSON/Schema 合同
-> 丢弃非法 Draft,不做宽松提取或模型修复
-> ProgressSnapshot 有已验真 observed facts:INSUFFICIENT_EVIDENCE
-> 无安全进展:FAILED
```
Draft 合同失败不是新的 `stop_reason`。Trace 只记录固定失败类别、输出字节数和是否存在可发布进展,不记录模型原文、字段值或解析异常文本。
`ChatApplicationUseCase` 只负责编排、持久化,以及把无法形成安全业务内容的不可恢复故障映射为 `FAILED / CANCELLED`。预算 Fallback 不再由它单独构造。`ChatSseSession` 只发送 `content|failure + done`,不判断诊断语义。
## 10. 生命周期
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED / 低收益清零
COLLECTING --> COLLECTING: NO_GAIN / 未达到阈值
COLLECTING --> SATURATED: 连续 NO_GAIN 达到阈值
COLLECTING --> BUDGET_STOP: 达到硬调用上限
COLLECTING --> TECHNICAL_FAILURE: 不可恢复的 Tool 或基础设施故障
COLLECTING --> DRAFT_READY: 模型输出 DiagnosisDraft
COLLECTING --> DRAFT_INVALID: 最终 Draft 为空或违反合同
COLLECTING --> CANCELLED: 用户取消
SATURATED --> RELEASE_INPUT: INFORMATION_SATURATED + ProgressSnapshot
BUDGET_STOP --> RELEASE_INPUT: BUDGET_LIMIT_REACHED + ProgressSnapshot
DRAFT_READY --> RELEASE_INPUT: Draft + ProgressSnapshot
DRAFT_INVALID --> RELEASE_INPUT: 有已验真 observed facts
DRAFT_INVALID --> FAILED: 无安全进展
RELEASE_INPUT --> EVIDENCE_GUARD: conclusion 非空
RELEASE_INPUT --> FALLBACK: conclusion 为空或无 Draft
EVIDENCE_GUARD --> SEMANTIC_GUARD: 引用有效或修复成功
EVIDENCE_GUARD --> FALLBACK: 引用无法安全验证
SEMANTIC_GUARD --> SUCCESS: 证据支持最终结论
SEMANTIC_GUARD --> FALLBACK: 无证据推断
TECHNICAL_FAILURE --> FAILED
SUCCESS --> [*]
FALLBACK --> [*]
FAILED --> [*]
CANCELLED --> [*]
```
## 11. 示例
查询:`诊断切换企业失败的问题`
```text
第 1 次 lookup_knowledge
execution_status = SUCCEEDED
returned_count = 5
relevance_level = REFERENCE
information_gain = NO_GAIN(模型)
consecutiveLowYield = 1
第 2 次 query_logs
execution_status = SUCCEEDED
returned_count = 0
information_gain = NO_GAIN(Harness)
consecutiveLowYield = 2
Harness
collectionState = SATURATED
拒绝新的 Tool 调用
注入 STOP_REQUIRED
最终结果
stop_reason = INFORMATION_SATURATED
release_outcome = FALLBACK
SafeFallback.type = INSUFFICIENT_EVIDENCE
展示已检查范围、客观结果、限制和需要补充的信息
不发布 INTERNAL_FAILURE
```
如果第二次查询返回了能够排除某个假设的日志,模型应标记 `GAINED`,低收益计数清零,允许继续进行有目标的诊断。
如果用户首次请求即缺少企业、时间范围等有效查询条件,模型可以零次 Tool 调用直接输出 `conclusion=null`,把缺口写入 `limitations.missing_info`。Release 将其发布为 `FALLBACK + MISSING_REQUIRED_CONTEXT`,不执行 EvidenceRepair 或 SemanticGuard。
## 12. 非目标
- 不通过简单增加 Tool、Token 或超时预算解决空转。
- 不让 Tool 或 Harness 判断业务根因。
- 不增加多级质量分数、`UNKNOWN` 或复杂状态矩阵。
- 不引入 `new_count` 和跨 Tool 通用内容指纹。
- 不新增独立 Progress Judge 模型调用。
- 不重新引入 Planner/Executor/Verifier/Composer 多角色 Graph。
## 13. 模型 Token 与 Tool 拒绝审计
Run 使用 `RunBudget` 作为 Token 总账,`diagnosis_trace_event` 作为模型调用明细账,`AgentStep.token_count` 只作为 Diagnosis Agent 轮次摘要,不新增独立 Token 表。
```mermaid
flowchart LR
R["Router / System / Knowledge / Repair / Semantic"] --> G["GuardModelCall"]
A["Diagnosis ReAct round"] --> I["HarnessModelInterceptor"]
G --> U["Provider Usage"]
I --> U
U --> L["Run ModelCallLedger"]
U --> B["RunBudget Token 总账"]
U --> T["MODEL_TOKEN_USAGE Trace"]
I --> S["AgentStep.token_count"]
L --> F["RUN_FINISHED 对账摘要"]
B --> F
```
每个 `MODEL_TOKEN_USAGE` 只记录:
- `component` 与 `component_round`;
- `usage_available`;
- Usage 可用时的 `input_tokens / output_tokens / total_tokens`。
Usage 不可用时不写 Token 字段,不把未知值记成零;Run 结束以 `usage_unavailable_count` 和 `tokens_reconciled=false` 暴露缺口。`RUN_FINISHED` 同时记录预算总量与审计明细合计,便于 exact Run 对账。
Tool 请求进入 `HarnessToolInterceptor` 后若被进展协议、重复 scope、信息饱和或观察合同门禁拒绝,写入 `TOOL_REQUEST_REJECTED`。事件只含安全 Tool Call ID、Tool name 和稳定错误码,不含业务参数、normalized scope 正文、原始响应或内部异常。实际执行仍只由 `TOOL_INVOCATION` 表达,因此两类数量不能混用。
协议拒绝与信息增益相互独立:`INVALID_PROGRESS_PROTOCOL` 不累计 `NO_GAIN`,而是维护连续协议错误计数。未达阈值时返回可修正 observation(`repair_required`、`violation_type`、期望上一轮 Tool Call ID、允许的 `information_gain`);达到 `stop-after-consecutive-progress-protocol-violations`(默认 2)后交付一次 `STOP_REQUIRED/PROGRESS_PROTOCOL_VIOLATED`,继续请求 Tool 走受控停止。`TOOL_REQUEST_REJECTED` 额外记录安全 `violation_type`、`repair_prompt_delivered`、连续协议错误次数和 stop reason。
## 14. 验收标准
- [x] Tool 执行状态、信息增益、收集状态和发布状态职责分离。
- [x] RAG `REFERENCE` 不再自动等同于诊断证据。
- [x] `NO_EVIDENCE` 和重复的 `tool_name + normalized_scope` 被 Harness 确定性标记为 `NO_GAIN`;`REFERENCE` 由模型评价。
- [x] 模型继续调用 Tool 前,对上一轮待评价结果给出 `GAINED` 或 `NO_GAIN`。
- [x] 下一次 Tool Call Envelope 能回传上一轮语义评价;Harness 应用评价后才决定是否放行,并在调用业务 Tool 前剥离控制字段。
- [x] 不引入独立 `next_action`;Harness 从 Tool Call 或 Draft 等实际输出推导模型行为。
- [x] `stop-after-consecutive-no-gain` 可配置且默认值为 `2`;连续低收益达到阈值后 Harness 阻止新的 Tool 调用。
- [x] 只对 `tool_name + normalized_scope` 做确定性去重,第一版不承诺自然语言语义去重。
- [x] `INFORMATION_SATURATED` 与 `BUDGET_LIMIT_REACHED` 分开,硬预算不进入 `SATURATED`。
- [x] `FAILED` 不被统计为 `NO_GAIN`,技术故障与证据不足保持区分。
- [x] 模型可以零次 Tool 调用输出 `conclusion=null + limitations.missing_info`,收到 `STOP_REQUIRED` 后必须停止。
- [x] `conclusion=null` 不触发 EvidenceRepair;只有存在结论时才执行完整 EvidenceGuard、EvidenceRepair 和 SemanticGuard 链路。
- [x] SemanticGuard 能把无证据确定性结论转换为安全的证据不足结果。
- [x] `DiagnosisReleaseUseCase` 统一处理 Draft、信息饱和和预算终止,并复用 `SafeFallback.observed_facts` 展示 `ProgressSnapshot`。
- [x] 空或非法 Draft 仅在有已验真进展时降级为 `INSUFFICIENT_EVIDENCE`,无安全进展继续 fail closed。
- [x] 不引入 `CONFIRMED / INSUFFICIENT_EVIDENCE / NEED_MORE_INFO` 第二套生命周期状态;后两种语义由 `SafeFallback.type` 表达。
- [x] Tool Schema 只通过模型原生 Tool Calling 通道注册,不拼接进 Prompt。
- [x] 原始复现 Query 在预算耗尽前正常收敛,不再发布通用 `INTERNAL_FAILURE`。
- [x] Trace 能解释每轮信息增益和停止原因,但不保存原始 Tool 载荷或内部 thought。
- [x] 模型调用 Token 可按组件和轮次对账,Run 总账与明细账差异显式可见。
- [x] Tool 请求拒绝与实际 Tool invocation 分开审计,且拒绝事件不泄露 payload。
+11 -1
View File
@@ -1,6 +1,6 @@
# Harness 与质量门禁
**更新日期**:2026-07-23
**更新日期**:2026-07-27
**状态**:当前可运行架构
## 1. Harness 定位
@@ -57,6 +57,16 @@ SemanticGuard 使用隔离的单轮模型调用,只接收原始 query、完整
Trace 写入失败只记录警告,不应改变业务执行结果;`details` 禁止包含 Prompt、Thought、Draft 正文或 raw Tool payload。普通 Trace API 按 `sequence_no, id` 返回 Timeline。
### 6.1 Token 对账
所有 Harness 模型入口统一从 Provider Usage 记录 `MODEL_TOKEN_USAGE`:Router、System Chat、Knowledge Answer、Diagnosis Agent、Evidence Repair 和 SemanticGuard。事件按组件和组件轮次记录 input/output/total Token;Usage 不可用时只记录 unavailable,不估算消耗。
`RUN_FINISHED` 同时记录 RunBudget 总账、模型调用明细合计、不可用 Usage 数量和 `tokens_reconciled`。Diagnosis Agent 的每轮 total Token 还会回填 `AgentStep.token_count`;其他模型组件不创建伪 AgentStep。
### 6.2 Tool 拒绝
`TOOL_INVOCATION` 只表示进入业务 ToolBoundary 的实际执行。协议错误、重复 scope、信息饱和或观察合同拒绝使用独立 `TOOL_REQUEST_REJECTED`,只记录 Tool Call ID、Tool name 和稳定错误码。预算 Tool 计数、实际执行次数和 Harness 拒绝次数是三个不同观察维度。
## 7. Audit 安全
AgentStep 不保存 Prompt、消息正文、模型正文、Tool arguments 或 Thought。ToolInvocation 不保存完整 request、SQL/日志 query、raw response 或 Agent projection。Provider reasoning 仅写入独立 `agent_reasoning_audit`,不进入普通 Trace 或发布结果;无 Provider 内容时必须记录 unavailable,不能伪造。应用日志不得打印这些字段。
+7 -6
View File
@@ -37,7 +37,8 @@ disconnect、timeout 与 send failure 通过同一个 `ChatRunControl` 请求取
Redis canonical invocation 不是 Trace API 的长期响应内容;它只供当前 Run EvidenceGuard 验真。
普通 Trace 不读取 `agent_reasoning_audit`,也不返回 reasoning 原文。Agent 模型步骤和 Timeline 只暴露 `reasoning_available`、`reasoning_bytes` 等有界 metadata。
普通 Trace 不读取 `agent_reasoning_audit` 的 **原文**。Agent 模型步骤和 Timeline 只暴露 `reasoning_available`、`reasoning_bytes` / `assistant_bytes`、`content_source` 等有界 metadata。
普通 Trace 的 `run` 可返回 `query` 与提取后的 `conclusion`(业务结论读出,非 thinking)。
## 4. PreviousTurn
@@ -45,11 +46,11 @@ Redis canonical invocation 不是 Trace API 的长期响应内容;它只供当
## 5. Reasoning 审计查询
`GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}` 提供独立 reasoning 审计读取:
`GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}` 提供独立 LLM 步骤审计读取:
- `runId` 必填,服务端先验证 Run 存在且属于 path `sessionId`,禁止跨 Session 串读。
- 结果按 `step_index` 返回 Agent、reasoning availability、受限原文、字节数和创建时间。
- Provider 未返回 reasoning 时仍保留 unavailable 记录,以区分“没有返回”与“审计遗漏”。
- Reasoning 数据不回流到 PreviousTurn,不进入普通 Trace、SSE、Evidence Snapshot 或发布结果。
- 结果按 `step_index` 返回:`reasoningAvailable`、`reasoningContent`、`assistantText`、`contentSource`、`contentBytes`、创建时间等。
- Provider 未返回 reasoning 时仍保留记录(`reasoningAvailable=false`,可仍有 `assistantText`),以区分“没有 thinking”与“审计遗漏”。
- Reasoning / assistant 审计原文不回流到 PreviousTurn,不进入普通 Trace 的 steps/timeline 原文、SSE、Evidence Snapshot 或发布结果。
该端点属于敏感审计面。当前完成了分表、独立查询和归属校验;身份认证、权限模型、保留期限、加密及真实 Provider/V015 验证尚未完成,由 ISS-015 阶段 3 收敛。
该端点属于敏感审计面。分表、独立查询、归属校验,以及 DeepSeek 真实 thinking 捕获路径(`DeepSeekAssistantMessage.reasoningContent`)与 V015–V017 迁移已验证;身份认证、权限模型、保留期限、加密仍由 ISS-015 阶段 3 收敛。
+8 -5
View File
@@ -2,11 +2,14 @@
- [ ] SSE metadata 的 `session_id`、`run_id` 非空且与数据库完全一致。
- [ ] `diagnosis_run.intent=DIAGNOSIS`,status/release_outcome 与 done outcome 一致。
- [ ] `diagnosis_run.conclusion` 与发布结论一致(SUCCESS 时非空短结论;FALLBACK 可为 type/message 摘要)。
- [ ] `agent_step.agent_name` 只出现 `diagnosis_agent`。
- [ ] AgentStep model_input/model_output 只含 metadata,thought 为空。
- [ ] ToolInvocation 全部属于 exact runId,Tool 名在 ACI allowlist 内。
- [ ] ToolInvocation input/output/retrieval details 不含 SQL、日志 query、raw response 或 evidence body。
- [ ] AgentStep `model_input`/`model_output` 只含有界 metadata(可含 `reasoning_available`/`content_source`/bytes);**不含** reasoning/assistant 原文。
- [ ] `agent_step.thought` 若非空,应为 reasoning 或 assistant 的兼容镜像,不得替代 `/trace/reasoning` 双字段验收。
- [ ] `/trace/reasoning`:DeepSeek thinking 开启时应有 `reasoningAvailable=true` 且 `reasoningContent` 非空;`assistantText` 有正文和/或 tool-call 计划;`contentSource` 合理;**无** tool 结果体。
- [ ] ToolInvocation 全部属于 exact runId,Tool 名在 ACI allowlist 内;`step_id` 能挂到对应 `agent_step.id`。
- [ ] ToolInvocation input/output/retrieval details 不含 SQL 全文、raw response 或 evidence body 泄露(query 等允许的有界字段除外)。
- [ ] Run 的模型/Tool/Token/字节预算均未超过集中配置。
- [ ] content 只出现一次并来自 Release Policy;failure 与 content 互斥。
- [ ] `logs/application.log` 不含 Prompt、Thought、完整 Tool 参数、raw response、vendor exception 或 stack 泄漏。
- [ ] query_logs 标记为 Mock;query_mysql 只使用隔离只读 datasource contract。
- [ ] `logs/application.log` 不含 Prompt、完整 reasoning 原文、完整 Tool 参数、raw response、vendor exception 或 stack 泄漏。
- [ ] query_logs 标记为 Mock;query_mysql 仅在配置了隔离 datasource 时注册。
+90
View File
@@ -0,0 +1,90 @@
# MVP 工程纪要(Engineering Notes)
**更新日期**:2026-07-30
**定位**:项目推进中真实遇到的问题、决策、解决思路与 E2E 验收叙事。
与其它目录分工:
| 目录 | 读什么 |
|---|---|
| [architecture/](../architecture/) | **现行架构**(系统现在怎么跑) |
| **engineering/**(本目录) | **为何这样定**、踩坑、方案取舍、live 验收导读 |
| [issues/](../issues/) | 未完成事项与状态 |
| [tables/](../tables/) | 表结构说明 |
|
| `devflow/projects/` | 单次变更的 brief/decisions/evidence 切片 |
本文档**不是** API 规范的唯一真理源;冲突时以 `architecture/` 与代码为准。
---
## RAG
> 2026-09-29 RAG 模块已抽离为独立 py-rag 知识服务(架构见
> [../architecture/RAG知识检索架构.md](../architecture/RAG知识检索架构.md))。
> 下列纪要保留决策过程价值,涉及进程内 Milvus / L0 的实现细节以各文顶部说明为准。
| 文档 | 内容 |
|---|---|
| [rag/RAG证据链探索笔记-从py-rag响应到引用验真.md](rag/RAG证据链探索笔记-从py-rag响应到引用验真.md) | **抽离后链路首发导读**:数据逐站形态、关键字段、设计哲学与化石清单 |
| [rag/RAG排序-多路召回与RRF.md](rag/RAG排序-多路召回与RRF.md) | K、多路融合、RRF、L0 边界(实现已下沉 py-rag,判断框架仍有效) |
| [rag/RAG-Hybrid质量分与后处理.md](rag/RAG-Hybrid质量分与后处理.md) | qualityScore 统一、后处理(分数语义现为 RERANK 直传) |
| [rag/RAG-Agent如何读relevance_level.md](rag/RAG-Agent如何读relevance_level.md) | Agent 侧相关度标签含义与误读(现行) |
| [rag/RAG离线评测-基线设计.md](rag/RAG离线评测-基线设计.md) | Golden/Fixture、hybrid 评测与闸门(需按新语义重新校准) |
| [rag/Milvus-Hybrid接入清单.md](rag/Milvus-Hybrid接入清单.md) | Hybrid 交付拆分与接入清单(已过时,仅历史追溯) |
| [rag/RAG审计补丁-stepid-query-E2E验收.md](rag/RAG审计补丁-stepid-query-E2E验收.md) | step_id / query 审计 live 验收(现行) |
架构对照:
- [architecture/RAG知识检索架构.md](../architecture/RAG知识检索架构.md)
- [architecture/RAG检索可观测性与审计.md](../architecture/RAG检索可观测性与审计.md)
相关 Issue:
- [ISS-017 L0 过滤收窄与 Fallback 加固](../issues/active/ISS-017-rag-l0-filter-fallback-hardening.md)(已失效:L0 下沉 py-rag,问题前提不复存在)
---
## Harness
| 文档 | 内容 |
|---|---|
| [harness/README.md](harness/README.md) | **从这里开始**:用一次诊断请求理解 Harness,不要求先掌握组件和状态 |
| [harness/Harness面试速查-一张图讲清设计.md](harness/Harness面试速查-一张图讲清设计.md) | **收尾速查**:一张架构图、一条请求主链、三个核心决策和常见面试追问 |
| [harness/案例-从一次支付超时诊断看Harness如何控制Agent.md](harness/案例-从一次支付超时诊断看Harness如何控制Agent.md) | **案例导读**:跟随一次真实支付超时 Run,看 Agent、Tool、Guard 和 Release 如何协作 |
| [harness/Harness设计演进-从多Agent编排到确定性控制边界.md](harness/Harness设计演进-从多Agent编排到确定性控制边界.md) | **设计演进**:从多 Agent、Gatekeeper 和 StateGraph 逐步收敛到 Single ReAct + Harness |
| [harness/Harness失败图谱-异常-停止-降级与终态.md](harness/Harness失败图谱-异常-停止-降级与终态.md) | **失败图谱**:用可继续性、安全进展和发布资格解释异常、停止、降级与终态 |
| [harness/components/README.md](harness/components/README.md) | **组件渐进式导读**:沿一次请求分四步理解运行控制、Agent 收敛、Tool 事实边界和验证发布 |
| [harness/CONTEXT.md](harness/CONTEXT.md) | Harness 统一术语、命名规则、状态维度与已知命名债务 |
| [harness/Harness生命周期与状态.md](harness/Harness生命周期与状态.md) | Run、Tool、Progress、Guard、Release、SSE 和持久化生命周期及状态映射 |
| [harness/Harness设计-非确定性Agent的确定性控制边界.md](harness/Harness设计-非确定性Agent的确定性控制边界.md) | Harness 根问题、设计不变量、方案取舍、关键决策、代价与真实问题反推 |
| [harness/Harness组件全景-职责-设计原因与边界.md](harness/Harness组件全景-职责-设计原因与边界.md) | 当前 Harness 10 个职责域、全部生产类型、设计原因、作用与边界 |
| [harness/Harness-Tool双视图-从原始结果到可验证证据.md](harness/Harness-Tool双视图-从原始结果到可验证证据.md) | canonical truth、Control View、Agent Observation 与 metadata audit 的数据边界 |
| [harness/Harness证据安全链-从引用真实到结论可发布.md](harness/Harness证据安全链-从引用真实到结论可发布.md) | EvidenceGuard、EvidenceRepair、SemanticGuard 和唯一 Release Policy |
| [harness/Harness信息增益停止-让无证据诊断正常收敛.md](harness/Harness信息增益停止-让无证据诊断正常收敛.md) | GAINED/NO_GAIN、重复检测、协议停止、ProgressSnapshot 与过程型 Fallback |
---
## 审计
| 文档 | 内容 |
|---|---|
| [audit/README.md](audit/README.md) | **从这里开始**:从“这份答案为什么可信”理解审计系统,不要求先掌握表和事件类型 |
| [audit/从一次诊断Run看审计系统如何记录决策.md](audit/从一次诊断Run看审计系统如何记录决策.md) | **真实 Run 案例**:从最终结果倒推 Run、Timeline、Agent Step、Tool、Token 与 Release 决策 |
| [audit/审计系统设计-从调试日志到可回放的决策证据.md](audit/审计系统设计-从调试日志到可回放的决策证据.md) | **主设计**:日志为何不够、设计不变量、typed decision、数据分层、失败语义与当前代价 |
| [audit/审计设计演进-从SessionTrace到ExactRun.md](audit/审计设计演进-从SessionTrace到ExactRun.md) | **设计演进**:从 Session Trace、Evidence 语义和多轮串线,演进到 canonical/durable 分层、Timeline、Token 与 Reasoning |
---
## 诊断 / E2E
| 文档 | 内容 |
|---|---|
| [diagnosis/一次诊断全流程-E2E导读.md](diagnosis/一次诊断全流程-E2E导读.md) | SUCCESS 全流程:阶段、token、timeline、字段 |
| [diagnosis/Session-Run-Trace隔离-从串线到可回放.md](diagnosis/Session-Run-Trace隔离-从串线到可回放.md) | 多轮串线问题、身份拆分、协议迁移与 exact-run E2E |
架构对照:
- [architecture/current-mvp-architecture.md](../architecture/current-mvp-architecture.md)
- [architecture/session-trace-lifecycle.md](../architecture/session-trace-lifecycle.md)
- [architecture/agent-orchestration.md](../architecture/agent-orchestration.md)
+95
View File
@@ -0,0 +1,95 @@
# 审计系统:先从一份已经返回的答案开始
这不是审计表结构手册,而是一页渐进式入门导读。
第一次阅读时,不需要记 `diagnosis_run`、`agent_step` 或事件类型。先回答一个问题:**系统已经给出了诊断答案,为什么还需要审计?**
## 1. 如果只有答案和日志,会发生什么
假设用户收到一份“数据库连接池可能耗尽”的诊断报告。业务日志也显示 Agent 和 Tool 都执行成功。
但系统仍然无法直接证明:
- 报告是否属于本轮请求,而不是混入同一 Session 的上一轮数据;
- Agent 是否真的调用了它声称使用的 Tool;
- Tool 成功是否等于找到了证据;
- EvidenceGuard 和 SemanticGuard 是否真正执行;
- 最终发布结果与 Run 终态、SSE outcome 是否一致;
- Router、Agent 和 Guard 的 Token 能否与总数对上。
这些不是“多打印几行日志”就能稳定解决的问题。它们需要明确的执行身份、决策类型、顺序和关联关系。
## 2. 审计在一次请求中做了什么
```mermaid
flowchart LR
Q["一次诊断请求"] --> R["建立 exact Run<br/>固定本轮身份"]
R --> D["在原始决策点记录<br/>Router / Agent / Tool / Guard / Release"]
D --> S["Run、Timeline、Step、Tool<br/>分别保存各自事实"]
S --> T["Trace 聚合回放<br/>计数与 Token 对账"]
T --> A["回答答案为何产生<br/>也诚实暴露审计缺口"]
```
顺着这条线,审计只做三类事情:
1. **固定身份**:用 `runId` 把一次执行与多轮 Session 分开。
2. **记录决定**:让真正做决定的组件留下有类型、有顺序的结构化事实。
3. **聚合对账**:从最终结果倒推 Timeline、Agent Step、Tool 和 Token,检查它们是否一致。
审计不会替 Agent 诊断,也不会替 Harness 决定能否发布。它负责让这些行为在事后可以被准确解释。
## 3. 先建立这个最小心智模型
```mermaid
flowchart TB
EXEC["一次 Agent 执行"] --> RUN["Run<br/>结果封面"]
EXEC --> TL["Timeline<br/>决策脊柱"]
EXEC --> DETAIL["Step / Tool<br/>行为明细"]
EXEC --> SENSITIVE["Reasoning<br/>独立敏感审计面"]
RUN --> TRACE["普通 Trace"]
TL --> TRACE
DETAIL --> TRACE
SENSITIVE -.-> RTRACE["独立受限查询"]
```
第一次阅读只需要记住:
> Run 告诉我们结果,Timeline 告诉我们决定怎样发生,Step 和 Tool 告诉我们模型与外部世界实际做过什么;它们通过 exact `runId` 组合成一条可回放的证据链。
到这里可以先停下,不需要继续记表名。
## 4. 推荐阅读顺序
```mermaid
flowchart LR
START["先建立直觉"] --> CASE["01 真实 Run 案例<br/>审计怎样使用"]
CASE --> DESIGN["02 审计主设计<br/>为什么这样设计"]
DESIGN --> EVOLUTION["03 设计演进<br/>为什么变成现在这样"]
EVOLUTION --> NEXT["后续专题<br/>数据模型、Token、Tool、Reasoning、失败"]
```
| 顺序 | 先回答的问题 | 阅读 |
|---|---|---|
| 01 | 拿到一份结果后,怎样从后向前回放整次执行? | [从一次诊断 Run 看审计系统如何记录决策](从一次诊断Run看审计系统如何记录决策.md) |
| 02 | 为什么日志不够,为什么需要 exact Run、typed event 和分层数据? | [审计系统设计:从调试日志到可回放的决策证据](审计系统设计-从调试日志到可回放的决策证据.md) |
| 03 | 这套设计经历了哪些真实问题,哪些阶段性方案后来被替换? | [审计设计演进:从 Session Trace 到 Exact Run](审计设计演进-从SessionTrace到ExactRun.md) |
建议一次只读一篇。第一篇建立使用直觉,第二篇理解设计取舍,第三篇再理解这些边界如何被真实问题一步步推出来;数据表、完整事件类型和代码类名都不是第一次阅读的前置知识。
## 5. 遇到具体问题时再往下读
| 当你想知道 | 当前资料 |
|---|---|
| 一次 SUCCESS 诊断的业务链路、字段和真实数值 | [一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md) |
| Session、Run、Trace 和 Reasoning 当前生命周期 | [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md) |
| exact Run 为什么出现,曾经发生过什么串线问题 | [Session-Run-Trace 隔离工程纪要](../diagnosis/Session-Run-Trace隔离-从串线到可回放.md) |
| 如何人工验收一条 Trace 是否完整和安全 | [Trace 检查清单](../../demo/trace-inspection-checklist.md) |
后续本目录会继续补充数据模型、Token、Tool、Reasoning、失败图谱和面试速查。它们会继续保持同样的渐进式结构,不要求从目录头到尾顺序阅读。
## 6. 与 Harness 文档的分工
- [Harness 入门](../harness/README.md)回答“如何让一次非确定性 Agent 执行受控并安全发布”。
- 审计文档回答“这些控制和决定如何被记录、回放和对账”。
- Harness 是执行控制边界,Audit 是决策证据边界;二者协作,但不是同一个职责。
@@ -0,0 +1,255 @@
# 从一次诊断 Run 看审计系统如何记录决策
这篇文章不从表结构和类名开始,而是从一份已经返回给用户的诊断报告开始,倒着追问:**这份答案为什么可以被系统发布?**
先说结论:审计系统不是把运行日志存下来,而是为每一次诊断建立一个独立的 `Run`,再分别记录决策顺序、模型步骤、Tool 调用和最终结果。查询时,这些记录才被重新聚合成一条可以回放、可以对账的执行证据链。
第一次阅读只看第 1、2、3、7 和 8 节即可。先建立直觉,再回来理解为什么要拆成多种记录。
## 1. 只有最终答案,为什么还不够
假设系统返回:
> 当前证据更支持数据库连接池耗尽这一排查方向,但缺少生产指标和实时日志,暂时不能确认最终根因。
这段话看起来很谨慎,但面试官、开发者或评测系统仍然会继续追问:
- 这是本轮请求产生的答案,还是混入了同一会话的上一轮数据?
- Agent 实际调用了什么 Tool,还是只在文本里声称自己查过?
- Tool 返回了候选资料以后,哪个模型步骤使用了它?
- EvidenceGuard 和 SemanticGuard 是否真的执行,最终是谁决定放行?
- 这次请求到底调用了几次模型、消耗多少 Token,数字能否对上?
普通应用日志可以告诉我们“某段代码运行过”,但很难稳定回答这些领域问题。日志行也没有天然的 Run 归属、决策类型和对账关系。
所以审计系统要解决的根问题不是“多记一些信息”,而是:
> 把一次非确定性的 Agent 执行,转换成一组具有明确身份、顺序、责任和边界的可验证记录。
## 2. 先看这次真实 Run
本文使用已有 SUCCESS E2E 样本:
| 项目 | 结果 |
|---|---|
| `session_id` | `e2e-rag-success-20260728162949` |
| `run_id` | `27415045-e674-41af-9cea-de01f27ce040` |
| 最终结果 | `SUCCESS / DIAGNOSIS_REPORT` |
| Agent Step | 2 |
| Tool Invocation | 1 次 `lookup_knowledge` |
| 模型调用 | Router 1 次、Agent 2 次、SemanticGuard 1 次 |
| Timeline | 15 个有序事件 |
| 总 Token | 13,236,`tokens_reconciled=true` |
| 总耗时 | 约 25 秒 |
业务视角看到的是一份诊断报告,审计视角看到的是报告背后的五个问题:
```mermaid
flowchart TB
OUT["用户收到 DIAGNOSIS_REPORT"]
OUT --> RUN["Run 摘要<br/>这次执行最终怎样结束?"]
OUT --> TL["决策 Timeline<br/>先后做过哪些决定?"]
OUT --> STEP["Agent Step<br/>模型每一轮做了什么?"]
OUT --> TOOL["Tool Invocation<br/>实际调用过什么?"]
OUT --> LEDGER["Token 账本<br/>资源消耗能否对上?"]
```
这五个视角不是重复保存同一份内容。它们分别回答不同的问题,并在 `run_id` 下汇合。
## 3. 审计的正确阅读顺序:从结果向前倒推
回放一次诊断时,不应该从第一条日志开始逐行翻。更有效的顺序是:先确认终态,再沿决策链向前寻找依据。
```mermaid
flowchart RL
FIN["RUN_FINISHED<br/>SUCCESS,Token 已对账"] --> REL["RELEASE_DECISION<br/>允许发布"]
REL --> SG["SEMANTIC_GUARD_DECISION<br/>SUPPORTED"]
SG --> EG["EVIDENCE_GUARD_INITIAL<br/>PASSED"]
EG --> A2["Agent Step 1<br/>生成报告草稿"]
A2 --> T1["Tool Invocation<br/>RAG 返回候选证据"]
T1 --> A1["Agent Step 0<br/>发出 Tool Call"]
A1 --> ROUTE["ROUTING_DECISION<br/>DIAGNOSIS"]
ROUTE --> START["RUN_STARTED"]
```
沿着这条链可以得到一个比“请求成功”更具体的结论:
1. 本次 Run 最终正常结束,并发布了诊断报告;
2. Release 放行前,SemanticGuard 给出 `SUPPORTED`;
3. SemanticGuard 之前,EvidenceGuard 已确认引用结构有效;
4. 报告草稿来自 Agent 第 2 轮;
5. 草稿使用的候选证据来自第 1 轮发出的真实 RAG Tool Call;
6. 整条链都属于同一个 exact `run_id`。
审计的价值就在这里:它不是保存一份“成功日志”,而是让最终结果可以逐层找到前置依据。
## 4. 第一层:Run 是这次执行的封面
`sessionId` 表示多轮对话目录,`runId` 才表示一次独立执行。
```mermaid
flowchart TB
S["chat_session<br/>同一个多轮会话"] --> R1["diagnosis_run A<br/>第一轮问题"]
S --> R2["diagnosis_run B<br/>第二轮问题"]
R1 --> D1["自己的 Step / Tool / Timeline"]
R2 --> D2["自己的 Step / Tool / Timeline"]
```
这个拆分来自真实问题:早期系统只使用 `sessionId`。同一会话连续执行两轮后,主记录会被后一轮覆盖,而 Agent Step 和 Tool Invocation 继续追加,最终造成 Trace、Feedback 和评测跨轮混合。
因此当前模型把两种身份分开:
- `sessionId` 回答“这些问题属于哪段对话”;
- `runId` 回答“这条记录属于哪一次执行”。
本次样本的 `diagnosis_run` 像一张封面,保存查询、状态、意图、发布结果、总耗时、总 Token 和最终安全内容。看到它,我们先知道故事结尾,但还不知道过程细节。
## 5. 第二层:Timeline 记录决定,不复制所有正文
本次 Run 的 15 个事件可以压缩成七个阶段:
| 阶段 | 关键事件 | 它证明什么 |
|---|---|---|
| RUN | `RUN_STARTED` | 一次独立执行已经建立 |
| ROUTING | Token、Attempt、Decision | Router 被真实调用,并选择 `DIAGNOSIS` |
| AGENT | 两轮 Token 与 Model Step | Agent 先规划 Tool,后生成草稿 |
| TOOL | `TOOL_INVOCATION` | `lookup_knowledge` 被实际执行 |
| EVIDENCE | `EVIDENCE_GUARD_INITIAL` | 引用和证据结构检查通过 |
| SEMANTIC | Token、Attempt、Decision | SemanticGuard 被调用并判断 `SUPPORTED` |
| RELEASE / RUN | Release、Finish | 报告被允许发布,Run 正常收尾 |
Timeline 只承担“什么时候做了什么决定”。它不保存完整 Tool raw,也不试图替代 Agent Step 和 Tool Invocation 的详细字段。
这是一个有意的设计取舍:如果把所有信息都塞进一张巨大的事件表,查询一条时间线会很方便,但模型步骤、Tool 证据和资源账本都会退化成难以约束的 JSON。当前方案让 Timeline 保持稳定的决策语义,领域明细继续由各自记录负责。
代价是读取时必须做聚合,不能只查一张表。但这个复杂度被集中在 Trace 查询服务中,而不是扩散给每个调用方。
## 6. 第三层:Step 与 Tool 共同证明“模型真的做过什么”
这次诊断有两个 Agent Step:
| Step | 模型行为 | Token | 关联结果 |
|---|---|---:|---|
| 0 | 没有正文,发出 `lookup_knowledge` Tool Call | 2,657 | 产生 1 条 Tool Invocation |
| 1 | 读取 Tool Observation,生成报告草稿 | 4,703 | 进入 EvidenceGuard |
`AgentStepAuditTracker` 在 Step 落库后绑定 `step_id`,Tool 执行时再把这个 ID 写入 `tool_invocation`。因此系统不只知道“这一轮有 Tool Call”和“某处有一次 Tool 调用”,还可以把两者连起来。
```mermaid
flowchart LR
S0["agent_step #0<br/>发出 lookup_knowledge"] -->|"step_id"| T["tool_invocation<br/>READY / EVIDENCE_FOUND"]
T --> O["有界 Observation"]
O --> S1["agent_step #1<br/>生成 Draft"]
```
Tool 审计也没有永久复制完整原始结果。长期记录的是 exact identity、Tool 名称、状态、耗时、请求和结果字节数、稳定错误码,以及 RAG 的检索模式、命中数和相关度等有界元数据。
完整 Tool 结果属于当前 Run 的短期 canonical truth,由 Harness 用于 EvidenceGuard 验真;它与长期 durable audit 的目的不同:
- canonical truth 要回答“当前发布校验依据的事实到底是什么”;
- durable audit 要回答“长期复盘时,这次调用发生了什么”。
把完整 raw 同时写进长期 Trace 虽然排障直接,但会扩大敏感数据、存储体积和保留治理范围,因此没有采用。
## 7. 第四层:Token 不是一个总数,而是一套可对账账本
如果只在 Run 结束时写一个 `total_token_count`,我们无法判断数字是否漏掉 Router、Guard 或隐藏重试。
本次 Run 的模型账本是:
| 组件 | Token |
|---|---:|
| Intent Router | 906 |
| Diagnosis Agent 第 1 轮 | 2,657 |
| Diagnosis Agent 第 2 轮 | 4,703 |
| SemanticGuard | 4,970 |
| 合计 | 13,236 |
对账关系为:
```text
sum(MODEL_TOKEN_USAGE.total_tokens)
= diagnosis_run.total_token_count
= RUN_FINISHED.run_total_tokens
= 13236
```
因此 `tokens_reconciled=true` 不是“记录了一个 Token 数”,而是三个独立视角得到相同结果。
还有一个容易误读的点:`agent_step.tokenCount` 之和只有 7,360,因为它只统计 Diagnosis Agent 的两轮;Run 总额还包括 Router 和 SemanticGuard。审计把组件和轮次分开,就是为了让成本和延迟可以定位,而不是只得到一个无法解释的总数。
Provider 没有返回 usage 时,系统会记录 `usage_available=false`,而不是把缺失伪造成 0 Token。缺数据本身也是需要被审计的事实。
## 8. 这套设计做了哪些关键选择
### 选择一:Trace 独立查询,不塞进 Chat 响应
- **问题**:Chat 面向用户流式返回结果,Trace 面向复盘和评测,生命周期与数据量不同。
- **决策**:通过只读 Trace API 按 `sessionId + runId` 聚合持久化证据。
- **放弃方案**:把完整 Trace 嵌入 `/api/chat` SSE。
- **代价**:调用方需要在收到 metadata 后保存 `runId`,再进行第二次查询。
### 选择二:exact Run 是审计边界
- **问题**:仅按 Session 查询曾造成多轮 Step、Tool、Feedback 和评测串线。
- **决策**:每次有效执行创建独立 Run,所有明细携带同一个 `run_id`。
- **兼容代价**:当前 API 仍保留“不传 `runId` 时读取 latest run”和 legacy 回退;这是迁移兼容,不是推荐的新调用方式。
### 选择三:决策 Timeline 与领域明细分开
- **问题**:单看 Run 摘要不知道过程,单看 Step 或 Tool 又不知道整体决策顺序。
- **决策**:Timeline 保存 typed decision event,Step 和 Tool 保存各自明细,查询时聚合。
- **放弃方案**:一张万能 Trace 表承载所有正文和字段。
- **代价**:需要维护事件与明细之间的计数、身份和顺序一致性。
### 选择四:普通审计长期保存有界信息
- **问题**:完整 Prompt、Reasoning 和 Tool raw 虽然方便排障,却会把敏感数据永久扩散到普通 Trace。
- **决策**:普通 Trace 以 metadata 为主;Reasoning 使用独立审计面,Tool raw 只在当前 Run 的 canonical store 中短期存在。
- **当前缺口**:Reasoning 的认证、权限、加密和保留期限仍未完全闭环;`agent_step.thought` 还存在兼容镜像语义,不能把当前状态描述成彻底隔离。
### 选择五:审计写入失败不改变业务结果
- **问题**:如果长期审计数据库短暂不可用,是否应该让一次本可安全完成的诊断直接失败?
- **决策**:durable audit 采用 fail-open,写入失败告警,但不改变业务结果。
- **边界**:用于 EvidenceGuard 验真的 canonical truth 不是普通审计;它缺失时无法证明证据,必须 fail-closed。
- **代价**:一次业务成功的 Run 可能存在审计缺口,所以 Trace summary 和计数对账必须显式暴露缺失,而不能假装记录完整。
## 9. 审计能证明什么,不能证明什么
审计可以证明:
- 某个结果属于哪个 exact Run;
- 哪些模型和 Tool 被实际调用;
- 决策以什么顺序发生;
- Tool Call 属于哪个 Agent Step;
- Guard 和 Release 给出了什么结果;
- Token、步骤数、Tool 数和 Timeline 数量是否对账。
审计不能自动证明:
- Tool 返回的外部数据本身一定正确;
- `SUPPORTED` 永远不会发生模型误判;
- 没有记录的 Reasoning 可以被事后还原;
- durable audit 写入失败时,缺失的事件仍然存在;
- 有了 Trace 就可以忽略权限、脱敏、保留期限和加密。
这也是为什么审计系统的目标不是“绝对正确”,而是让执行过程的身份、决定、依据和缺口都变得可见。
## 10. 先记住这一句话
> 一次诊断结束后,Run 告诉我们结果,Timeline 告诉我们决定是怎样发生的,Agent Step 和 Tool Invocation 告诉我们模型与外部世界实际做过什么,Token 账本负责对账;它们通过 exact `runId` 组合成一条可回放的决策证据链。
理解这句话,就已经抓住了审计系统的主干。表结构、事件全集、Reasoning 和失败治理都可以在需要时再展开。
## 11. 事实来源与延伸阅读
本文没有重新从源码推导设计,主要依据现有工程资料:
- [一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md):本文 SUCCESS 样本、15 个 Timeline 事件和 Token 数据来源;
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md):当前 exact-run、普通 Trace 与 Reasoning 边界;
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:为什么建设独立 Trace API;
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:Session/Run 串线问题与身份拆分决策;
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Harness-native Agent/Tool durable audit 和 canonical/durable 边界;
- [Trace 检查清单](../../demo/trace-inspection-checklist.md):当前验收边界与兼容镜像说明。
@@ -0,0 +1,328 @@
# 审计系统设计:从调试日志到可回放的决策证据
第一篇文章跟随一条真实 Run,展示了怎样从最终报告倒推出 Router、Agent、Tool、Guard 和 Release。这一篇换一个角度:**为什么这件事不能靠普通日志完成,系统又为什么选择了现在这套审计结构?**
先说核心判断:Agent 系统的审计对象不是代码执行过程,而是一次运行中产生的关键决定。日志可以帮助开发者定位异常,但审计必须让系统回答:这个决定属于哪次执行、由谁作出、依据是什么、先后关系怎样、最终结果是否与过程对得上。
第一次阅读只看第 1、2、3、6 和 9 节即可。它们构成最小设计主线;其余章节用于展开具体取舍。
## 1. 根问题:系统给出了答案,但无法证明答案怎样产生
一个最简单的实现可能只留下这样的日志:
```text
start diagnosis
call lookup_knowledge success
semantic check passed
finish diagnosis
```
这些日志能说明代码大概运行过,却回答不了几个关键问题:
- 它们是否属于同一个请求,还是混入了同一 Session 的另一轮执行?
- `success` 表示 Tool 正常返回、找到证据,还是结论已经被证据支持?
- 哪一轮 Agent 发出了 Tool Call,后续草稿是否真的使用了这次结果?
- `semantic check passed` 前是否完成了引用真实性检查?
- 最终报告、Run 终态和 SSE outcome 是否一致?
- Router、Agent 和 Guard 的 Token 能否与总数对上?
问题不在于日志太少。即使增加更多日志,仍然缺少稳定的身份、类型、顺序和关联关系。
| 普通日志擅长回答 | 审计必须回答 |
|---|---|
| 哪段代码报错了 | 哪一次 Run 在哪个决策阶段停止 |
| 某方法耗时多久 | Router、Agent、Tool、Guard 各自消耗多少 |
| 某次调用返回成功 | 调用是否产生证据,证据是否支持发布 |
| 当前进程发生了什么 | 持久化后能否精确回放同一次执行 |
| 给开发者阅读文本 | 给 API、评测和对账提供稳定结构 |
因此审计不是“更详细的日志”,而是一套独立的数据责任。
## 2. 设计目标:把非确定性执行变成可验证记录
这套审计设计需要满足六条不变量。
### 2.1 每条记录都有 exact Run 身份
`sessionId` 可以跨多轮复用,`runId` 只属于一次执行。Agent Step、Tool Invocation、Timeline 和最终结果必须落在同一个 `runId` 下。
### 2.2 决定在发生的位置被记录
Router 记录路由决定,ToolBoundary 记录真实 Tool 调用,Guard 记录校验结论,Release 记录最终发布决定。审计层不应在事后根据日志文本猜测发生了什么。
### 2.3 先后顺序可以稳定重放
同一个 Run 内,Timeline 使用单调 `sequence_no` 表达决定顺序。回放不依赖不同线程日志的打印时间,也不依赖数据库自增 ID 恰好连续。
### 2.4 不同记录只承担一种责任
Run 保存终态摘要,Timeline 保存决策脊柱,Agent Step 保存模型轮次,Tool Invocation 保存调用元数据,Reasoning 保存独立敏感正文。任何一种记录都不应成为无限扩张的万能 JSON。
### 2.5 长期记录必须有数据边界
普通 Trace 不永久复制完整 Prompt、Tool raw 和任意嵌套参数。缺少 Provider usage 时记录“不可用”,而不是伪造为 0;Reasoning 需要独立治理。
### 2.6 审计缺口必须可见,但不能随意改变业务语义
长期审计写入失败不应把一条本可安全完成的请求变成业务失败;与此同时,查询结果必须通过计数、对账状态和缺失标记暴露审计并不完整。
可以把这六条压缩成一句话:
> 审计记录必须属于精确执行、来自原始决定、顺序稳定、职责单一、内容有界,并且能够诚实表达缺失。
## 3. 整体设计:写入时分工,查询时聚合
审计没有设计成一个集中式拦截器抓取所有内容。真正知道“发生了什么”的组件,在自己的决策点产生结构化记录。
```mermaid
flowchart TB
REQ["一次 Chat 请求"] --> RUN["创建 exact Run"]
subgraph owners["决策所有者"]
ROUTER["Router<br/>路由尝试与决定"]
AGENT["Agent Hook<br/>模型步骤与 Reasoning"]
TOOL["ToolBoundary<br/>真实 Tool 调用"]
GUARD["Evidence / Semantic Guard<br/>验证决定"]
RELEASE["Release<br/>发布结果"]
end
RUN --> ROUTER
RUN --> AGENT
RUN --> TOOL
RUN --> GUARD
RUN --> RELEASE
ROUTER --> TL[("Decision Timeline")]
AGENT --> STEP[("Agent Step")]
AGENT --> RSN[("Reasoning Audit")]
TOOL --> INV[("Tool Invocation")]
AGENT --> TL
TOOL --> TL
GUARD --> TL
RELEASE --> TL
RUN --> SUMMARY[("Diagnosis Run")]
SUMMARY --> QUERY["Trace 聚合查询"]
TL --> QUERY
STEP --> QUERY
INV --> QUERY
RSN -.->|"独立受限端点"| RQUERY["Reasoning 查询"]
```
写入侧保持分工,读取侧由 Trace 服务形成一个面向复盘的 read model:
| 观察面 | 主要回答 |
|---|---|
| `diagnosis_run` | 这次执行最终怎样结束、用了多少资源、发布了什么 |
| `diagnosis_trace_event` | 决策按什么顺序发生 |
| `agent_step` | Diagnosis Agent 每一轮模型调用做了什么 |
| `tool_invocation` | 哪些 Tool 被真实调用,状态、耗时和有界结果怎样 |
| `agent_reasoning_audit` | Provider 是否返回 Reasoning,以及独立保存的敏感正文 |
这是一种“写入分散、读取聚合”的设计。分散不是随意写表,而是让记录权归属于最了解该决定的组件;聚合则把跨组件复杂度集中在 Trace 查询边界。
## 4. 为什么记录 typed decision,而不是事后解析日志
系统当前的 Timeline 使用稳定的 Phase 和 EventType,例如:
```text
RUN_STARTED
ROUTING_DECISION
MODEL_TOKEN_USAGE
AGENT_MODEL_STEP
TOOL_INVOCATION
EVIDENCE_GUARD_INITIAL
SEMANTIC_GUARD_DECISION
RELEASE_DECISION
RUN_FINISHED
```
这些名字表达的是领域事实,而不是实现细节。`SEMANTIC_GUARD_DECISION` 可以稳定表示语义门控的结果,即使以后底层模型客户端或方法名发生变化。
如果改为事后解析日志,会产生三个问题:
1. 日志文案改变就可能破坏审计;
2. 多线程和异步输出使顺序不可靠;
3. “Tool 执行成功”和“Tool 找到证据”很容易被同一个 `success` 混淆。
typed event 让状态语义在写入时就确定。它的代价是新增事件类型需要维护协议和测试,不能随意写一段字符串就算完成审计。
### 这不是 Event Sourcing
Timeline 虽然是追加式事件序列,但系统不会依靠它重建业务状态:
- `diagnosis_run` 仍保存当前终态和发布结果;
- Agent Step 与 Tool Invocation 仍有自己的领域记录;
- Timeline 用于解释“决定怎样发生”,不是整个系统的唯一事实源。
选择完整 Event Sourcing 会引入事件版本、状态重放、快照和迁移复杂度,当前 MVP 没有这项需求。
## 5. 为什么 Timeline 和领域明细必须分开
一种看起来更简单的方案,是把所有信息都写进 `diagnosis_trace_event.details`。这样只查询一张表就能得到全部内容。
但一张万能事件表会同时承担:
- 模型轮次和 Token 字段;
- Tool 请求、状态和 RAG 检索详情;
- Guard 决策;
- Run 终态和最终内容;
- Reasoning 与 assistant text。
最后所有约束都会退化为“不同 EventType 对应不同 JSON 结构”,数据库无法清楚表达关联、索引和数据治理。
当前设计把两类问题分开:
```mermaid
flowchart LR
Q1["什么时候做了什么决定?"] --> TL["Timeline<br/>有序、稳定、轻量"]
Q2["这个对象的详细事实是什么?"] --> DETAIL["Run / Step / Tool<br/>领域字段与关联"]
TL --> TRACE["Trace Read Model"]
DETAIL --> TRACE
```
例如,Timeline 的 `TOOL_INVOCATION` 只需要表达某次调用在决策链中的位置;`tool_invocation` 才负责 `step_id`、Tool 名、状态、耗时、bytes、错误码和 RAG 派生字段。
代价是查询时需要跨表聚合和计数对账,但数据所有权和长期演进更清楚。
## 6. 六个关键决策及其代价
### 决策一:Trace 使用独立只读 API
- **问题**:Chat SSE 面向实时用户体验,Trace 面向复盘、评测和排障,数据量与生命周期不同。
- **选择**:通过 `GET /api/diagnosis/{sessionId}/trace?runId={runId}` 聚合持久化记录。
- **未选**:把完整 Trace 嵌入 `/api/chat` 响应。
- **代价**:客户端必须保存 metadata 中的 `runId`,需要第二次请求才能读取 Trace。
这项决策在最初 MVP Trace 建设时就已确定:可观测性不侵入 Chat 输出协议。
### 决策二:`runId` 而不是 `sessionId` 定义审计边界
- **问题**:同一 Session 连续两轮执行时,主记录覆盖而 Step/Tool 追加,曾导致 Trace、Feedback 和评分跨轮污染。
- **选择**:`chat_session` 表示多轮目录,`diagnosis_run` 表示一次执行,所有明细按 `run_id` 隔离。
- **未选**:继续在旧主表上追加字段,或用“最新一轮”推断明细归属。
- **代价**:接口、反馈、评测和历史迁移都要理解 Session/Run 两级身份。
### 决策三:在原始决策点生成记录
- **问题**:集中式审计器无法准确知道 Router 为什么选择意图、Tool 是否真正执行、Guard 做出了什么领域判断。
- **选择**:让 Application、Router、Agent Hook、ToolBoundary、Guard 和 Release 各自在原始位置记录 typed fact。
- **未选**:请求结束后解析日志或根据最终结果反推中间过程。
- **代价**:每个新决策路径都必须显式接入审计,遗漏不会被“万能拦截器”自动补齐。
### 决策四:Timeline 与领域明细分离
- **问题**:既需要一条简单的决策脊柱,也需要可查询的 Step、Tool 和 Run 字段。
- **选择**:Timeline 记录顺序,领域表记录详情,读取时聚合。
- **未选**:单一超大 Trace 表或全部 JSON event。
- **代价**:需要维护 exact identity、顺序和 persisted/returned count 对账。
### 决策五:普通 Trace 只持久化有界信息
- **问题**:永久保存完整 Prompt、Tool raw、Reasoning 和参数,会放大敏感数据、体积和访问治理风险。
- **选择**:Tool durable audit 只保存有界元数据;完整 Tool truth 只在当前 Run 的 canonical store 中短期存在;Reasoning 使用独立表和独立端点。
- **未选**:为了排障方便,把所有上下文复制到 MySQL Trace。
- **代价**:长期 Trace 不能还原所有原始正文;Reasoning 还需要单独的权限、保留和加密治理。
### 决策六:durable audit fail-open,canonical truth fail-closed
- **问题**:审计数据库不可用时是否让业务失败,以及证据真理源不可用时是否仍允许发布。
- **选择**:长期审计写入失败只告警,不改变业务结果;用于当前 Run 验真的 canonical truth 缺失时不能继续证明证据。
- **未选**:所有审计失败一律中断请求,或所有审计失败都静默忽略。
- **代价**:业务成功不保证审计绝对完整,必须显式暴露 audit gap;canonical store 则成为发布安全的关键依赖。
## 7. 最重要的失败边界:同样叫“记录”,失败语义不同
Tool 执行后会形成两个用途完全不同的数据面:
```mermaid
flowchart TB
TOOL["Tool 执行结果"] --> CAN["Canonical Truth<br/>当前 Run 的完整验真依据"]
TOOL --> DUR["Durable Audit<br/>长期有界元数据"]
CAN -->|"可用"| GUARD["EvidenceGuard 验真"]
CAN -->|"写入失败"| CLOSED["fail-closed<br/>不能证明就不能发布"]
DUR -->|"可用"| TRACE["长期 Trace 可回放"]
DUR -->|"写入失败"| OPEN["fail-open<br/>告警并暴露审计缺口"]
```
为什么不能统一成一种策略?
- canonical truth 参与当前业务决策。没有它,EvidenceGuard 无法独立验证 Agent 引用;继续发布会改变安全语义。
- durable audit 服务长期复盘。它很重要,但让数据库短暂故障覆盖一条已经安全完成的业务结果,会把可观测性变成新的业务单点。
fail-open 不等于“失败无所谓”。审计记录器需要告警,Trace summary 需要对比持久化数量与返回数量,Token 账本需要给出 `tokens_reconciled`,Provider usage 缺失需要写明 `usage_available=false`。
## 8. 查询模型:普通 Trace 与敏感 Reasoning 分开
普通回放入口聚合:
```text
chat_session metadata
+ exact diagnosis_run
+ agent_step metadata
+ tool_invocation metadata
+ ordered diagnosis_trace_event
= DiagnosisTraceResponse
```
Reasoning 使用独立入口:
```http
GET /api/diagnosis/{sessionId}/trace/reasoning?runId={runId}
```
它要求显式 `runId`,并校验该 Run 属于 path 中的 `sessionId`。Provider 没有返回 Reasoning 时也记录 `reasoning_available=false`,不能用 assistant text 或人工摘要伪造。
分开读取的目的不是让敏感正文“换一张表就安全”,而是为后续访问控制、加密和保留期限提供独立治理边界。
## 9. 当前设计没有假装解决所有问题
当前仍有四类明确债务:
1. **兼容查询债务**:普通 Trace 不传 `runId` 时仍可读取 latest run,无 run-backed 数据时还能回退 legacy `diagnosis_session`。这是迁移能力,不是推荐契约。
2. **Reasoning 兼容镜像**:独立 Reasoning 表已经存在,但 `agent_step.thought` 仍可能保存 reasoning 或 assistant text 的兼容镜像,普通 Trace metadata-only 目标尚未完全收口。
3. **敏感治理未完成**:Reasoning 端点的身份认证、权限模型、保留期限和加密仍在 ISS-015 中,不能把“分表”描述成完整安全闭环。
4. **审计天然可能缺失**:durable audit fail-open 意味着必须把缺记录与业务失败分开诊断,不能默认数据库里没有就代表运行中没有发生。
这些不是文章末尾附带的 TODO,而是当前架构代价的一部分。审计系统的可信度来自诚实表达边界,不是把所有状态都包装成完整。
## 10. 方案对比:为什么没有选择看起来更简单的办法
| 方案 | 短期优势 | 没有采用的主要原因 |
|---|---|---|
| 只增加业务日志 | 实现快、开发者熟悉 | 缺少稳定身份、类型、关联和对账协议 |
| Chat 响应携带完整 Trace | 一次请求拿到全部数据 | 污染 SSE 协议,扩大响应和敏感数据暴露 |
| 一张万能 Trace 表 | 查询表面简单 | JSON 结构失控,领域约束、索引和治理困难 |
| 完整 Event Sourcing | 理论上可重放全部状态 | 当前只需解释决策,引入事件版本和状态重建过重 |
| 永久保存全部 raw | 排障最直接 | 敏感数据、成本和保留治理不可控 |
| 所有审计失败都 fail-closed | 记录最完整 | 长期可观测性会成为业务可用性的单点 |
| 所有审计失败都 fail-open | 业务最不易被阻断 | canonical truth 缺失时仍发布会破坏证据安全 |
## 11. 先记住这张图
```mermaid
flowchart LR
EXEC["非确定性 Agent 执行"] --> ID["exact Run 身份"]
ID --> DEC["原始决策点产生 typed record"]
DEC --> SPLIT["Timeline 与领域明细分工"]
SPLIT --> BOUND["普通审计有界<br/>Reasoning 独立"]
BOUND --> READ["Trace 聚合、计数与 Token 对账"]
READ --> EXPLAIN["可以回放,也能说明缺口"]
```
用一句话概括:
> 这套审计设计不是复制运行内容,而是让每个决策所有者在 exact Run 内留下有界、可排序、可关联的结构化事实,再通过独立查询模型把这些事实聚合成可回放、可对账的证据链。
## 12. 事实来源与延伸阅读
- [从一次诊断 Run 看审计系统如何记录决策](从一次诊断Run看审计系统如何记录决策.md):用真实 SUCCESS Run 查看这些设计怎样落到记录中;
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md):当前身份、生命周期和查询契约;
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:独立 Trace API 的初始问题与决策;
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:多轮串线证据和 exact Run 决策;
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Harness-native Agent/Tool audit 与 canonical/durable 失败边界;
- [ISS-015 诊断运行质量与 Reasoning 审计收敛](../../issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md):Reasoning 当前完成度和剩余治理缺口。
@@ -0,0 +1,398 @@
# 审计设计演进:从 Session Trace 到 Exact Run
今天看到的审计系统包含 exact Run、决策 Timeline、Agent Step、Tool Invocation、Token 对账和独立 Reasoning。它看起来像一套预先设计好的完整架构,但真实过程并不是这样。
这套设计是被一系列具体问题推出来的:先是答案无法回放,然后是 Tool 记录语义不一致,再后来是同一 Session 多轮串线,最后 Single ReAct 重构又让旧审计链失去所有权。每次变化都解决了当时最紧迫的问题,也留下了下一阶段才看得见的新缺口。
这篇文章不按提交逐条记流水账,而是解释六次能力跃迁。第一次阅读只看第 1、2、4、7 和 10 节,就能抓住主线。
## 1. 先看完整演进
```mermaid
flowchart LR
A["只能看到最终答案"] --> B["01 可查询 Trace<br/>答案可以回放"]
B --> C["02 Evidence 与质量语义<br/>记录开始可比较"]
C --> D["03 exact Run<br/>多轮不再串线"]
D --> E["04 Canonical Tool Truth<br/>事实与长期审计分层"]
E --> F["05 Single ReAct 审计重建<br/>责任回到 Harness"]
F --> G["06 Timeline / Token / Reasoning<br/>决策、成本与敏感正文分治"]
```
六个阶段分别改变了审计系统回答问题的能力:
| 阶段 | 审计开始能够回答 |
|---|---|
| 01 可查询 Trace | 一次诊断大致执行过哪些 Agent Step 和 Tool |
| 02 语义加固 | Tool 成功、无证据、失败以及 Prompt/规则版本分别是什么 |
| 03 exact Run | 这些记录究竟属于同一 Session 中的哪一轮 |
| 04 Canonical Tool Truth | 当前发布校验依赖的完整事实,与长期审计元数据如何分开 |
| 05 Single ReAct 重建 | 新 Harness 中谁负责记录 Agent 和 Tool,审计失败如何处理 |
| 06 决策、成本与敏感正文 | 决策顺序、模型成本、Reasoning 可用性和治理缺口是什么 |
这不是从“简单”机械升级到“复杂”。每一步都在重新定义审计的边界。
## 2. 第一阶段:先让最终答案可以被回放
### 当时的问题
系统已经能执行诊断并返回结果,但演示者只能展示最终答案。面试官如果继续问“Agent 调了哪些 Tool、Verifier 做了什么、证据在哪里”,只能翻数据库或日志临时解释。
### 当时的选择
2026-07-03 的 MVP Trace 变更增加了独立只读 API:
```http
GET /api/diagnosis/{sessionId}/trace
```
它直接聚合已有持久化数据:
```mermaid
flowchart LR
API["Trace API"] --> SESSION[("diagnosis_session")]
API --> STEP[("agent_step")]
API --> TOOL[("tool_invocation")]
SESSION --> RESP["聚合 Trace Response"]
STEP --> RESP
TOOL --> RESP
```
这个阶段没有改变 Chat 主流程,也没有新增数据库表。目标很克制:先把已经存在的执行记录变成一个稳定、只读、可演示的查询入口。
### 没有选择什么
- 没有把 Trace 塞进 `/api/chat` 响应;
- 没有为了演示制造全离线假运行时;
- 没有先设计通用事件平台;
- 没有重写已有 Agent 和 Tool 持久化。
### 解决了什么
系统第一次可以从最终答案回到 Agent Step 和 Tool 记录,演示流程也形成了“Chat → Trace → Feedback”的闭环。
### 新暴露的问题
Trace 能查出来,不代表记录语义已经可靠:
- 不同 Tool 的持久化路径并不统一;
- `success`、无结果和失败的含义不稳定;
- Trace 仍以 `sessionId` 为唯一身份;
- 记录能展示,但还不能稳定支撑评测。
第一阶段解决的是“有没有回放入口”,不是“审计是否已经正确”。
## 3. 第二阶段:从“有记录”走向“有稳定语义”
### 当时的问题
评测系统准备使用 Tool Trace 计算证据质量时,发现不同 Tool 对状态的表达不一致:有的调用统一走 recorder,有的在本地 helper 中写表;无命中、失败和降级路径也可能被混成一个模糊结果。
与此同时,AIOps 还是一条独立入口。它能执行自动诊断,却没有与 Chat 相同的 Trace 故事。
### 当时的选择
2026-07-04 到 07-09 的一组变化没有急着增加新表,而是先收紧契约:
1. 统一 Tool recorder 和 evidence summary 语义;
2. 区分调用成功、没有证据和真正失败;
3. 覆盖 Verifier 缺失、非法输出和 degraded path;
4. 让 AIOps 复用已有 Trace 基础设施;
5. 用 compact `prompt_audit` 和 Gatekeeper rule-set version 记录决策版本,不保存完整 Prompt。
```mermaid
flowchart TB
TOOL["Tool 调用"] --> S1["执行是否成功"]
S1 --> S2["是否找到 Evidence"]
S2 --> S3["Verifier / Gatekeeper 如何判断"]
S3 --> S4["使用哪一版 Prompt / Rule"]
S4 --> EVAL["稳定 Trace 与确定性评测"]
```
### 没有选择什么
- 没有把完整 Prompt 存进 Trace;
- 没有使用 Prompt 内容 hash 作为频繁变化的版本协议;
- 没有引入 LLM-as-judge 代替确定性 fixture;
- 没有为 AIOps 另建一套 Trace 数据模型。
### 解决了什么
审计记录开始拥有可比较的语义。评测可以区分“Tool 正常但无证据”和“Tool 本身失败”,也能知道某次判断使用了哪版 Prompt 与规则。
AIOps 当时被定义为 Chat 的兄弟入口:不同触发方式,共享同一套持久化 Trace。这在当时是合理的复用选择。
### 新暴露的问题
两个入口共享 Trace,并没有解决最根本的身份问题。只要同一个 `sessionId` 连续执行多轮,所有 Step 和 Tool 仍会混到一起。
而且审计维度越丰富,跨轮污染的破坏越大:不仅回放错误,Feedback、Verifier、评分和案例沉淀都会读错对象。
## 4. 第三阶段:Session 不是一次执行,必须引入 exact Run
### 触发它的真实故障
2026-07-10 的 E2E 使用同一个 `sessionId` 连续请求两轮。Redis 多轮上下文表现正常,但 MySQL 出现了另一种现实:
```text
diagnosis_session
query / answer 被后一轮覆盖
agent_step
第一轮 + 第二轮持续追加
tool_invocation
第一轮 + 第二轮持续追加
```
主记录表达最新一轮,明细却表达多轮混合。Trace 不再代表某一次诊断,Feedback 也不知道评价的是哪一轮结果。
### 当时的选择
系统没有只在旧表上补一个轮次字段,而是拆分两种生命周期:
```mermaid
flowchart TB
S["chat_session<br/>多轮对话目录"] --> R1["diagnosis_run 1<br/>第一轮执行"]
S --> R2["diagnosis_run 2<br/>第二轮执行"]
R1 --> D1["step / tool by run_id"]
R2 --> D2["step / tool by run_id"]
```
核心决策包括:
- `sessionId` 表示多轮会话;
- `runId` 成为正式 API 字段,表示一次可回放执行;
- `agent_step` 和 `tool_invocation` 增加 `run_id`;
- Trace、Feedback、Evaluation 和案例来源优先绑定 Run;
- 指定 `runId` 时必须校验它属于 path 中的 `sessionId`。
### 没有选择什么
- 没有继续让 `diagnosis_session` 同时承担会话与执行;
- 没有尝试根据时间戳把历史混合 Trace 伪造成多个真实 Run;
- 没有在这个阶段引入 `diagnosis_trace_event`;
- 没有立即删除旧表和旧客户端兼容。
“当时没有引入 Timeline”很重要。这个阶段只解决身份隔离,复用 `agent_step` 与 `tool_invocation` 表达 Trace;typed Timeline 是后续才增长出来的能力。
### 解决了什么
两轮 E2E 得到同一个 Session 下两个不同 Run:
- run1 exact Trace 只返回 run1;
- run2 exact Trace 只返回 run2;
- step/tool mixed row check 为 0;
- 多轮对话上下文仍然连续。
审计终于拥有稳定的最小单位:**一次 Run 可以独立回放、评分、反馈和沉淀。**
### 新暴露的问题
Run 隔离修复了“记录属于谁”,但没有回答“Tool 的完整事实由谁保管”。旧 JPA ToolInvocation 更像长期 preview,无法成为 EvidenceGuard 的独立验真来源。
兼容策略也留下了债务:不传 `runId` 时读取 latest run、旧表保留、历史 mixed trace 只能映射为 compatibility run。
## 5. 第四阶段:长期 Trace 不能同时充当证据真理源
### 当时的问题
Single ReAct + Harness 设计要求 EvidenceGuard 独立验证 Agent 引用。如果 Guard 只能读取 Agent 已经看过的裁剪结果,等于让模型用自己的输入证明自己。
旧 `tool_invocation` 又不能直接升级为完整事实存储:长期保存 raw response 会扩大敏感数据、存储体积和保留治理范围。
### 当时的选择
2026-07-21 的 ToolBoundary 设计把一次 Tool 调用拆成不同用途的数据面:
```mermaid
flowchart TB
RAW["Tool Raw Result"] --> CAN["Canonical Invocation<br/>Redis 短期完整真相"]
CAN --> OBS["Agent Observation<br/>有界投影视图"]
CAN --> GUARD["EvidenceGuard<br/>独立验真"]
CAN -.-> DUR["Durable Audit<br/>MySQL 长期元数据"]
```
Canonical Invocation 保存当前 Run 所需的 request、raw response、Agent projection、状态和时间,并拥有 `PROJECTING / READY / ERROR` 生命周期。Agent 只能获得投影结果,不能访问 Redis 或完整 raw。
### 没有选择什么
- 没有让 JPA `tool_invocation` 保存全部 raw;
- 没有让 Agent 获取 Redis key 或 canonical record;
- 没有把无证据和调用失败合并;
- 没有让 raw 超限时静默截断后继续充当完整真相。
### 解决了什么
系统第一次明确区分:
- **当前 Run 的验真事实**:完整、短期、Harness-only;
- **Agent 的推理观察**:有界、稳定、可消费;
- **长期审计记录**:适合复盘,但不复制完整 raw。
这也奠定了后来的失败语义:canonical store 缺失时无法验真,需要 fail-closed;durable audit 写入失败不应改变 Tool observation,可以 fail-open。
### 新暴露的问题
新 ToolBoundary 有了 canonical store,却尚未接回长期 `tool_invocation` 审计。与此同时,旧 recorder 依赖 ThreadLocal 和旧 Tool 副作用,不能直接带进新 Harness。
换句话说,新架构已经有了“事实”,但一度失去了“长期记录”。
## 6. 第五阶段:Single ReAct 后,审计必须重新确定所有权
### 当时的问题
旧系统的审计依附于多 Agent、`@Tool`、ThreadLocal 和旧 `AgentLoggingHook`。Single ReAct 清理删除 Planner、Executor、Verifier、Composer 和独立 AIOps 入口后,继续保留这些记录路径会出现三个问题:
- 审计逻辑仍依赖已经退出生产链的结构;
- Tool backend 可能通过旧 annotation/recorder 产生重复副作用;
- Agent hook 可能长期保存正文和 Thought,违反新的数据边界。
### 当时的选择
2026-07-22 的收尾没有给旧 recorder 增加更多兼容分支,而是重建 Harness-native 审计:
| 旧方式 | 新方式 |
|---|---|
| 多 Agent 名称和旧 Hook 决定步骤语义 | `HarnessAgentAuditHook` 记录单体 Diagnosis Agent 步骤 |
| ThreadLocal 回退传递身份 | exact RunContext / run-scoped tracker |
| Tool 自身带 recorder 副作用 | ToolBoundary 统一触发 durable audit port |
| Agent 输入输出正文与 Thought 混入普通记录 | 普通 Step 以 roles、count、Tool names、bytes 等 metadata 为主 |
| JPA 审计与 canonical truth 概念混合 | Redis canonical fail-closed,JPA durable audit fail-open |
| Chat 与 AIOps 两条公开诊断入口 | 统一 `/api/chat` + Intent Router |
### 没有选择什么
- 没有把 `/api/ai_ops` 迁移成第二个 Harness use case;
- 没有保留旧 `@Tool` 与新 ACI 双注册;
- 没有让 audit DB 故障直接改变 Agent Observation;
- 没有为了兼容继续使用旧多 Agent 身份。
### 解决了什么
审计责任终于与当前执行架构一致:Agent Step 由 Agent Hook 负责,Tool durable audit 由 ToolBoundary 负责,最终 Run 由应用和 Release 收口。
这一步也说明演进不是只做加法。07-04 保留 AIOps 兄弟入口是当时的合理方案;07-22 删除它,则是“唯一入口、唯一 Harness”新目标下的必要替换。
### 新暴露的问题
metadata-only 解决了普通 Trace 的泄漏风险,却无法满足“为什么 Agent 选择这个 Tool”的深度审计需求。Run 虽然有 Step 和 Tool,也仍缺一条统一的决策 Timeline 和完整模型成本账本。
## 7. 第六阶段:从执行记录扩展到决策、成本与 Reasoning
### 新架构暴露的新问题
真实 E2E 出现过一条失败 Run:Diagnosis Agent 重复调用 `lookup_knowledge`,累计 12 次 Tool、45,087 Token 后才进入 `BUDGET_EXHAUSTED`;Evidence Repair 还出现过结构解析失败。
旧 Trace 可以看见许多 Step 和 Tool,却不容易直接回答:
- Agent 为什么继续查询,什么时候没有信息增益;
- Token 消耗来自 Router、Agent、Repair 还是 SemanticGuard;
- Evidence、Semantic 和 Release 决策按什么顺序发生;
- Provider 是否真的返回 Reasoning,还是系统只保存了 assistant text。
### 当前选择
2026-07-23 之后,审计继续分化为三个正交能力:
1. **typed Decision Timeline**:用 RUN、ROUTING、AGENT、TOOL、EVIDENCE、SEMANTIC、RELEASE 阶段记录决定顺序;
2. **Model Token Ledger**:按组件和轮次记录 usage,在 Run 结束执行总额对账;
3. **Reasoning Audit**:独立保存 Provider Reasoning 与 assistant text,普通 Trace 只暴露可用性和 bytes 等 metadata。
```mermaid
flowchart TB
RUN["exact Run"] --> TL["Decision Timeline<br/>决定如何发生"]
RUN --> STEP["Agent Step<br/>模型轮次"]
RUN --> TOOL["Tool Invocation<br/>外部行为"]
RUN --> TOKEN["Token Ledger<br/>组件成本"]
RUN --> RSN["Reasoning Audit<br/>敏感正文"]
TL --> TRACE["普通 Trace"]
STEP --> TRACE
TOOL --> TRACE
TOKEN --> TRACE
RSN -.-> RAPI["独立受限查询"]
```
### 没有选择什么
- 没有把 Reasoning 塞回普通 Trace 或 SSE;
- 没有在 Provider 不返回 Reasoning 时伪造思考过程;
- 没有只在 Run 结束写一个无法解释的 Token 总数;
- 没有把 Timeline 变成用于重建业务状态的完整 Event Sourcing。
### 解决了什么
当前 Trace 不仅能展示“发生过哪些调用”,还可以解释决定顺序、组件成本和发布依据。`tokens_reconciled` 能检查分项与总额,`usage_available=false` 能区分真实零消耗与供应商未返回 usage。
### 仍未完成什么
- Reasoning 访问控制、保留期限和加密尚未闭环;
- 非 DeepSeek Provider 和 reasoning unavailable live 样本仍需补充;
- `agent_step.thought` 仍存在兼容镜像语义;
- latest-run 与 legacy Trace fallback 仍然存在;
- durable audit fail-open 意味着业务成功仍可能伴随审计缺口。
演进到这里,系统不再把“记录越多”当作审计完成,而开始治理哪些记录应该存在、谁能读取、缺失时如何表达。
## 8. 哪些设计被保留,哪些被替换
| 早期设计 | 当前结果 | 判断 |
|---|---|---|
| Trace 与 Chat 响应分离 | 保留独立只读 Trace API | 核心边界正确,持续保留 |
| 聚合持久化 Step/Tool | 保留,并增加 Run、Timeline 和对账 | 基础能力被扩展 |
| Session 作为 Trace 身份 | 改为 exact Run,Session 只作目录 | 被真实串线问题替换 |
| AIOps 作为兄弟入口 | 删除,统一 `/api/chat` | 阶段性合理,后续架构收敛后退出 |
| ToolInvocation 同时承担事实与审计 | 拆为 canonical truth 与 durable audit | 责任过载,被数据分层替换 |
| 旧 ThreadLocal/Tool 副作用 recorder | 改为 Harness-native Hook、Tracker 和 Audit Port | 与当前执行架构重新对齐 |
| 普通 Step 保存 Thought/正文 | 转向 metadata + 独立 Reasoning | 安全边界收紧,但兼容镜像尚未完全清除 |
| 一个 Token 总数 | 组件轮次账本 + Run 对账 | 从统计值升级为可解释成本 |
演进中真正稳定下来的不是某张表,而是三条原则:执行身份要精确、数据责任要分层、缺失和失败要诚实可见。
## 9. 为什么不能一开始就设计成现在这样
站在今天回看,很容易认为最初就应该有 Run、Timeline、Reasoning 和 canonical store。但当时缺少三个关键事实:
1. 没有多轮 E2E,就无法证明 Session 身份会造成怎样的跨轮污染;
2. 没有 EvidenceGuard,就无法看清长期 Tool audit 与当前验真 truth 的责任冲突;
3. 没有真实 Token 和 Reasoning 样本,就无法确定哪些字段应该对账、哪些正文必须隔离。
过早一次性设计完整审计平台,很可能得到通用事件总线、万能 JSON 和全量 raw 存储,却没有解决 MVP 当时最重要的问题。
这套演进采用的是另一种方式:先围绕可观察故障收紧最小契约,再让新的真实运行暴露下一层边界。
## 10. 用一张图记住设计为什么变成现在这样
```mermaid
flowchart TB
P1["答案无法解释"] --> D1["独立 Trace API"]
P2["Tool 状态语义不一致"] --> D2["Evidence / Quality 契约"]
P3["同一 Session 多轮串线"] --> D3["exact Run"]
P4["长期审计不能独立验真"] --> D4["Canonical Truth / Durable Audit 分层"]
P5["旧审计依赖旧多 Agent 与 ThreadLocal"] --> D5["Harness-native Audit"]
P6["决策顺序、成本和 Reasoning 不可解释"] --> D6["Timeline / Token / Reasoning 分治"]
D1 --> NOW["当前可回放、可对账、有数据边界的审计系统"]
D2 --> NOW
D3 --> NOW
D4 --> NOW
D5 --> NOW
D6 --> NOW
```
一句话概括:
> 审计系统最初只是把已有 Session 记录聚合出来;真实 E2E 先迫使它建立稳定 Evidence 语义,再用 exact Run 修复多轮身份,随后 Single ReAct 重构又把 Tool 真理源与长期审计分开,最终才发展出统一 Timeline、Token 对账和独立 Reasoning 治理。
## 11. 事实来源与延伸阅读
- `devflow/projects/2026-07-03-mvp-demo-trace-acceptance/`:独立只读 Trace API 的起点;
- `devflow/projects/2026-07-04-evidence-trace-hardening/`:Evidence 状态和 degraded path 契约加固;
- `devflow/projects/2026-07-04-aiops-traceable-diagnosis-entry/`:早期 AIOps 兄弟入口与共享 Trace;
- `devflow/projects/2026-07-09-interview-demo-quality-audit/`:Prompt/Rule version 与确定性质量审计;
- `devflow/projects/2026-07-10-session-run-trace-isolation/`:多轮串线、exact Run 决策和两轮 E2E;
- `devflow/projects/2026-07-21-single-react-tool-invocation-store/`:canonical Tool truth 与 durable audit 分层;
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/`:Single ReAct 审计所有权重建;
- [ISS-015 诊断运行质量与 Reasoning 审计收敛](../../issues/active/ISS-015-diagnosis-runtime-quality-and-reasoning-audit.md):Timeline、Token、Reasoning 和当前缺口;
- [审计主设计](审计系统设计-从调试日志到可回放的决策证据.md):当前设计不变量与关键取舍;
- [真实 Run 案例](从一次诊断Run看审计系统如何记录决策.md):当前审计能力的具体回放方式。
@@ -0,0 +1,315 @@
# Session、Run、Trace 隔离:一次身份建模错误的修复
**更新日期**:2026-07-29
**性质**:MVP 工程问题与架构决策复盘
**结论状态**:Session / Run 身份模型沿用至当前架构
---
## 1. 问题到底是什么
一句话定义:**系统用同一个 `sessionId`,同时标识“多轮对话”和“一次诊断执行”,导致一次 Trace 不再对应一次真实执行。**
问题由一个两轮 E2E 暴露。用户在同一会话中连续发起两次请求:
```text
Round 1:诊断支付接口超时
Round 2:基于上一轮结论,列出还缺哪些证据
```
两轮复用 `sessionId` 是正确的,因为第二轮需要上一轮上下文。但当时持久化和 Trace 查询也只使用 `sessionId`:
- `diagnosis_session` 是覆盖写,第二轮把第一轮的 query、answer、status 覆盖掉;
- `agent_step` 和 `tool_invocation` 是追加写,两轮明细累积在同一个 `sessionId` 下;
- Trace API 再按 `sessionId` 聚合主表和明细。
结果不是简单的“重复数据”,而是一个系统中从未真实发生过的混合执行:
```mermaid
flowchart LR
R1["Round 1<br/>query A / steps A / tools A"] --> SID["同一个 sessionId"]
R2["Round 2<br/>query B / steps B / tools B"] --> SID
SID --> Main["diagnosis_session<br/>只剩 query B / answer B"]
SID --> Steps["agent_step<br/>steps A + steps B"]
SID --> Tools["tool_invocation<br/>tools A + tools B"]
Main --> Trace["混合 Trace"]
Steps --> Trace
Tools --> Trace
Trace --> Error["无法回答:<br/>哪组证据支持了哪次回答?"]
```
这个错误会沿数据链继续放大:
| 消费方 | 错误结果 |
|---|---|
| Trace 回放 | 一条 Trace 混合两轮步骤和 Tool |
| Verifier / Gatekeeper | 当前轮可能读到上一轮证据 |
| Evaluation | 评分对象和证据集合不再属于同一次执行 |
| Feedback | 无法确定用户评价的是哪一轮回答 |
| CaseLibrary | 可能把反馈沉淀到错误的 query/answer 上 |
因此,核心问题不是某个 Repository 的更新方式,而是**会话边界被错误地当成了证据与审计边界**。
---
## 2. 设计必须守住什么
修复前先定义四条不变量。后续方案不是凭表结构偏好选择,而是看能否同时满足这些约束。
### 不变量 1:一次执行只有一个稳定身份
从请求被接受到最终 `SUCCESS / FALLBACK / FAILED / CANCELLED`,必须有一个不可变 ID。模型步骤、Tool 调用、预算、最终答案和反馈都属于它。
### 不变量 2:一条 Trace 只描述一次执行
Trace 中的 Run、AgentStep、ToolInvocation 和生命周期事件必须使用同一个 exact ID 聚合,不能依赖时间邻近或“最新一条”猜测归属。
### 不变量 3:上下文连续不等于执行合并
同一个 Session 可以包含多个 Run。后一个 Run 可以读取前一轮安全发布结果,但不能继承前一轮的 Tool、Trace、评分或失败状态。
### 不变量 4:兼容不能伪造精度
旧客户端可以有迁移期 fallback,旧数据也可以保留;但系统必须让歧义可观察,不能把无法恢复的历史混合数据伪装成精确多轮记录。
这四条不变量共同导出一个结论:系统需要两个身份,而不是给 `sessionId` 增加更多解释。
---
## 3. 为什么不是在旧表上继续修
设计阶段考虑的不是“拆表还是不拆表”这一个问题,而是如何建立稳定的执行边界。
| 候选方案 | 能解决什么 | 为什么没有选择 |
|---|---|---|
| 继续复用 `sessionId`,修正覆盖逻辑 | 避免主表被覆盖 | 明细仍无法区分轮次,根因未解决 |
| 增加 `round_no` | 可以表示第几轮 | 并发请求、重试和多个执行入口下顺序不稳定;外部引用仍需复合身份 |
| 按时间窗口拆分历史 Step/Tool | 无需改协议 | 时间不能证明归属,会制造看似精确的错误 Trace |
| 每次请求插入一条新的 `diagnosis_session` | 形成一行一次执行 | 实际上已经引入 Run 概念,但名称和 Session 生命周期仍混淆,也缺少会话主实体 |
| 拆分 `chat_session` 与 `diagnosis_run` | 显式表达一对多生命周期 | 需要 Schema、API 和客户端迁移,但能满足全部不变量 |
最终选择最后一种。它的判断依据不是“范式更规范”,而是只有它能让对话连续性和执行可审计性同时成立。
---
## 4. 核心设计
目标模型是一个清晰的一对多关系:
```mermaid
flowchart TB
Client["Client"] --> Session["chat_session<br/>sessionId:多轮对话目录"]
Session --> Run1["diagnosis_run<br/>runId 1:一次执行"]
Session --> Run2["diagnosis_run<br/>runId 2:另一次执行"]
Run2 --> Step["agent_step<br/>模型步骤 metadata"]
Run2 --> Tool["tool_invocation<br/>Tool 审计 metadata"]
Run2 --> Event["diagnosis_trace_event<br/>生命周期 Timeline"]
Run2 --> Reasoning["agent_reasoning_audit<br/>受限原文"]
Run2 --> Trace["普通 Trace API"]
Step --> Trace
Tool --> Trace
Event --> Trace
Reasoning --> Audit["独立 Reasoning API"]
```
这里有三个不同的职责:
- `chat_session` 回答“哪些 Run 属于同一段对话”;
- `diagnosis_run` 回答“这次请求的输入、状态、结果和资源消耗是什么”;
- Trace 回答“这个 Run 具体经历了什么”,它是按 Run 聚合的读模型。
---
## 5. 六项关键决策
### 决策 1:引入正式的 `runId`
**选择**:每次有效 Chat 执行创建新的 `runId`,并通过 SSE metadata 返回 `sessionId + runId`。
**理由**:Run 必须能被 API、数据库、日志、Feedback 和评测独立引用。数据库自增 ID 不适合作为外部协议;轮次编号又不能稳定处理并发和重试。
**代价**:客户端必须保存并向后续 Trace/Feedback 请求传递 `runId`。
**边界**:`runId` 是不透明标识。早期实现使用 `run-` + UUID,后续格式发生过演进,客户端不得解析其前缀或长度。
### 决策 2:拆分会话态与运行态
**选择**:新增 `chat_session` 和 `diagnosis_run`,旧 `diagnosis_session` 停止承载新的运行写入。
**理由**:两者生命周期不同。
| 对象 | 保存内容 | 更新特点 |
|---|---|---|
| `chat_session` | 会话状态、轮次数、最近活动时间等目录信息 | 跨多轮持续更新 |
| `diagnosis_run` | 单次 query、answer、终态、intent、release outcome、预算 | 一次执行内从 RUNNING 走向唯一终态 |
完整多轮正文没有因为拆表就复制到 MySQL。身份拆分解决的是审计归属,不应顺带扩大长期数据保存范围。
### 决策 3:Trace 是 Run 的聚合视图,不另造身份
**选择**:第一阶段复用 `agent_step` 和 `tool_invocation`,增加 `run_id`;不为了修复隔离问题再创建一个独立 `traceId`。
**理由**:隔离所缺的是执行外键,不是第三套身份。引入 `traceId` 只会产生 `sessionId / runId / traceId` 的映射问题。
后续单 Agent + Harness 重构新增 `diagnosis_trace_event`,用于表达统一生命周期 Timeline。这是 Run 下的新明细,不是新的聚合根,也没有改变 `runId` 的边界。
### 决策 4:exact Run 是目标协议,latest Run 只是迁移桥梁
**选择**:目标查询使用:
```http
GET /api/diagnosis/{sessionId}/trace?runId={runId}
```
服务端同时验证 Run 存在且属于 path 中的 Session,防止跨 Session 串读。
旧客户端暂时只传 `sessionId` 时,可以解析 latest Run;但这是显式兼容路径,不是新的业务语义。如果必须计算 latest,应按:
```text
created_at DESC, id DESC
```
而不是 `updated_at`。旧 Run 可能因 Feedback 或异步处理再次更新,最近修改不等于最近执行。
### 决策 5:所有下游语义绑定 Run
**选择**:Trace、Feedback、Evaluation、Tool evidence 和新 Case provenance 都以 `runId` 为执行边界。
**理由**:这些对象评价或引用的是一次回答,不是整段会话。
Feedback 在迁移期缺少 `runId` 时可以绑定 latest Run,但响应必须暴露 `fallbackToLatestRun=true`。兼容如果不可观察,就会从临时措施变成永久歧义。
`case_library.diagnosis_id` 因复用旧列,在过渡期存在历史 `session_id` 和新 `run_id` 两种语义。这是明确接受的迁移成本,而不是应被隐藏的数据一致性。
### 决策 6:历史混合数据不做推测性拆分
**选择**:每条旧 `diagnosis_session` 最多映射为一个 compatibility Run,不根据时间或 Agent 名称猜测真实轮次。
**理由**:旧数据没有记录边界,任何自动拆分都只能产生无法证明的归属。审计系统宁可明确“不知道”,也不能制造虚假的精确回放。
---
## 6. 协议和影响范围
这是一次有意的行为与协议变化,不是纯内部重构。
| 范围 | 变化 | 受影响方 |
|---|---|---|
| Chat / SSE | metadata 增加 `runId` | 前端、脚本、调用方 |
| Trace API | 支持 exact `runId` 查询 | Trace UI、排障工具、评测 |
| Run API | 提供 Session 下的 Run 列表 | 多轮历史浏览 |
| Feedback | request/response 增加 Run 绑定和 fallback 标志 | 前端、CaseLibrary |
| 数据库 | 新增两张主表,明细增加 `run_id` | 持久化、迁移、查询脚本 |
| 其它入口 | 当时的 AIOps 同步采用 Run 边界 | SSE 消费方、Trace |
之所以把当时的 AIOps 一并迁移,是因为它同样会产生可回放执行;只修 Chat 会留下第二条具有同类缺陷的数据链。后续 ISS-014 删除了旧 AIOps 双入口,但这不改变当时“所有执行入口必须共享 Run 边界”的设计判断。
---
## 7. 风险如何处理
### 风险 1:兼容路径继续产生歧义
控制方式是让 fallback 可观察,并把 exact `runId` 定义为目标协议。兼容是迁移机制,不能反向成为领域模型。
### 风险 2:异步链路丢失或串用身份
`sessionId` 与 `runId` 必须作为同一执行上下文传播。当前架构将二者放入显式 `RunContext`,ToolBoundary、审计 Hook 和持久化都校验当前 Run,避免只依赖线程隐式状态。
### 风险 3:历史和新 provenance 共用旧列
保留旧列降低了迁移破坏性,但查询和文档必须承认双语义,不能把旧 `session_id` 当作非法 `run_id` 清理。
### 风险 4:旧混合 Trace 永远无法恢复
这是明确接受的事实。系统保留 compatibility 访问和回滚能力,但不承诺不存在的历史精度。
---
## 8. 如何证明设计成立
验收问题不是“接口里有没有 `runId`”,而是下面五个条件是否同时成立:
```text
同一 Session 连续执行两轮
+ 两轮获得不同 runId
+ 每个 exact Trace 只返回本 Run 明细
+ 数据库不存在跨 Run 混合行
+ 第二轮仍能使用会话上下文
```
真实 E2E 使用:
```text
sessionId = e2e-phase6-chat-codex-20260710-2120
run1 = run-e2a97696-4398-4abc-90e4-28f45c838f92
run2 = run-76ce6a6e-92ab-40c9-800a-eca0c1bb5172
```
结果:
- 两轮 Chat 成功并复用同一个 `sessionId`;
- 两轮返回不同 `runId`;
- run1 exact Trace 只返回 run1,run2 exact Trace 只返回 run2;
- `diagnosis_run` 中存在两条独立运行记录;
- AgentStep:run1 为 10 行,run2 为 9 行;
- ToolInvocation:run1 为 14 行,run2 为 8 行;
- mixed row check 为 0;
- `chat_session.message_pair_count = 2`,上下文连续性没有因隔离而丢失;
- focused tests、baseline diff、日志和数据库核验通过,未观察到 baseline drift。
这组证据同时验证了“该分开的确实分开”和“该连续的仍然连续”。
---
## 9. 后续演进验证了什么
系统后来从多角色 Agent 编排重构为单 Diagnosis ReAct Agent + Harness。执行结构发生了大变化,但 Session / Run 模型没有被替换,反而成为新架构的基础:
- `ChatApplicationUseCase` 创建和结束 Run;
- `RunContext` 显式携带 `sessionId + runId`;
- ToolBoundary 校验 Tool 请求属于当前 Run;
- Redis canonical invocation 使用 `runId + toolCallId` 定位;
- EvidenceGuard 只接受当前 Run 的 READY invocation;
- AgentStep、ToolInvocation、TraceEvent 和 ReasoningAudit 都绑定 Run。
这说明当时解决的不是某一版代码的局部 bug,而是找到了稳定的领域边界。Agent 编排可以替换,Session 与 Run 的生命周期差异不会消失。
---
## 10. 可复用的设计判断
这次问题可以归纳为四条通用经验:
1. **生命周期不同的对象,不应共享同一个聚合身份。**
2. **上下文复用不代表证据、状态和审计记录也可以复用。**
3. **兼容 fallback 必须可观察、可退出,不能静默猜测。**
4. **无法恢复的历史边界应明确降级,不能伪造精确性。**
判断类似系统是否存在同类问题,可以直接问:
- 一次请求是否有独立于 Session 的执行 ID?
- 所有 Step、Tool、Event、Feedback 是否都能精确归属一次执行?
- “查询最新”是否被误当成“查询指定执行”?
- 主表覆盖写、明细追加写是否使用了同一个过宽的关联键?
- 历史迁移是在保留不确定性,还是通过猜测制造精确性?
如果这些问题没有明确答案,那么 Trace 即使看起来完整,也未必能作为可信审计证据。
---
## 11. 资料索引
- [ISS-010:同 session 多轮诊断 Trace 隔离](../../issues/archived/ISS-010-session-run-trace-isolation.md)
- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md)
- [当前 MVP 架构](../../architecture/current-mvp-architecture.md)
- [数据表索引](../../tables/README.md)
- [devflow brief](../../../devflow/projects/2026-07-10-session-run-trace-isolation/brief.md)
- [devflow decisions](../../../devflow/projects/2026-07-10-session-run-trace-isolation/decisions.md)
- [devflow acceptance](../../../devflow/projects/2026-07-10-session-run-trace-isolation/acceptance.md)
- [devflow evidence](../../../devflow/projects/2026-07-10-session-run-trace-isolation/evidence.md)
File diff suppressed because it is too large Load Diff
+378
View File
@@ -0,0 +1,378 @@
# Harness Context:统一术语与命名边界
**更新日期**:2026-07-29
**状态**:当前实现口径
**适用范围**:`com.superbiz.agent.harness`、Chat SSE、Diagnosis Run 持久化与相关工程文档
> 本文是术语词典,不建议第一次接触 Harness 时顺序阅读。入门请从 [README.md](README.md) 开始,遇到名词歧义时再回到本文查询。
## 1. 为什么需要这份 Context
当前系统同时存在 Run 状态、发布结果、Tool 状态、证据状态、收集状态、停止原因和 Fallback 原因。它们都使用了 `SUCCESS`、`ERROR`、`FAILED`、`READY` 等相近词汇,但回答的是不同问题。
如果把这些词排成一条“大状态机”,会产生错误理解,例如:
- `NO_EVIDENCE` 被理解为 Tool 调用失败;
- `FALLBACK` 被理解为 Run 执行失败;
- `SATURATED` 被理解为预算耗尽;
- `READY` 被理解为证据足以支持根因;
- 数据库 `status=SUCCESS` 被理解为已经找到根因。
本文件是 Harness 工程文档的术语入口。阅读其他文章前,先以这里的定义区分身份、数据、状态和组件责任。代码与现行架构文档仍是最终事实来源;本文件不创建新的运行协议。
## 2. 一句话定义 Harness
**Harness** 是包围非确定性模型执行的确定性控制边界:Agent 负责业务推理和 Draft,Harness 负责 Run 身份、生命周期、预算、取消、Tool 门禁、证据验真、停止控制、发布和审计。
Harness 不是:
- 业务工作流引擎;
- Planner / Executor / Verifier / Composer 编排图;
- 框架 ReAct loop 的第二份实现;
- 判断业务根因的规则引擎;
- 用于存放所有 Agent 相关代码的泛化名称。
## 3. 三层范围:不要把 Harness、Core 和 Application 当成同义词
| 名称 | 定义 | 包含 | 不包含 |
|---|---|---|---|
| Chat Application | 一次 Chat 请求的应用用例 | Session/Run 创建、路由、分支执行、持久化、公开结果 | HTTP/SSE 连接本身、业务推理细节 |
| Diagnosis Harness | Diagnosis Agent 外部的确定性控制系统 | Core、Interceptor、ToolBoundary、Progress、Guard、Release、Audit | 根因推理和 ReAct 规划 |
| Harness Core | 最小运行控制内核 | RunContext、deadline、budget、cancel、lifecycle、retry policy | 路由、Tool backend、Guard、持久化、SSE |
代码包 `com.superbiz.agent.harness` 同时包含 Chat Application 和 Diagnosis Harness 的实现,这是代码组织范围,不代表所有类都属于 Harness Core。
`DiagnosisHarnessCore` 目前也被 System Chat、Knowledge Query 和 Router 复用预算与生命周期能力。类名前缀保留了演进历史,概念上应理解为当前 Chat Run 的 Harness Core。
## 4. 身份术语
```mermaid
flowchart TB
S["Chat Session<br/>sessionId,多轮容器"] -->|"1:N"| R1["Run<br/>runId,一次请求"]
S -->|"1:N"| R2["Run<br/>下一次请求"]
R1 -->|"1:N"| AS["Agent Step<br/>一次模型步骤"]
R1 -->|"1:N"| TC["Tool Call / Invocation<br/>framework tool_call_id"]
R1 -->|"1:N"| TE["Trace Event<br/>sequence_no"]
TC --> CI["Canonical Invocation<br/>Run 内短期 Tool 真相"]
AS --> TR["Diagnosis Trace<br/>按 exact Run 聚合"]
TC --> TR
TE --> TR
R1 -.->|"SUCCESS diagnosis only"| PT["PublishedResult<br/>可投影为下一轮 PreviousTurn"]
```
图中的所有明细都必须绑定 exact runId。Session 只负责组织多轮,不能替代 Run 归属;Trace 是聚合视图,不能反过来成为执行身份。
### 4.1 Chat Session
一次多轮对话容器,由 `sessionId` 标识。同一个 Session 可以包含多次 Run。
Session 用于:
- 组织多轮请求;
- 查找安全的 PreviousTurn;
- 查询历史 Run。
Session 不代表一次执行,不拥有 Tool Call 或模型步骤的终态。
### 4.2 Run
一次独立的 Chat Application 执行,由 `runId` 标识。每次请求创建新 Run,无论 intent 是 System Chat、Knowledge Query 还是 Diagnosis。
代码和表名中仍大量使用 `DiagnosisRun` / `diagnosis_run`,但当前 Chat Application 会为三种 intent 都创建该记录。因此文档中优先使用 **Run**;只有引用 Java 实体或数据库表时才写 `DiagnosisRun`。
### 4.3 Agent Step
Diagnosis ReAct Agent 的一次模型步骤。多个 Agent Step 属于一个 Run,用于记录每轮模型调用的有界元数据和 Token。
Agent Step 不是 Run,也不是 retry attempt。ReAct 的下一轮模型调用是业务循环;retry 是同一技术操作的再次 attempt。
### 4.4 Tool Call / Tool Invocation
- **Tool Call**:模型通过框架产生的调用请求,由 framework `tool_call_id` 标识。
- **Tool Invocation**:该请求进入 ToolBoundary 后的一次实际执行记录。
- **Tool Request Rejection**:在 progress、重复或停止门禁处被拒绝,backend 没有执行,不算 Tool Invocation。
Harness 不生成第二套 Tool Call ID。
### 4.5 Trace
按 exact `sessionId + runId` 聚合的可回放观察视图,包含 Run、Agent Step、Tool metadata 和统一 Timeline。
Trace 是观察结果,不是新的执行上下文或状态所有者。`TraceEventStatus` 只描述单个事件,不能替代 RunState 或 ReleaseOutcome。
## 5. 核心执行术语
### 5.1 RunContext
一次 Run 的显式执行上下文。结构不可变地携带:
```text
sessionId
runId
deadline
RunCancellation
RunBudget
ModelCallLedger
HarnessRetryPolicies
RunLifecycle
DiagnosisProgressTracker
```
“结构不可变”表示 record 字段引用不变化;预算、取消、生命周期和进展通过各自线程安全句柄在 Run 内变化。
### 5.2 Run Lifecycle
Run 的内存执行终态,由 `RunLifecycle` 所有,状态类型是 `RunState`。它采用 first-terminal-wins,后到达的成功、失败或取消不能覆盖第一个终态。
### 5.3 Run Cancellation
请求停止 Run 的协作机制,由 `RunCancellationReason` 记录第一个原因。取消会阻止后续边界和迟到发布,但不承诺一定能立即物理中断已发送给 Provider 的同步请求。
### 5.4 Run Budget
单 Run 的资源账本和门禁,包括模型调用、Tool 调用、单 Tool 次数、输入/输出/总 Token 和 Run bytes。
Budget 只回答“还能不能消耗资源”,不回答“继续诊断是否有价值”。后者属于 Information Gain 和 Collection State。
### 5.5 Retry Attempt
同一个技术操作因允许的技术失败而再次执行。当前 Router 和 SemanticGuard 最多 2 次 attempt,Diagnosis Agent、Tool 和 EvidenceRepair 只有 1 次。
以下不是 retry:
- ReAct Agent 的下一轮思考;
- 改用另一个 Tool;
- `NO_EVIDENCE` 后继续查询;
- 用户发起下一次 Run。
## 6. Agent 与输出术语
### 6.1 Diagnosis Agent
唯一拥有业务 ReAct Tool loop 的 Agent,负责提出假设、选择 Tool、评价非空结果的信息增益并生成 `DiagnosisDraft`。
它不负责 Run 生命周期、Tool 授权、证据物理验真、SemanticGuard 或最终发布。
### 6.2 DiagnosisDraft
Diagnosis Agent 的结构化草稿,是发布链输入,不是已经发布的报告。Draft 可以有结论,也可以 `conclusion=null`。
Draft 中的 Tool 引用和结论必须经过 Release Pipeline 后才能成为公开内容。
### 6.3 Agent Result
当前 Tool 代码中的 `agent_result` 指 Tool-specific Projector 生成、存入 canonical invocation 的标准化有界结果。
它不是:
- Diagnosis Agent 的最终 Draft;
- Chat Application 的最终结果;
- 直接进入模型上下文的完整内容。
更准确的理解是 **Canonical Projected Tool Result**。当前字段名因协议兼容保留。
### 6.4 Model Observation
从 canonical `agent_result` 再次白名单投影后,真正作为 Tool Response 进入 Diagnosis Agent 上下文的内容。
预算、阈值、重复指纹、raw response 和完整 Harness 控制状态不进入 Model Observation。
### 6.5 PreviousTurn 与 PublishedResult
- `PublishedResult`:只有 `DIAGNOSIS + ReleaseOutcome.SUCCESS` 才能持久化的安全诊断结果。
- `PreviousTurn`:从 PublishedResult 生成的有界下一轮上下文。
Fallback、失败、取消、raw evidence 和完整历史不会进入 PreviousTurn。
`PublishedResult` 不等于当前请求直接返回的 `ChatApplicationResult`。
## 7. Tool 与证据术语
### 7.1 ToolBoundary
所有业务 Tool 的统一执行边界,负责 Run/ID/授权/只读校验、预算、bytes、canonical 状态迁移和 metadata audit。
ToolBoundary 不判断信息增益或业务根因。
### 7.2 Canonical Invocation
Redis TTL 内的完整 Tool 调用真相,包含 request、raw response、标准化 `agent_result`、调用状态和证据状态。
它用于当前 Run 的 EvidenceGuard 和 ProgressSnapshot,不是长期审计记录。
### 7.3 Durable Audit
长期保存的有界元数据:Run/Tool identity、状态、耗时、bytes、模型步骤和 Token。它不保存 Prompt、完整 Tool 参数、raw response 或 canonical record。
### 7.4 Evidence
Tool 在特定 scope 下返回、经过 Projector 标准化并能由当前 Run canonical record 验真的事实或负向观察。
`EVIDENCE_FOUND` 只代表存在候选内容,不代表它支持根因。
### 7.5 Negative Observation
`READY + NO_EVIDENCE` 形成的限定范围事实,例如“在时间窗 T、服务 S、查询 Q 下没有匹配日志”。
它不能被解释为“故障不存在”或“系统健康”。Draft 中使用 `AnalysisKind.NEGATIVE_OBSERVATION` 表达这种分析。
### 7.6 VerifiedEvidenceSnapshot
EvidenceGuard 从 canonical invocation 中投影出的已验真、最小证据集合,供 SemanticGuard 使用。它不包含 raw response。
### 7.7 ProgressSnapshot
Tool loop 结束后,根据已完成 Tool Call identity 回读 canonical records 生成的有界过程视图,用于受控停止或无结论 Fallback。
VerifiedEvidenceSnapshot 面向“有结论报告的语义审查”;ProgressSnapshot 面向“没有可发布结论时说明已经检查了什么”,两者用途不同。
## 8. Guard 与发布术语
### 8.1 EvidenceGuard
确定性引用验真器。检查 Draft 结构、analysis 引用闭包、当前 Run 所有权、READY 状态、EvidenceStatus 和 typed projection 自洽性。
不调用模型,不判断结论是否被证据支持。
### 8.2 EvidenceRepair
一次性的模型修复步骤,只修结构和引用。修复前后 `SemanticDraftView` 必须保持用户可见语义一致,之后重新执行 EvidenceGuard。
它不是新的报告作者,也不是 retry Agent。
### 8.3 SemanticGuard
隔离的单轮语义审查器,只判断 verified evidence 是否支持 Draft,输出 `SUPPORTED / UNSUPPORTED`。
它使用模型,但无 Tool、无记忆、无 ReAct loop、无报告改写权,因此文档中不要称它为第二个业务 Agent。
### 8.4 Release Pipeline
从 DiagnosisDraft 或受控停止输入,到 `DiagnosisReleaseResult` 的安全决策链:EvidenceGuard、可选 Repair/Recheck、SemanticGuard 和 SafeFallback。
### 8.5 ReleaseOutcome
应用层最终处理结果:
- `SUCCESS`:发布安全正常内容;
- `FALLBACK`:请求已被安全处理,但没有发布正常诊断结论;
- `FAILED`:无法形成安全业务结果;
- `CANCELLED`:Run 被取消。
ReleaseOutcome 不等于 RunState。尤其 `FALLBACK` 不是 RunState。
### 8.6 SafeFallback 与 FallbackType
SafeFallback 是确定性公开内容;FallbackType 解释为什么没有发布正常诊断结论,例如:
- `EVIDENCE_VALIDATION_FAILED`;
- `SEMANTIC_UNSUPPORTED`;
- `SEMANTIC_UNAVAILABLE`;
- `INSUFFICIENT_EVIDENCE`;
- `MISSING_REQUIRED_CONTEXT`。
FallbackType 是 `ReleaseOutcome.FALLBACK` 的原因,不是新的生命周期状态。
## 9. 状态维度速查
| 类型 | 所有者 | 回答的问题 | 不能回答的问题 |
|---|---|---|---|
| `RunState` | RunLifecycle | Run 的内存执行是否终止、如何终止 | 发布了正常内容还是 Fallback |
| `RunCancellationReason` | RunCancellation | 谁首先请求取消、为什么 | 最终公开结果是什么 |
| `ChatApplicationStatus` | Application Observer | 当前向用户展示哪个处理阶段 | Run 是否已经终止 |
| `InvocationStatus` | Canonical Invocation | Tool 调用记录是否完成 | 是否找到候选证据 |
| `EvidenceStatus` | Tool Projector | 当前 scope 是否有候选证据 | 是否支持根因 |
| `InformationGain` | Harness/Diagnosis Agent | 结果是否推进当前诊断 | Tool 是否技术成功 |
| `DiagnosisCollectionState` | ProgressTracker | 是否允许继续调用证据 Tool | Run 是否终止 |
| `DiagnosisStopReason` | ProgressTracker | 为什么停止继续收集 | 对外发布什么内容 |
| `SemanticVerdict` | SemanticGuard | 已验真证据是否支持 Draft | Run 是否成功执行 |
| `ReleaseOutcome` | Application/Release | 对外处理结果属于成功、降级、失败还是取消 | Tool 或收集过程的内部状态 |
| `FallbackType` | SafeFallbackFactory | FALLBACK 的业务/安全原因 | 整个 Run 的执行终态 |
| `TraceEventStatus` | 单个 Trace Event | 某条事件的局部结果 | 全局生命周期 |
| SSE Session `State` | ChatSseSession | 连接能否继续发送事件 | Harness Run 的业务结果 |
```mermaid
flowchart LR
subgraph Execution["执行控制维度"]
RS["RunState<br/>RUNNING -> terminal"]
CS["CollectionState<br/>COLLECTING / SATURATED"]
SR["StopReason<br/>停止收集原因"]
end
subgraph Tool["单次 Tool 维度"]
IS["InvocationStatus<br/>PROJECTING / READY / ERROR"]
ES["EvidenceStatus<br/>FOUND / NO_EVIDENCE / ERROR"]
IG["InformationGain<br/>GAINED / NO_GAIN"]
end
subgraph Validation["验证维度"]
EV["EvidenceGuardResult<br/>valid / violations"]
SV["SemanticVerdict<br/>SUPPORTED / UNSUPPORTED"]
end
subgraph Publication["发布与观察维度"]
RO["ReleaseOutcome<br/>SUCCESS / FALLBACK / FAILED / CANCELLED"]
FT["FallbackType<br/>FALLBACK 原因"]
SSE["SSE Session State<br/>连接发送状态"]
DB["diagnosis_run.status<br/>持久化通用状态"]
end
IS --> ES
ES --> IG --> CS
CS --> SR
ES --> EV --> SV
SR --> RO
SV --> RO
RS --> RO
RO --> FT
RO --> SSE
RO --> DB
```
箭头表示信息参与后续决策,不表示枚举之间一一转换。例如 `READY + EVIDENCE_FOUND` 仍可能得到 `NO_GAIN`,`RunState.SUCCESS` 也可能对应 `ReleaseOutcome.FALLBACK`。
完整状态转换和跨层映射见[Harness生命周期与状态.md](Harness生命周期与状态.md)。
## 10. 命名规则
后续代码和文档遵守以下用词:
1. 说“Run 成功/失败/取消/超时/预算耗尽”时,明确写 `RunState`。
2. 说“发布正常内容/Fallback/失败/取消”时,明确写 `ReleaseOutcome`。
3. 不单独写“Tool 成功”,改写为 `InvocationStatus=READY`,并同时说明 EvidenceStatus。
4. 不写“找到有效证据”,除非已经说明是候选内容、已验真事实还是足以支持结论。
5. `NO_EVIDENCE` 必须附带 scope,不得写成全局否定。
6. `SATURATED` 只用于 Collection State;预算耗尽使用 `BUDGET_LIMIT_REACHED` 或 `RunState.BUDGET_EXHAUSTED`。
7. `Fallback` 只指安全发布降级,不用来泛指异常兜底代码。
8. `Agent` 默认指 Diagnosis Agent;SemanticGuard 和 EvidenceRepair 分别称“语义审查器”和“引用修复步骤”。
9. `agent_result` 引用字段名时保留原名,概念说明使用“canonical projected Tool result”。
10. `status` 单独出现没有意义,必须注明所属类型或存储字段。
## 11. 已知命名债务
### 11.1 `DiagnosisRun` 名称大于实际诊断范围
当前 Chat Application 对三种 intent 都写入 `diagnosis_run`。文档统一称 Run;是否重命名实体和表属于单独的协议/迁移决策,本次不修改代码。
### 11.2 `SseOutcome` 当前未被运行时使用
`SseOutcome` 枚举存在,但当前 `ChatSseEvent.Done` 直接携带 `ReleaseOutcome`,并禁止公开 `CANCELLED`。因此当前 SSE 真理源是 `ReleaseOutcome`,不要再基于 `SseOutcome` 推导协议。
### 11.3 `FallbackType.BUDGET_EXHAUSTED` 不是当前诊断发布主路径
枚举值仍存在,但当前 Diagnosis Release 对预算受控停止的行为是:有已验真进展时发布 `INSUFFICIENT_EVIDENCE`,无安全进展时保持失败。文档不能仅因枚举存在就声称系统会发布 `BUDGET_EXHAUSTED` Fallback。
### 11.4 数据库 `status=SUCCESS` 不等于找到根因
`JpaChatRunStore` 将 `ReleaseOutcome.SUCCESS` 和 `FALLBACK` 都映射为数据库 `status=SUCCESS`,表示请求被正常处理。是否发布根因必须结合 `release_outcome`、`content_type` 和 FallbackType 判断。
### 11.5 RunState 没有直接作为独立字段持久化
当前持久化主记录保存通用 `status` 和 `release_outcome`,Trace 保存阶段事件;内存 `RunTermination.state/reason` 不是独立数据库字段。排查时不能只靠数据库 `status` 反推 TIMED_OUT 或 BUDGET_EXHAUSTED 的精确内部终态。
## 12. 如何使用本文
本文不是必读的第一章,而是遇到名词歧义时使用的词典。第一次接触 Harness,请先读 [README.md](README.md) 建立最小心智模型;需要理解某个状态或概念时,再回到本文对应章节查询。
@@ -0,0 +1,140 @@
# Harness LLM Judge 设计笔记:从不可信判定到可信裁决
**更新日期**:2026-08-04
**主题**:SemanticGuard + EvidenceRepair + GuardModelCall = LLM-as-a-judge 模式在证据安全链的完整落地(面试问答版)
**代码位置**:`src/main/java/com/superbiz/agent/harness/guard/semantic/` + `src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java`
## 1. 定位:三个角色
```text
SemanticGuard → 典型 LLM judge:判「结论是否被已验证证据支持」,输出 verdict + reason
EvidenceRepair → judge 的修复延伸(rewriter):验真失败后「只修引用、不修结论」
GuardModelCall → 受控 LLM 调用底座:judge 类调用的基础设施(共用)
```
## 2. 面试五段式回答稿(完整叙事)
### ① 动机(先讲问题,不报组件名)
> 我们的证据安全链里有一道「机械验真」——检查模型引用的每条证据是不是真实来自工具结果,这个用规则就能做。但光验真不够:模型可能引用真实的证据,结论却是「站不住」的——比如证据只支持 A 场景,它却拿去支撑 B 结论。这个「结论被没被证据支持」是**语义判断**,规则引擎做不了,必须靠模型。所以我们需要一个「裁判模型」来判——但裁判模型本身是不可信的,它可能乱判、可能输出奇怪的形状、可能跑很久。所以核心问题是:**怎么让一个不可信的模型做可信的判定**。
### ② 决策(方案 + 放弃了什么)
> 我的方案是:用**隔离的轻量判定模型**——单轮、无工具、输出被强约束,跟主 Agent 的循环完全分离。这里放弃了两条路:第一,让主 Agent 自己判——不行,它已经写了自己的结论,有偏向;第二,纯规则判——语义判断规则做不到。同时有个关键决策:**判读的输入是「视图」不是原始内容**——裁判只看到用户将看到的内容和已验证证据,看不到内部 id 这些实现细节,防止信息污染影响裁判的客观性。
### ③ 实现(关键机制)
> 三个关键机制:
> **输入视图化**:把 draft 投影成「用户可见视图」再交给裁判,剥离内部引用 id;
> **输出硬校验**:裁判的输出必须是恰好两个字段——verdict 和 reason,verdict 必须是合法枚举,reason 不能为空。多一个字段都不接受——我们不信任模型输出的形状,只信它在一个极小的空间里做选择;
> **受控调用**:裁判跑在独立线程、有硬超时、Run 取消能强杀它、它的输入输出都计入预算和 Token 账本——裁判的花费不是无底洞,它也是 Run 的一部分。
### ④ 边界(诚实说不做什么)
> 裁判不判「内容对不对」——那是事实问题,由证据链负责;裁判不自己调工具,单轮无工具;裁判有硬截止线,超时就放弃判定;裁判失败走降级,**不阻塞主结论的发布路径**——我们宁可没有裁决,也不让裁决失败卡死整个流程。
### ⑤ 30 秒话术
> "LLM judge 的完整设计:**动机**是结论的支持度是语义判断、规则做不了,但裁判模型不可信,所以核心是让不可信的模型做可信的判定。**方案**是隔离的轻量判定模型——单轮、无工具、强约束输出。三个关键机制:输入视图化(裁判只看用户可见内容,防信息污染)、输出硬校验(恰好 {verdict, reason} 两字段,多一个都不接受)、受控调用(独立线程、硬超时、取消强杀、计预算记账)。**边界**:裁判不判事实、不调工具、超时即放弃、失败走降级不阻塞主路径。总结一句话——judge 不是追加一个模型调用,而是把『不可信判定』关进笼子里:限定输入、锁死输出、受控运行、失败降级。"
## 3. 追问应对大全
### Q1:为什么 judge 不判事实?(最容易混的边界)
```text
分工:事实由证据链保证,judge 只判「支持关系」
事实真伪 → 证据来自真实工具结果 + EvidenceGuard 验引用真实(根在数据源)
支持关系 → SemanticGuard 判结论与证据的逻辑/相关性
judge 判不了事实的三个原因:
① 没有事实源——它只看「视图 + 已验证证据」,不能查库,判事实只能猜
② 事实真伪需要权威源复核(真实值在哪),judge 拿不到
③ 如果 judge 判事实,它成了第二个事实来源——两个来源可能打架
例子 1(judge 能判的——判支持不是判真伪):
证据:mysql 返回 count(*)=1000;结论:「user 表有 2000 条」
EvidenceGuard 验引用真实 → 通过;SemanticGuard → UNSUPPORTED(数字不一致)
注意:judge 不知道真实值是多少,它只发现「结论与证据不一致」
例子 2(支持关系成立,但事实未必对):
证据:慢查询日志显示 DB 全表扫描;结论:「延迟由 DB 全表扫描导致」
SemanticGuard → SUPPORTED(逻辑上站得住)
但真实原因可能是网络抖动——judge 判不了(没有网络数据源)
→ judge 只能保证「在现有证据下结论站得住」,不能保证「事实就是如此」
一句话:EvidenceGuard 保证「引用的证据是真的」,SemanticGuard 保证
「基于这些证据结论说得通」——事实的真伪从来不是 judge 的职责。
```
### Q2:为什么重试 2 次(semanticGuard 策略)?
```text
可重试性分析:语义审查单轮、无副作用(幂等)——多试几次不会造成破坏
但也不能无限重试:判定有硬截止线(总超时耗尽即放弃)
→ 2 次 = 一次失败的成本 × 收益的平衡点;judge 失败走 Fallback,不影响主路径
```
### Q3:语义不变性怎么保证(EvidenceRepair)?
```text
双重锁死:prompt(只能改 analysis_id / tool_call_ids / based_on_analysis_ids 三字段)
+ SemanticDraftView.hasSameUserVisibleSemantics(修复前后逐字段比对)
关键:比的是「用户可读的内容」不是内部引用 id——
Conclusion 只比 text,不比 basedOnAnalysisIds
Action 只比 action + requiresHumanConfirmation,不比 basedOnAnalysisIds
→ 引用 id 允许变(这正是修复目标),用户看到的文字不许动(碰了判 SCHEMA_INVALID 重试)
```
### Q4:judge 判错了怎么办?
```text
judge 不是最终真相源,是「安全链的一道闸」:
① judge 判 UNSUPPORTED → 不发布,走 Fallback(宁可保守)
② judge 判 SUPPORTED 但事实错 → 那是事实问题,不在 judge 职责(见 Q1)
③ judge 自身失败 → 降级(不阻塞主路径)
→ 设计哲学:judge 的角色是「挡住明显不成立的结论」,不是「证明结论正确」
```
### Q5:为什么独立线程 + 单独超时?
```text
judge 调用不能阻塞主流程(主 Agent 循环)
独立线程 + future.get(timeout) = 硬超时截断
Run 取消 → future.cancel(true) 强杀在途判定(judge 也是 Run 的一部分)
```
### Q6:输入为什么视图化?
```text
judge 只看该看的:用户可见内容 + 已验证证据
剥离内部 id(tool_call_id 等)——防止 judge 用内部信息做「看起来合理」的裁决
(信息污染:judge 看到内部 id 可能产生不当关联,或泄露内部结构到裁决)
```
## 4. 通用 LLM Judge 设计要素(可迁移)
| 通用要素 | 本项目实现 |
|---|---|
| 判什么(judgment task) | 结论是否被已验证证据支持 |
| 输入视图(该看什么) | SemanticDraftView(剥离内部 id)+ 已验证证据 |
| 输出约束(schema) | 恰好 {verdict, reason} + 枚举合法 + reason 非空 |
| 硬校验 | 字段集 equals({verdict, reason})——不多不少 |
| 隔离 | 单轮、无工具、独立线程——judge 不能自己调工具 |
| 硬超时 | 每次 attempt 剩余超时递减,总超时耗尽即放弃 |
| 重试策略 | 可重试性分析:单轮无副作用 → 2 次 |
| 失败降级 | judge 失败 → Fallback(不卡死主路径) |
| 可审计 | ModelCallLedger 记账 + semanticAttempt/evidenceRepairAttempt trace |
| 成本控制 | 输入/输出字节限制 + reserveRunBytes 计入 Run 预算 |
## 5. 代码位置索引
| 类 | 文件 |
|---|---|
| `GuardModelCall` | `src/main/java/com/superbiz/agent/harness/guard/semantic/GuardModelCall.java` |
| `SemanticGuard` | `src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticGuard.java` |
| `SemanticDraftView` | `src/main/java/com/superbiz/agent/harness/guard/semantic/SemanticDraftView.java` |
| `SemanticGuardInput` / `SemanticGuardLimits` | `src/main/java/com/superbiz/agent/harness/guard/semantic/` |
| `EvidenceRepair` | `src/main/java/com/superbiz/agent/harness/release/EvidenceRepair.java` |
| `EvidenceRepairLimits` / `EvidenceRepairPrompt` | `src/main/java/com/superbiz/agent/harness/release/` |
| 重试策略(semanticGuard/evidenceRepair) | `src/main/java/com/superbiz/agent/harness/retry/HarnessRetryPolicies.java` |
@@ -0,0 +1,158 @@
# Harness MySQL 沙箱学习笔记:从 SQL 校验到脱敏投影
**更新日期**:2026-08-04
**主题**:query_mysql 工具完整链路——三层防线(语义/连接/输出)
**配套**:[tool 域代码学习笔记](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)(注册/调用/执行全链路)
## 1. 定位:可查询、不可破坏、不可越界、不可拖库
query_mysql 让模型查询授权数据库,但封死三种攻击面:
```text
破坏:写/删/改(非 SELECT)→ 语义层拒绝
越界:未授权表/列 → 白名单拒绝
拖库:全表通配(*)/无界读取 → 禁通配符 + 三重有界截断
```
**为什么 MySQL 要安全层而 RAG 不要**:Milvus 天然只读检索无破坏面;MySQL 直接连数据库,SELECT 之外全是风险面——**安全设计随攻击面走**。
## 2. 架构总览(三层防线 + 接线员)
```mermaid
flowchart LR
E["MysqlToolAdapter<br/>接线员"] -->|"parse 请求"| V["第 1 层 语义层<br/>MysqlSqlValidator<br/>AST fail-closed"]
V -->|"MysqlQueryPlan"| X["第 2 层 连接层<br/>JdbcMysqlReadOnlyExecutor<br/>JDBC 只读+超时+取消"]
X -->|"MysqlRawResult<br/>(Harness-only)"| P["第 3 层 输出层<br/>MysqlResultProjector<br/>脱敏+有界"]
P -->|"MysqlToolResult<br/>(冻结契约)"| B["ToolBoundary<br/>统一门禁"]
```
**接线员**:Adapter 把 validator/executor/projector 组装进 ToolBoundary;任何安全/参数异常统一映射 INVALID_REQUEST(不泄露内部细节)。
## 3. 定义层(6 个小文件)
| 类 | 作用 |
|---|---|
| `MysqlReadOnlyExecutor` | 函数式接口(执行端口):plan + RunContext → raw 结果 |
| `MysqlQueryPlan` | 执行计划:request + dataSource + normalizedSql + params(params 深拷贝) |
| `MysqlRawResult` | Harness-only raw 结果:columns + rows + truncated(不直接给 Agent) |
| `MysqlSecurityException` | 安全异常:Adapter 映射稳定错误码 |
| `MysqlToolLimits` | 限额:100 行 / 2000 字单元 / 64KB 总字节 / 5 秒超时 |
| `MysqlDataSourceDefinition` | 逻辑数据源 + **schema → table → 列三级白名单**(访问边界) |
**白名单深拷贝**(TreeSet 保证确定性)+ `defaultSchema 必须出现在白名单里`——配置即边界。
## 4. 第 1 层:Validator——语义层(fail-closed)
### 4.1 禁止清单(JSqlParser 解析 AST,逐节点拒绝)
```text
非 SELECT / 多条语句 / WITH / 子查询(SubSelect) / 通配符投影(* 和 t.*)
窗口函数(Analytic) / CASE / EXISTS / 分层查询(OracleHierarchical)
锁读(FOR UPDATE / SKIP LOCKED) / OFFSET/FETCH/TOP/SKIP/FIRST/OPTIMIZE FOR
字面量:StringValue/LongValue/DoubleValue/HexValue/DateValue/TimeValue/TimestampValue
—— 全部必须参数化(防注入的最强形态)
```
### 4.2 允许清单
```text
白名单内表列的 INNER/LEFT JOIN(禁 CROSS/RIGHT/FULL/OUTER)
聚合函数:COUNT/SUM/AVG/MIN/MAX(COUNT(*) 只允许 COUNT)
占位符参数 ?(PreparedStatement 绑定)
```
### 4.3 附加校验
```text
恰好一条语句 + 必须是 Select + 无 WITH + 普通 PlainSelect(禁集合操作/值语句)
FROM 必须是白名单内实体表(禁子查询来源);重复别名拒绝(不区分大小写)
列校验:限定表 → 查该表列白名单;未限定 → 已注册表恰好一个命中(防歧义/未授权)
占位符数量 == params 数量(防参数错位/少传)
```
### 4.4 fail-closed 原则
```text
任何解析/校验异常 → 统一转 MysqlSecurityException(默认拒绝,不是默认放行)
业务规则违规原样穿出;解析/未知异常包装统一信息(不泄露内部细节)
→ 安全策略是「拒绝清单外的全允许」的反面:「允许清单外的全拒绝」
```
## 5. 第 2 层:Executor——连接层(双保险)
```text
connection.setReadOnly(true) ← JDBC 连接层强制只读(语义层之外的物理防线)
PreparedStatement 参数化 ← 占位符绑定(防注入第二道)
setQueryTimeout(5s) ← 慢查询截断
setMaxRows(maxRows+1) ← 多取一行用于检测截断
context.cancellation().onCancel → statement.cancel() ← Run 取消联动
checkRun 每行检查 ← 取消/超时即刻中止(与 core 终态联动)
jsonSafe:byte[] → Base64;字符串截断 maxCellChars
estimatedBytes:字节预算超限移除末行并标记截断
```
**关键**:查询不是独立资源——**受 Run 生命周期管**(取消 → 立即 cancel 语句),与 RAG 检索同理(checkRun 与 Harness core 终态联动)。
## 6. 第 3 层:Projector——输出层
### 6.1 脱敏(输出时,不是查询时)
```text
列名含 password/passwd/token/secret/api_key/apikey/credential → [REDACTED]
→ raw 保留真实值,只对 Agent 可见层脱敏(查询照常执行,输出才遮)
```
### 6.2 有界(三重截断 + 兜底)
```text
行数(maxRows) + 单元格字符(maxCellChars) + 总字节(maxResultBytes)
列名必须非空且唯一(防歧义投影)
fitBudget 兜底:逐行裁掉尾部 → 裁空诚实降级 NO_EVIDENCE → 仍超限 fail closed
```
### 6.3 客观证据语义
```text
rows 空 → NO_EVIDENCE;非空 → EVIDENCE_FOUND
→ 与 RAG 的 evidence_status 同一套契约(证据状态由「有没有内容」客观决定)
```
## 7. 与 RAG 对照(同类架构,不同复杂度)
| | query_mysql | lookup_knowledge |
|---|---|---|
| 安全层 | 有(Validator + 只读连接 + 脱敏) | 无(Milvus 天然只读检索) |
| 后端复杂度 | 简单(Validator→Executor→Projector) | 复杂(三段 + 降级 + 双 trace) |
| raw → 契约 | MysqlRawResult → MysqlToolResult | LookupResult → RagToolResult |
| 数量限制 | maxRows=100 / 64KB | returnN=5 / 8 条 / 16KB |
| 证据语义 | rows 空不空(NO_EVIDENCE/EVIDENCE_FOUND) | evidenceBlocks + relevance_level |
| 与 Harness 衔接 | 同为 Boundary/Projector/Adapter 模式 | 同为 Boundary/Projector/Adapter 模式 |
## 8. 易错点
| 易错 | 正确 |
|---|---|
| Validator 只查 SELECT | 还有白名单表列、禁字面量、禁通配符、占位符计数 |
| 语义层够了 | 连接层 setReadOnly + 参数化是物理防线(纵深防御) |
| 查询独立于 Run | 查询受 Run 取消/超时联动(onCancel → statement.cancel) |
| 脱敏在查询层 | 脱敏在投影层(raw 保留真实值,只对 Agent 脱敏) |
| 有界只限行数 | 行 + 单元格 + 字节三重截断 + fitBudget 兜底 |
| 安全异常抛原样 | 统一映射 INVALID_REQUEST(不泄露内部细节) |
| 字面量可以清洗放行 | 字面量全拒必须参数化(清洗是弱防线,参数化是强防线) |
## 9. 面试话术(30 秒)
> "query_mysql 是三层防线的只读沙箱:**语义层**(JSqlParser 解析 AST,fail-closed 拒绝一切不安全形态——非 SELECT、多语句、子查询、通配符、字面量、未授权表列、锁读全部拒绝,只允许白名单表列的 INNER/LEFT JOIN 和聚合 + 参数化占位符,且占位符数量必须与 params 匹配);**连接层**(JDBC setReadOnly + PreparedStatement 参数化 + 超时 + maxRows + 取消联动——Run 取消立即 cancel 语句);**输出层**(敏感列脱敏 [REDACTED] + 行/单元格/字节三重截断 + rows 空不空定证据状态)。安全异常统一映射稳定错误码,不泄露内部细节。"
## 10. 代码位置索引
| 类 | 文件 |
|---|---|
| `MysqlToolAdapter` | `src/main/java/com/superbiz/agent/harness/tool/adapter/MysqlToolAdapter.java` |
| `MysqlSqlValidator` | `src/main/java/com/superbiz/agent/harness/tool/mysql/MysqlSqlValidator.java` |
| `JdbcMysqlReadOnlyExecutor` | `src/main/java/com/superbiz/agent/harness/tool/mysql/JdbcMysqlReadOnlyExecutor.java` |
| `MysqlResultProjector` | `src/main/java/com/superbiz/agent/harness/tool/mysql/MysqlResultProjector.java` |
| `MysqlDataSourceDefinition` | `src/main/java/com/superbiz/agent/harness/tool/mysql/MysqlDataSourceDefinition.java` |
| `MysqlReadOnlyExecutor` | `src/main/java/com/superbiz/agent/harness/tool/mysql/MysqlReadOnlyExecutor.java` |
| `MysqlQueryPlan` / `MysqlRawResult` | `src/main/java/com/superbiz/agent/harness/tool/mysql/MysqlQueryPlan.java` 等 |
| 契约(MysqlToolRequest/Result) | `src/main/java/com/superbiz/agent/harness/tool/contract/MysqlTool*.java` |
@@ -0,0 +1,352 @@
# Harness RAG 检索体系学习笔记:从 query 到可验证证据
> **现状说明(2026-09-29)**:RAG 模块抽离后,本文「检索前 L0 导航」与
> `MilvusHybridKnowledgeStore` / `KnowledgeQueryTransformer` 相关章节描述的类已删除
> (L0 与向量检索下沉 py-rag 服务端);检索后处理/打包/投影/审计部分仍然现行。
> 当前架构见 [../../architecture/RAG知识检索架构.md](../../architecture/RAG知识检索架构.md)。
**更新日期**:2026-08-03
**主题**:lookup_knowledge 完整后端链路——检索前/检索/检索后/打包/组装/降级/契约/验证
**设计文档**:`mvp/engineering/rag/`(RAG 排序、Hybrid 质量分、relevance_level 等)
**代码视角**:[Harness tool 域代码学习笔记](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)(工具链衔接)
## 1. 定位与骨架
一次 `lookup_knowledge` 从 query 到证据的旅程(模块化三段 + 收尾):
```mermaid
flowchart LR
Q["query"] --> A["检索前<br/>KnowledgeQueryTransformer<br/>L0 导航(分类/域/关键词)"]
A --> B["检索<br/>KnowledgeDocumentRetriever<br/>dense + BM25 → RRF 融合"]
B --> C["检索后<br/>KnowledgeEvidencePostProcessor<br/>qualityScore/去重/判级/闸门"]
C -->|"低质"| B2["降级重试<br/>UNFILTERED_VECTOR_RETRY<br/>(去过滤 + 原始 query)"]
C --> D["打包<br/>KnowledgeContextPacker"]
D --> E["组装<br/>LookupResultAssembler<br/>→ LookupResult"]
E --> F["投影<br/>RagResultProjector<br/>→ RagToolResult(Agent 契约)"]
```
**关键特征**:模块化三段各自独立 Service、降级是阶段间控制流、双轨可观测(RetrievalTrace + RerankTrace)、LookupResult 是内部契约(Agent 看到的是投影后的 RagToolResult)。
## 2. 检索前:L0 导航(缩短边界,不决定边界)
- **产出**:categoryFilter / domainHints / matchedKeywords / entities / l0Titles(从 query 语义推导)
- **只缩短边界**:categoryFilter 限定「搜哪些分类」(FILTERED_VECTOR)
- **不决定边界**:低质 → 降级去掉过滤重查(L0 边界可被推翻)
- **只解释不打分**:L0 命中只写 hitReasons(l0_domain_overlap),不改 qualityScore 和排序——防关键词碰瓷
- **语义差异应对**:降级用原始 query(非 rewritten)——抹掉 L0 推导误差
## 3. 检索:多路召回 + RRF
**为什么混合检索**:旧方案(dense 语义 + L0 关键词加权重排)有词频碰瓷误差——词频高但相关性不高的排前面。
```mermaid
flowchart LR
subgraph 召回
D["dense ANN(L2)<br/>抓语义相似"]
S["sparse BM25<br/>抓精确匹配"]
end
D --> R["RRF 融合<br/>score = Σ w/(k + rank)"]
S --> R
R --> F["融合排序(originalRank)"]
```
**关键决策**:
- **RRF 用排名不用分数**——屏蔽跨路分数尺度不可比(dense 的 L2 vs BM25 的稀疏分)
- **k=60**(`retrieval.hybrid.rrf-k`)——平滑参数,排名差异对分数的影响平缓
- **加权是预留能力**:RrfFusion 支持 `w/(k+rank)`,但当前 Milvus 服务端走等权 RRFRanker(只传 k)——想让某路更可信时再调旋钮
- **召回优先**(RAG 排序文档观点):候选池只有 3 条时,精排只能换座位,召不回的内容永远排不上来
## 4. 检索后:qualityScore 统一 + 质量闸门
### 4.1 为什么需要统一分数
三路返回三种分数(L2 距离 / BM25 稀疏分 / RRF 融合分)——不可比,必须统一成 qualityScore ∈ [0,1]。
### 4.2 打分(RetrievalScoreNormalizer)
```text
DENSE: l2ToQuality(score) = 1 - clamp(L2)/maxL2 (maxL2 默认 2.0)
HYBRID: denseDistance != null ? l2ToQuality(denseDistance) ← 恢复绝对质量
: rankToQuality(rank, batchSize) ← BM25-only 保守回退
```
**denseDistance 的来源**(隐藏机制):hybrid 融合后**再单独跑一次 searchDense**,按 id 把 L2 补到融合结果上——因为服务端 RRF 只输出融合分,原始 L2 信息丢了。`attachDenseDistances` 只填充不改 score/label/order(排序评估分离的又一体现)。
### 4.3 排序与评估分离(核心设计)
```text
originalRank(RRF 融合序)→ 排序:谁在前面(相对序)
qualityScore(L2/rank) → 评估:够不够格、要不要降级(绝对度)
→ 排序不用 quality 重排(防 boost 操纵)
→ quality 只被判级和闸门消费
```
**为什么排名不能证明质量**:排名是「序」(A 在 B 前),质量是「度」(0.75 就是 0.75)——排第 1 只代表「这批里最好」,不代表「够好」(候选池全是低质时排第 1 的也低质);RRF 分本身不含距离信息;跨批次的两个「第 1 名」绝对质量天差地别。
### 4.4 五步流程(process)
```
① 打分(toQualityScore)→ ② 排序(originalRank,不用 quality 重排)
③ 去重/截断:evidenceKey 合并 + maxChunksPerDocument=2 + returnN=5
④ 判级:top qualityScore ≥0.75→PRECISE / ≥0.5→REFERENCE
⑤ 闸门:topSimilarity <0.5 → 低质 → 降级重查
```
**两级去重**(粒度不同):
```
evidenceKey 去重(chunk 级):同 chunk(docId#chunkIndex)被两路召回 → mergeEvidence 合并
—— mergeEvidence 只合并 hitReasons + 补 breadcrumb,不处理 content(同 chunk 内容相同)
maxChunksPerDocument(文档级):同文档不同 chunk 最多 2 个 → 防单文档垄断证据槽位
```
**判级用 ranked(全量)不是 deduped**——判级评估「整体质量」(全量 top),去重决定「输出内容」(合并片段),两件事平行。
**BM25-only 的弱点**:无 dense 邻居 → rankToQuality 回退(排第 1 恒为 1.0)——整批 BM25-only 时闸门永不降级(topSimilarity=1.0 ≥ 0.5)。改善方向:文本相似度兜底(绝对信号)+ 批次一致性检查(整体水平),而非返回 BM25 分(统计度量无绝对语义)。
## 5. 契约语义:relevance_level 是什么、不是什么
- **是什么**:一次 lookup_knowledge 调用整体有多相关的**粗档标签**(判级产出:PRECISE/REFERENCE/null)
- **不是什么**:不是单条 evidence 的分数、不是相似度数值、不是「结论可发布」判据
- **Agent 正确用法**:evidence_status 管有没有证据,relevance_level 管这批评据多硬/要不要再查,实际写诊断引用的是 evidence[].excerpt
- **REFERENCE ≠ NO_GAIN**:一般相关可能仍排除一个假设——语义价值由模型判断(progress 原则)
## 6. 打包与组装
### ContextPacker(打包)
```
输入顺序即优先级 → 逐条塞进 4000 字符预算
单条放不下 → content 截断(+"...")
连 header 都放不下 → 整条省略(记 omittedSources)
输出:packedText + strategy + charBudget/usedChars + included/omittedSources
```
**当前定位**:Agent 主要看结构化 evidence 列表,packedText 更多用于内部/调试/审计(Harness 用结构化列表因为可验真——evidence_ref 引用 document_id)。
### LookupResultAssembler(组装)
- **内部契约出口**:found / evidenceBlocks / 双数量 / 双 trace / relevanceLevel / completenessHint / message
- **found 在这里综合判定**:evidence.hasUsableEvidence()
- **message 语义**:found=false 时「知识库未检索到可用证据,请结合日志、指标、告警继续排查」——证据不足不是失败,是换方向引导
- **与投影的关系**:LookupResult 是后端完整出口,RagResultProjector 再裁剪成 Agent 契约(两层契约)
## 7. 降级:突破 L0 边界的兜底
```
触发:categoryFilter != null && isLowQuality(无证据 或 topSimilarity < 0.5)
动作:原始 query(非 rewritten)+ 去掉分类过滤 + 覆盖选择(不合并两轮)
原因分类:FALLBACK_LOW_QUALITY(查到了但低质)/ FALLBACK_NO_EVIDENCE(完全没查到)
有限降级:只一次(防重查风暴);retry 低质也接受(降级失败直接返回)
trace:attempts 记录全部尝试(FILTERED/UNFILTERED/UNFILTERED_RETRY)
+ selectedAttempt + fallbackReason → 可对比「过滤 vs 全域」判断 L0 过滤是否过度
```
## 8. 投影衔接(RagResultProjector)
- **契约转换**:LookupResult JSON → RagToolResult(evidence[] + relevance_level + evidence_status + truncated)
- **四重有界**:query 500 字 / excerpt 1200 字 / 条数 8(maxEvidence)/ 总字节 16KB(fitBudget)
- **优先级链去重**:evidenceKey → document_id → docId#chunk-idx → legacy 序号(chunk 级身份)
- **诚实标记**:一切有损(截断/去重丢弃/query 截断)都置 truncated;fitBudget 裁空诚实降级 NO_EVIDENCE + relevance 置 null(没有证据就没有相关度,自洽)
- **两级数量限制**:后端 returnN=5(业务目标值)vs maxEvidence=8(Harness 护栏)——5<8 时护栏休眠,后端配置失控时兜底
## 9. 验证:审计 + 离线评测
### 9.1 审计 vs Trace(两套记录系统)
| | DiagnosisTrace(事件流) | ToolInvocationAudit(调用档案) |
|---|---|---|
| 粒度 | 事件(一次调用多个事件) | 记录(一次调用一行) |
| 覆盖 | Run 全生命周期 | 仅工具调用 |
| 内容 | metadata-only(不存 raw) | 完整(raw + agentResult + enrichments) |
| 用途 | 时序回放 / Token 对账 | 单次调用深查 |
```
RAG 后端双 trace(Retrieval/Rerank)→ 进 LookupResult → Harness
→ 安全字段提取 → tool_invocation 审计(enrichments)
→ 轻量事件 → DiagnosisTrace
审计在投影之后、给模型之前(ToolBoundary.execute 内先落库)
```
### 9.2 离线评测设计(eval/rag-retrieval)
```
三层:offline(fixtures × golden-cases,无真实栈)/ snapshot 生成(真实跑一次冻结)/ live smoke
断言:行为契约(expectedDocIds/Breadcrumbs/Keywords/SelectedAttempt/FallbackReason/EvidenceStatus)
+ 不断言 raw scores / chunkId / 全序(脆或内部实现)
基线:baseline.json + diff(区分有意改进 vs 无意回归)
隔离:seed-docs + kb_scope=rag-eval
```
**断言设计原则**:
- 断言「用户可感知的结果 + 管道行为」,不断言「实现细节」(chunk/分数/全序)
- 双信号:fail 抓行为回归 + diff 抓「绿了但漂了」(通过但退化可见)
- **期望来自设计意图,不从当前输出反推**(否则固化 bug)
- golden set 是演进的:初期种子(设计意图)→ 中期真实数据(**必须人工验证**——成功案例只是观测,不是契约)→ 持续事故固化
### 9.3 指标(缺的下一步)
```
检索质量:recall@k / Hit@k / MRR(复用 expectedDocIds + rank,低成本)
管道行为:降级触发正确率 / 降级有效率 / 过滤误伤率(复用 trace 字段)
质量闸门:低质识别准确率 / 降级误杀率(BM25-only 弱信号代价可测)
需要新标注:precision@k(负例)/ nDCG(相关度分级)
```
## 10. 易错点
| 易错 | 正确 |
|---|---|
| RRF 排序了就不用打分 | 排序(RRF)与评估(qualityScore)分离——RRF 管谁在前,L2 管够不够格 |
| 排第 1 = 质量好 | 排第 1 只代表「这批里最好」——候选池全低质时排第 1 也低质 |
| 返回 BM25 分能解决 BM25-only | BM25 是统计度量(无界/依赖集合),无绝对语义——用文本相似度/一致性检查 |
| denseDistance 是融合分 | 是融合后再跑一次 dense 探测的 L2(RRF 丢了原始 L2) |
| 去重和判级有先后 | 平行:去重管输出(deduped),判级管评估(ranked 全量 top) |
| 后端 returnN=5,maxEvidence=8 多余 | returnN 是业务目标值,maxEvidence 是 Harness 护栏(防配置失控) |
| 线上成功案例可直接当 golden | 成功只是「当前实现没出错」的观测——必须人工确认设计意图 |
| 审计在给模型之后 | 审计在投影后、给模型前(ToolBoundary 内先落库) |
## 11. 讨论沉淀:值得记住的问题与洞见
本节收录学习过程中的关键问答——按价值分层,面试准备直接翻这里。
### 11.1 触及设计本质(第一梯队)
**① 「RRF 已经排序了,为什么还要打分」——排序与评估分离**
```text
RRF 管「谁在前面」(融合排序,相对序)
qualityScore 管「够不够格」(质量评估,绝对度)
为什么排名不能证明质量:
排名是「序」(A 在 B 前),质量是「度」(0.75 就是 0.75)
排第 1 只代表「这批里最好」,不代表「够好」——候选池全是低质时排第 1 也低质
RRF 分不含距离信息;跨批次的两个「第 1 名」绝对质量天差地别
```
**② 「最终落地到 dense 决定,RRF 白用了吗」——谁被评估 vs 评估够不够**
```text
RRF/BM25 管:召回 + 排序(BM25 路召回 dense 召不回的候选,RRF 让两路共识靠前)
dense L2 管:质量评估的绝对标尺(唯一有绝对语义的)
→ 分工:RRF 决定「谁能被评估」,dense 决定「评估结果够不够」
→ 「排第一但 dense 低质 → 降级」不是矛盾,是排序与评估分离的价值(发现域选错)
```
**③ 「能不能返回 BM25 分当质量」——度量类型决定能否设阈值**
```text
几何度量(L2):embedding 空间稳定 → 能设 0.75/0.5 绝对阈值
统计度量(BM25):无界、依赖集合 IDF、随集合演进漂移 → 设不了稳定阈值
→ 质量评估需要绝对标尺,只能来自几何度量或可解释相似度(字符重叠)
→ 改善 BM25-only:文本相似度兜底 / 批次一致性检查 / fail-closed,而非返回 BM25 分
```
**④ 「BM25-only 质量有问题」(自己发现的设计弱项)**
```text
rank 回退:排第 1 恒为 1.0 → 整批 BM25-only 时闸门永不降级(topSimilarity=1.0 ≥ 0.5)
根因:rank 是相对序(第 1 名不代表够 0.5),顶位给满分是「排序最好 = 质量满分」的错误等价
改进:rank 顶位保守化 / 文本相似度兜底 / 批次一致性检查
```
### 11.2 隐藏机制(第二梯队)
**⑤ 「denseDistance 是融合分还是 dense 分」**
```text
是 dense 那一路的 L2——RRF 服务端融合只输出融合分,原始 L2 信息丢了
→ attachDenseDistances 融合后再单独跑一次 searchDense,按 id 把 L2 补到融合结果
→ 只填充不改 score/label/order(排序评估分离的又一体现)
```
**⑥ 「为什么降级只一次」——有限降级**
```text
降级 = 突破 L0 边界重查(原始 query + 去过滤 + 覆盖选择)
只降级一次:预算约束(retrieveK × 2 检索成本)防重查风暴
fallbackReason 区分:FALLBACK_LOW_QUALITY(查到了但低质)/ FALLBACK_NO_EVIDENCE(没查到)
```
**⑦ 「returnN=5 为什么 maxEvidence=8」——目标值 vs 护栏**
```text
returnN=5:RAG 业务目标值(rag.return-n)——打算给 5 条
maxEvidence=8:Harness 安全上限(ToolProjectionLimits)——最多允许多少
两级解耦:业务层和安全层各自配置;5<8 时护栏休眠,后端配置失控时兜底
```
### 11.3 方法论沉淀(第三梯队,可迁移)
**⑧ 从 0 设计离线评测的八步**
```text
目标(回归保护)→ 粒度(工具级)→ 输入(冻结快照)→ 断言(行为契约)
→ 用例(行为维度覆盖)→ 隔离(种子数据)→ 基线(区分有意/无意变化)→ 成本(分层运行)
```
**⑨ golden set 怎么设计**
```text
行为清单 → 每个行为一个 case → query 拟真(能触发目标行为)
→ 期望来自设计意图(不从当前输出反推——否则固化 bug)→ 补负例/边界
→ 演进:初期种子打底 → 中期真实数据(人工验证后转契约)→ 持续事故固化
```
**⑩ 「线上成功案例能不能直接用」——观测 ≠ 契约**
```text
线上成功只是「当前实现没出错」的观测:可能恰好没触发 bug 路径、结果碰巧对
→ 必须人工确认「结果确实符合设计意图」后才从观测升级为契约
→ 失败案例则明确「期望应该怎样」作回归保护
```
**⑪ 「不用 chunk 断言也是数据原因吗」——不是**
```text
chunk 边界是切分实现细节:算法优化/文档微调都让 chunk 偏移 → 合法重构被误判回归
契约语义的证据单位是文档级(document_id 常等于 source)——chunk 模型都看不到
→ 即使数据充足也不该断言 chunk(和数据量无关)
```
### 11.4 三个核心洞见(最值得记住)
```text
① 排序与评估分离:RRF 管「序」(相对),L2 管「度」(绝对)——排名不能证明质量
② 度量类型决定能不能设阈值:几何(L2)可以,统计(BM25)不行
③ 期望来自设计意图,不从实现反推——这是评测和 golden set 的分水岭
```
## 12. 面试话术(30 秒)
### 11.1 排序与评估为什么分离
> "RRF 管『谁在前面』(融合排序),qualityScore 管『这批结果够不够格』(质量评估)——打分不是重排,是排序后的质量校验。RRF 分是排名派生的相对值,没法设绝对阈值(排第 1 不代表够 0.5,候选池全是低质时排第 1 的也低质);qualityScore 把 dense L2 归一化成 [0,1] 的绝对质量,用于判级(0.75/0.5 阈值)和闸门(<0.5 触发降级)。排序决定看哪些,评估决定够不够好。"
### 11.2 为什么 BM25 分不能当质量
> "质量评估需要绝对标尺,绝对标尺只能来自几何度量(L2 距离——embedding 空间稳定)或可解释的相似度(字符重叠),不能来自统计度量(BM25——无界、依赖文档集合的 IDF、随集合演进漂移)。BM25-only 命中用排名回退估质量(保守),但顶位给满分是设计弱项——改进方向是文本相似度兜底或批次一致性检查,而不是返回 BM25 分。"
### 11.3 降级设计
> "降级是突破 L0 过滤边界的兜底:带分类过滤检索结果低质(无证据或 topSimilarity<0.5)时,用原始 query + 去掉分类过滤重查一次(UNFILTERED_VECTOR_RETRY),结果覆盖选择、记录进 trace。fallbackReason 区分『查到了但低质』vs『完全没查到』;只降级一次(预算约束防重查风暴),降级失败也直接以低质结果返回。attempts 列表让审计能对比过滤 vs 全域检索差异,判断 L0 过滤是否过度。"
### 11.4 golden set 怎么设计
> "golden set 是行为契约的清单:先列要保护的行为,每个行为一个 case(不耦合可定位);query 用能触发目标行为的真实形态;期望来自设计意图(我知道这个文档属于这个场景),绝不从当前输出反推(否则固化 bug);补负例与边界;golden set 是演进的——初期人为种子打底,中期真实数据必须人工验证后才能转契约,持续事故修复固化。核心:断言用户可感知的结果 + 管道行为,不断言实现细节。"
## 13. 代码位置索引
| 类 | 文件 |
|---|---|
| `LookupKnowledgeTool` | `src/main/java/com/superbiz/agent/tool/LookupKnowledgeTool.java` |
| `KnowledgeQueryTransformer` | `src/main/java/com/superbiz/agent/service/KnowledgeQueryTransformer.java` |
| `KnowledgeDocumentRetriever` | `src/main/java/com/superbiz/agent/service/KnowledgeDocumentRetriever.java` |
| `KnowledgeEvidencePostProcessor` | `src/main/java/com/superbiz/agent/service/KnowledgeEvidencePostProcessor.java` |
| `KnowledgeContextPacker` | `src/main/java/com/superbiz/agent/service/KnowledgeContextPacker.java` |
| `LookupResultAssembler` | `src/main/java/com/superbiz/agent/service/LookupResultAssembler.java` |
| `RetrievalScoreNormalizer` | `src/main/java/com/superbiz/agent/service/retrieval/RetrievalScoreNormalizer.java` |
| `RrfFusion` | `src/main/java/com/superbiz/agent/service/retrieval/RrfFusion.java` |
| `MilvusHybridKnowledgeStore` | `src/main/java/com/superbiz/agent/service/milvus/MilvusHybridKnowledgeStore.java` |
| `RagResultProjector` | `src/main/java/com/superbiz/agent/harness/tool/projection/RagResultProjector.java` |
| 审计链路 | `src/main/java/com/superbiz/agent/harness/audit/`(RagLookupAuditEnricher / JpaToolInvocationAuditSink) |
| 离线评测 | `eval/rag-retrieval/` + `scripts/eval_rag_retrieval.py` |
@@ -0,0 +1,203 @@
# Harness Tool 调用链:一次工具调用的完整旅程
**更新日期**:2026-08-03
**主题**:从「模型决定调用工具」到「模型收到观察」的运行时完整链路——拦截器 → invoke → Adapter → ToolBoundary → 返回 → 二次加工 → ToolCallResponse
**结构篇**:[Harness tool 域代码学习笔记-工具的注册调用与执行链路](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)(讲装配/注册/静态结构)
**本文**:动态时序(一次调用怎么跑完)
## 1. 旅程全景(一张图)
```mermaid
sequenceDiagram
participant M as 模型
participant F as 框架 ReactAgent
participant I as HarnessToolInterceptor(per-Run)
participant ET as HarnessEvidenceTools(单例)
participant AD as RagToolAdapter(单例)
participant TB as ToolBoundary(单例)
participant P as DiagnosisProgressTracker
rect rgb(240, 248, 255)
Note over M,I: 阶段 A:模型决定 → 拦截器(执行前)
M->>F: 输出 tool_call(工具名 + 参数 JSON)
F->>I: 回调 interceptToolCall(request, handler)
I->>I: ① supports 注册检查
I->>ET: ② parse(typed 严格契约)
ET-->>I: ParsedAgentToolCall(previous_observation + input)
I->>P: ③ 协议校验(pending 评价)+ 判重
end
rect rgb(255, 250, 240)
Note over I,TB: 阶段 B:invoke → 执行(backend + 投影)
I->>ET: ④ invoke(context, toolName, toolCallId, args)
ET->>AD: bridge 闭包 → adapter.execute(context, envelope)
AD->>TB: boundary.execute(context, envelope, executor, projector)
TB->>TB: ⑤ 五阶段:preflight/预算/begin → executor 跑 backend → 校验 → projector 投影 → markReady
TB-->>I: ToolBoundaryResult(READY/ERROR)
end
rect rgb(245, 255, 245)
Note over I,M: 阶段 C:返回 → 模型(执行后)
I->>I: ⑥ 双源校验(controlView 重读 evidence_status)
I->>P: ⑦ recordCompleted(NO_EVIDENCE 立即 NO_GAIN / FOUND 挂 pending)
I->>I: ⑧ modelObservation 加工(有界观察 + stop_required/reason)
I-->>F: ToolCallResponse.of(toolCallId, toolName, observation)
F-->>M: observation 作为本轮 tool 结果
end
```
**三个阶段**:A 执行前(模型决定→门禁)→ B 执行中(invoke→backend→投影)→ C 执行后(校验→记账→成型)。
---
## 2. 阶段 A:模型决定 → 拦截器(执行前)
### 2.1 模型怎么知道有这个工具
```
模型 → callbacks 里看到工具(名字+描述+Schema)→ 决定调用 lookup_knowledge
→ 输出 tool_call JSON(工具名 + 参数)
```
工具名是**模型决定的**——框架把模型输出包成 `ToolCallRequest`(含 toolName + arguments),回调拦截器。
### 2.2 拦截器的三道执行前门
```mermaid
flowchart LR
A["① supports(toolName)?"] -->|"否(非证据工具)"| X["handler.call 透传"]
A -->|"是"| B["② parse:typed 严格契约<br/>FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS"]
B -->|"违规"| Y["协议处理(不执行)"]
B --> C["③ 协议校验(pending 评价)+ 判重"]
C -->|"重复"| Z["recordDuplicateScope(不执行)"]
C -->|"通过"| D["进入阶段 B:invoke"]
```
关键:**不是「拿到名字就执行」**——parse(模型输出必须精确匹配 `RagToolCall{previous_observation, input}`,多一个字段都炸 INVALID_ENVELOPE)、协议校验、判重,三道门不通过都不执行 backend。
---
## 3. 阶段 B:invoke → 执行(backend + 投影)
### 3.1 invoke 的委托链
```
I.invoke(context, "lookup_knowledge", "call-1", args)
→ ET.invokers.get("lookup_knowledge") ← 注册表取 bridge 闭包
→ bridge lambda:adapter.execute(context,
new ToolCallRequestEnvelope(runId, "call-1", "lookup_knowledge", args, true, true))
→ ragAdapter.execute(context, envelope)
→ boundary.execute(context, envelope, executor, projector)
```
**envelope 是 bridge 里现造的**:`authorized=true, readOnly=true` 写死——每个进 ToolBoundary 的信封都声明「已授权 + 只读」。
### 3.2 Adapter 组装两个函数(接线员)
```java
return boundary.execute(context, envelope,
// executor:跑 backend 拿 raw(LookupResult 序列化成 JSON 文本)
ignored -> objectMapper.writeValueAsString(legacyExecutor.execute(request.query())),
// projector:raw → 有界契约 + evidenceStatus
raw -> projector.project(request, envelope.toolCallId(), raw));
```
| 端口 | 干什么 | 产物 |
|---|---|---|
| `executor` | 调具体后端 | rawResponse(JSON 文本,执行链「货币」) |
| `projector` | 净化定型 | ProjectedToolResult(agentResult, evidenceStatus) |
**模型永远看不到 raw**——raw 只用于校验、落 canonical、投影。
### 3.3 ToolBoundary 五阶段
```mermaid
flowchart TD
A["① preflight + Tool 预算 + request bytes → begin(PROJECTING)"]
B["② executor.execute(requestJson) → backend raw"]
C["③ raw 大小校验 + Run bytes 预留"]
D["④ projector.project(raw) → 有界 agent_result + evidenceStatus"]
E["⑤ agent_result 校验 + bytes → markReady(READY) 或 markError(ERROR)"]
A --> B --> C --> D --> E
```
返回 `ToolBoundaryResult(READY/ERROR)`——PROJECTING 永不外泄。
---
## 4. 阶段 C:返回 → 模型(执行后)
### 4.1 拦截器的二次加工(不是直接返回)
```mermaid
flowchart LR
A["ToolBoundaryResult"] --> B{"status == READY?"}
B -->|"否"| E1["error observation<br/>BUDGET_EXHAUSTED 额外 markBudgetLimitReached"]
B -->|"是"| C["⑥ 双源校验:controlView 重读 evidence_status"]
C -->|"不一致"| E2["OBSERVATION_CONTRACT_MISMATCH 拒绝"]
C -->|"一致"| D["⑦ recordCompleted<br/>NO_EVIDENCE → 立即 NO_GAIN<br/>FOUND → 挂 pending"]
D --> F["⑧ modelObservation 加工<br/>(有界观察 + stop_required/reason)"]
F --> G["ToolCallResponse.of(...) → 框架 → 模型"]
```
### 4.2 双源校验(自洽性防线)
```text
源1:result.evidenceStatus() ← Projector 投影时计算的声明值
源2:controlView(agentResult).evidenceStatus() ← 从 agent_result 内容重读
一致 ? 通过 : OBSERVATION_CONTRACT_MISMATCH 拒绝
```
防止「声明有证据但内容空 / 声明无证据但内容有」的不一致状态进入 progress 记账。
### 4.3 给模型的对象形态
```
ToolCallResponse.of(toolCallId, toolName, observation)
observation = 有界观察:
正常结果:脱敏后的契约内容(可能裁剪)
饱和时: 附加 stop_required:true + reason
协议错误:repair_required:true + violation_type/期望ID/指令
```
框架把 observation 作为本轮 tool 结果给模型——**模型下一轮读取它,决定继续调用(带评价)还是输出 Draft 收尾**。
---
## 5. 旅程的衔接点(模型视角的闭环)
```mermaid
flowchart LR
A["模型调工具"] --> B["观察(有界契约)"]
B --> C{"模型决定"}
C -->|"继续"| D["下次 Tool Call + previous_observation 评价"]
C -->|"收尾"| E["输出 Draft → Release 发布"]
D --> B
```
**progress 协议的闭环**:模型每次继续调用,都要在 Envelope 里回带对上一轮的 GAINED/NO_GAIN 评价——这就是拦截器 ③ 校验的 pending 逻辑(可回看 progress 笔记)。
---
## 6. 关键点总结
| 阶段 | 关键认知 |
|---|---|
| A 执行前 | 工具名是模型决定的;parse 是 typed 严格契约(输出必须匹配 Schema);三道门不通过不执行 |
| B 执行中 | executor/projector 是 Adapter 组装进 boundary 的**参数**;执行链货币是 JSON 文本;模型永远看不到 raw |
| C 执行后 | 拦截器不直接返回——双源校验 + progress 记账 + modelObservation 成型;ToolCallResponse 才是模型拿到的对象 |
## 7. 面试 30 秒说法
> "一次工具调用的完整旅程分三段:执行前,模型从 callbacks 看到工具并决定调用,拦截器做 supports 分流、typed 严格 parse、协议校验和判重——三道门不通过都不执行 backend;执行中,invoke 经 bridge 到 Adapter,Adapter 把 executor(跑 backend 拿 raw)和 projector(raw 投影成有界脱敏契约)组装进 ToolBoundary 的五阶段门禁,返回 ToolBoundaryResult;执行后,拦截器不直接返回——先双源校验 evidence_status,再 recordCompleted 记进度,再 modelObservation 加工成有界观察,最后包装成 ToolCallResponse 给模型。模型看到的永远是脱敏后有界的观察,raw 只进 canonical 供审计验真。"
## 8. 代码位置索引
| 环节 | 文件 |
|---|---|
| 拦截器(A/C 阶段) | `src/main/java/com/superbiz/agent/harness/agent/HarnessToolInterceptor.java` |
| 注册表 + parse + invoke | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java` |
| Adapter 组装(B 阶段) | `src/main/java/com/superbiz/agent/harness/tool/adapter/RagToolAdapter.java` |
| 五阶段门禁 | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundary.java` |
| 双源校验 + 观察成型 | `src/main/java/com/superbiz/agent/harness/agent/ToolResultViewProjector.java` |
| 模型观察形态 | `src/main/java/com/superbiz/agent/harness/agent/ToolControlView.java` |
@@ -0,0 +1,186 @@
# Harness agent 域学习笔记:从框架 ReAct 接入到受控停止
**更新日期**:2026-08-04
**主题**:agent 域完整链路——装配(Factory)/ 双拦截器(Model/Tool)/ 循环外壳(UseCase)/ 受控停止 / 双视图投影
**配套**:[tool 域代码学习笔记](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)(工具链)、[progress 代码学习笔记](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md)(Tool 拦截器五道门)
## 1. 定位:框架 ReAct 接入层(粘合点)
```text
框架(spring-ai-alibaba ReactAgent):负责 ReAct 多轮(模型 ↔ tool_call)
Harness:不复制 loop,只通过 Interceptor 卡住【每次消耗】
→ Model Interceptor:每次模型调用(预算/审计/Token)
→ Tool Interceptor:每次工具调用(五道门)
原则:拦截器是挂点,不是 loop 实现——Harness 不需要知道框架内部怎么循环
```
## 2. 装配图(DiagnosisAgentFactory——粘合点)
```mermaid
flowchart LR
subgraph 框架能力
M["ChatModel"]
T["tools<br/>evidenceTools.callbacks()"]
L["ReactAgent 循环"]
end
subgraph Harness 控制面
I1["HarnessModelInterceptor<br/>预算+Token 审计"]
I2["HarnessToolInterceptor<br/>五道门+投影"]
H["Hooks<br/>agent_step 落库"]
O["outputSchema<br/>DiagnosisDraft(conclusion 可 null)"]
end
M --> L
T --> L
L --> I1
L --> I2
L --> H
L --> O
```
**关键装配决策**:
```text
.parallelToolExecution(false) ← 串行工具:预算与 step 绑定可解释
.returnReasoningContents(true) ← 推理内容返回
.releaseThread(true)
每次 run 新建 Agent(create(context))——拦截器持有 RunContext,不可跨 run 复用
```
## 3. HarnessModelInterceptor(模型拦截器)
```text
interceptModel(request, handler):
core.beforeModelCall(context) ← 预算门(模型调用前扣预算)
call = auditor.begin(...) ← 审计开始(Token 记账)
response = handler.call(request) ← 框架实际调用
recordUsage(call, response) ← 记 prompt/completion tokens
core.checkActive(context) ← 终态检查(预算耗尽在此打断)
return response
异常:记 0 token + 上抛(不吞)
```
**三个动作**:预算(beforeModelCall)→ 记账(auditor.begin/recordUsage)→ 终态(checkActive)——每次模型调用都被 Harness 卡住一次。Usage 字段 null/负数安全兜底(nonNegative)。
## 4. HarnessToolInterceptor(工具拦截器,已深学)
五道门(progress 会话已沉淀):证据工具必炸 handler → 拦截器唯一执行路径 → 边界投影 → 审计落库 → 返回。本会话只补装配视角:`interceptors` 列表里第二个,构造时注入 context + evidenceTools + objectMapper + traceRecorder。
## 5. DiagnosisAgentUseCase(循环外壳)
### 5.1 执行流程
```text
execute(context, input):
checkActive → 输入限制(query/previous_turn/input 字节)
reserveRunBytes(input) ← 输入也占 Run 预算
RunnableConfig.metadata 挂 RunContext ← 显式传递(避免隐式 ThreadLocal)
agent = factory.create(context) ← 每次 run 新建
response = agent.call(inputJson, config) ← ★ 框架跑完整个 ReAct 循环
output → 字节限制 → reserveRunBytes(draft) → parse DiagnosisDraft
→ completed(draft) 或 受控停止
```
### 5.2 受控停止(controlledExecution)——从异常栈捞回可控信号转正常返回值
```text
① DiagnosisCollectionStoppedException(信息饱和后仍强 tool)
→ stopped(stopReason) ← 收集该停(draft=null)
② RunAbortedException + BUDGET_EXHAUSTED
→ markBudgetLimitReached + stopped(BUDGET_LIMIT_REACHED)
③ 其他 RunAborted(取消/超时/内部失败终态)→ 原样再抛
← 留给 Application 写 CANCELLED/FAILED(不降级为正常停止)
④ BudgetExceededException 或 lifecycle 已 BUDGET_EXHAUSTED → 预算 stopped
⑤ 都识别不了 → 包装 DiagnosisAgentOutputException(Agent 执行失败)
```
**关键**:不是笼统「业务异常 → 正常」——**只识别 Harness 约定的可控信号**(沿 cause 链找,因框架可能再包一层);取消/超时必须上抛(诚实终态)。
### 5.3 与 recoverInvalidDraft 的分工
```text
controlledExecution:loop 被预算/收敛打断(往往还没有合法 draft)→ stopped
recoverInvalidDraft:loop 跑完了,但输出不是合法 DiagnosisDraft → 恢复/重试
```
### 5.4 输出解析(严格)
```text
draftReader = FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS(严格模式)
JsonParseException → INVALID_JSON / SchemaInvalid → SCHEMA_INVALID(分类错误码)
空输出 → EMPTY_DRAFT(分类错误码)
```
## 6. 双视图投影(ToolResultViewProjector)
```text
modelObservation() → 模型观察:只含该工具的内容字段
lookup_knowledge → scope.query + evidence + relevance_level
query_logs → source_kind + scope + patterns + events
query_mysql → scope + columns + rows
+ stop_required/reason(需要停止时附加)
controlView() → 控制视图:evidence_status / relevance_level / returned_count / truncated
(Harness 控制面读,模型看不到)
分工:模型看到「内容」,Harness 看到「控制信息」(判级/截断/进度消费)
```
## 7. 定义层小件
| 类 | 作用 |
|---|---|
| `DiagnosisAgentInput` | query + previous_turn(query 必填) |
| `DiagnosisAgentLimits` | maxQuery/PreviousTurn/Input/DraftBytes 四类字节上限 |
| `DiagnosisAgentPrompt` | classpath 加载系统提示词(prompts/diagnosis-agent-prompt.md) |
| `DiagnosisAgentExecution` | completed(draft, progress) / stopped(progress, stopReason) 双形态 |
| `DiagnosisDraftOutputSchema` | BeanOutputConverter postProcess:conclusion 允许 object/null(无结论合法) |
| `EvidenceToolInvoker` | 函数式:RunContext + toolCallId + arguments → ToolBoundaryResult |
| `ParsedAgentToolCall` | 解析出的工具调用(previousObservation + businessInput + arguments) |
## 8. 关键设计点(面试)
| 设计 | 为什么 |
|---|---|
| **不复制 loop** | 框架 ReAct 是标准能力;Harness 用拦截器挂在每次消耗点,不需要知道框架内部怎么循环 |
| 每次 run 新建 Agent | 拦截器持有 RunContext——Agent 与 run 绑定,防跨 run 串状态 |
| RunContext 显式传(metadata) | 避免隐式 ThreadLocal(框架线程池/异步下 ThreadLocal 不可靠) |
| 串行工具(parallel=false) | 预算与 step 绑定可解释(并行会让「哪一步花多少钱」不可审计) |
| 受控停止只认约定信号 | 取消/超时绝不降级为正常停止(诚实终态) |
| conclusion 允许 null | 无结论也是合法 Draft(FALLBACK 路径) |
| 双视图 | 模型观察 vs 控制视图分离——控制信息(判级/截断)不进模型上下文 |
| 输出严格解析 | FAIL_ON_UNKNOWN/TRAILING——防止模型输出混入意外字段 |
## 9. 易错点
| 易错 | 正确 |
|---|---|
| Harness 自己实现 Agent loop | 框架跑 loop,拦截器挂消耗点(不复制 loop) |
| 任何异常都转 stopped | 只认约定信号(CollectionStopped/预算);取消/超时原样上抛 |
| Agent 复用 | 每次 run 新建(拦截器绑定 RunContext) |
| ThreadLocal 传 context | RunnableConfig metadata 显式传 |
| 并行工具省时间 | 串行(预算与 step 绑定可解释) |
| 模型看到控制信息 | 双视图:模型看内容,Harness 看控制 |
| 输出宽容解析 | FAIL_ON_UNKNOWN + FAIL_ON_TRAILING(严格模式) |
## 10. 面试话术(30 秒)
> "agent 域是框架 ReAct 的接入层:不复制 loop——spring-ai-alibaba 的 ReactAgent 负责多轮循环,Harness 通过两个拦截器卡住每次消耗:Model Interceptor(每次模型调用前 checkActive + 预算,调用后记 Token 审计)、Tool Interceptor(工具调用五道门)。装配在 DiagnosisAgentFactory,每次 run 新建 Agent(拦截器绑定 RunContext,RunnableConfig metadata 显式传递避免 ThreadLocal)。循环外壳 DiagnosisAgentUseCase 做输入/输出字节限制 + Run 预算预留,并实现受控停止——只把 Harness 约定的可控信号(信息饱和、预算耗尽)从异常栈捞回转成 stopped,取消/超时原样上抛留给 Application 写 CANCELLED/FAILED。串行工具保证预算与 step 绑定可解释。"
## 11. 代码位置索引
| 类 | 文件 |
|---|---|
| `DiagnosisAgentFactory` | `src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java` |
| `HarnessModelInterceptor` | `src/main/java/com/superbiz/agent/harness/agent/HarnessModelInterceptor.java` |
| `HarnessToolInterceptor` | `src/main/java/com/superbiz/agent/harness/agent/HarnessToolInterceptor.java` |
| `DiagnosisAgentUseCase` | `src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentUseCase.java` |
| `ToolResultViewProjector` | `src/main/java/com/superbiz/agent/harness/agent/ToolResultViewProjector.java` |
| `HarnessEvidenceTools` | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java` |
| `DiagnosisAgentExecution` | `src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentExecution.java` |
| `DiagnosisAgentOutputException` | `src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentOutputException.java` |
| 契约(DiagnosisDraft/PreviousTurn) | `src/main/java/com/superbiz/agent/harness/contract/` |
@@ -0,0 +1,166 @@
# Harness application + audit 学习笔记:从 Run 编排到可回放审计
**更新日期**:2026-08-06
**主题**:application 域(Run 全生命周期编排 + 安全落库)+ audit 域(可观测账本)——重点讲清 audit 与 trace 的设计与区别
**配套**:[证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md)、[执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md)
## 1. 一句话定位
```text
application = Run 应用所有者:创建 Run / 路由意图 / 执行分支 / 持久化 / SSE 输出
audit = 可观测账本:Trace 时序回放 + Token 对账 + 各明细审计表(metadata-only)
```
## 2. application 域:ChatApplicationUseCase 六步编排
```mermaid
flowchart TD
A["Controller → execute(request, observer)"] --> B["① 读会话上下文<br/>RoutingHistory + PreviousTurn"]
B --> C["② core.startRun<br/>创建 Run 边界"]
C --> D["③ persistStart + observer.onStarted<br/>(SSE metadata + 取消句柄 CoreRunControl)"]
D --> E["④ router.route<br/>意图路由(单次模型调用)"]
E --> F["⑤ executePath 按 intent 分叉<br/>SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS"]
F --> G["⑥ completePath + persistFinish + 返回"]
G -.异常.-> H["统一失败出口<br/>terminalOutcome + safeFailure"]
```
### 2.1 关键设计点
| 设计 | 代码事实 | 意义 |
|---|---|---|
| 取消句柄 | `observer.onStarted(new CoreRunControl(core, context))` → `core.cancel(context, CLIENT_DISCONNECTED)` | 客户端断连 → 取消广播 → 强杀 in-flight 调用 |
| 取消有原因 | `RunCancellationReason.CLIENT_DISCONNECTED` | 区分断连/用户取消,可审计 |
| sessionId 白名单 | `SAFE_ID = [A-Za-z0-9][A-Za-z0-9._-]{0,63}` | 信任边界校验 |
| 多轮记忆有界 | RoutingHistory(intent+query) + PreviousTurn(PublishedResult 有界化) | 上一轮只传安全摘要,无 tool ids / raw evidence |
| 预算终态特殊处理 | `handledBudgetTermination()` 时不二次 completeSuccess | 不破坏 first-terminal-wins |
| 统一失败出口 | terminalOutcome → CANCELLED/FAILED + ChatFailureCode(文案安全) | 不暴露内部堆栈 |
| 路由输出契约 | `OUTPUT_FIELDS = {intent}`,值必须枚举名 | 防模型夹带 |
### 2.2 持久化(JpaChatRunStore + PublishedResultPolicy)
- start / markIntent / finish(@Transactional),finish 写 outcome + safeContentJson + publishedResult;
- PublishedResultPolicy:**只有 SUCCESS 且 draft 有结论才构造 PublishedResult**;sanitize 全套有界(query 2000/结论 2000/scope 1000/limitations 10×500/文档 10);
- sourceDocuments 只收 RAG 类型证据的 document_id(去重)——MySQL/日志证据不进发布文档列表;
- PreviousTurn = sanitize 后的有界摘要(多轮记忆来源)。
## 3. audit 域:可观测账本的层次
```mermaid
flowchart LR
subgraph 写入侧["写入侧(不阻断主流程)"]
H1["HarnessAgentAuditHook<br/>每模型步 → agent_step + agent_reasoning_audit"]
H2["ToolInvocationAuditSink<br/>工具 → tool_invocation"]
H3["ModelCallAuditor<br/>Token → ledger + core + agent_step 回写"]
H4["JpaDiagnosisTraceRecorder<br/>事件 → diagnosis_trace_event"]
H5["JpaChatRunStore<br/>Run → diagnosis_run"]
end
subgraph 读取侧["读取侧(回放)"]
S["DiagnosisTraceService"]
S --> R["DiagnosisTraceResponse<br/>timeline + steps + toolInvocations + run + summary"]
end
H1 --> DB[(MySQL 各表)]
H2 --> DB
H3 --> DB
H4 --> DB
H5 --> DB
DB --> S
```
### 3.1 各落库点(谁写哪张表)
| 落库点 | 表 | 内容 |
|---|---|---|
| HarnessAgentAuditHook | agent_step | 每模型步摘要(stepIndex/耗时/token/工具计划) |
| HarnessAgentAuditHook | agent_reasoning_audit | 推理 + assistant_text 正文(受限) |
| ToolInvocationAuditSink | tool_invocation | 工具入参/输出预览/检索明细 |
| JpaDiagnosisTraceRecorder | diagnosis_trace_event | 全链路事件时序线 |
| ModelCallAuditor | agent_step.token_count | Token 回写(仅 DIAGNOSIS_AGENT) |
| JpaChatRunStore | diagnosis_run / chat_session | Run 生命周期 + 发布契约 |
### 3.2 audit 的三个核心边界(fail-safe / metadata-only / 对账)
1. **审计不阻断主流程**:三个落库点全部 try-catch + log.warn——审计挂了不能让 Run 跟着挂;
2. **metadata-only 分层**:正文只允许出现在 agent_reasoning_audit(受限)和 tool_invocation(入参/输出预览),其余全部摘要化;
3. **Token 三写闭环**:ledger 分账 → core.recordTokens(Run 预算)→ agent_step.token_count 回写——审计与预算同源可对账。
## 4. audit vs trace:设计与区别(重点)
### 4.1 核心区别:包含关系,不是并列
```text
audit = 域(可观测账本的总集合,17 个文件)
├─ ★ trace = 域内的事件回放子体系(诊断时序线)
├─ ModelCallLedger / Auditor(Token 记账)
├─ HarnessAgentAuditHook(模型步审计)
├─ ToolInvocationAuditSink(工具审计)
├─ RagLookupAuditEnricher(RAG 检索审计)
└─ RunConclusionExtractor(结论提取)
```
**常见误解修正**:trace 不是「agent 执行记录」,而是**全链路七阶段时序线**(RUN/ROUTING/AGENT/TOOL/EVIDENCE/SEMANTIC/RELEASE);audit 也不只是「tool 审计」,tool 审计只是其中一小块。
### 4.2 一张表讲清区别
| 维度 | trace(diagnosis_trace_event) | audit(各明细账本) |
|---|---|---|
| 本质 | 时序事件流(按 sequence_no 排序) | 实体化明细记录 |
| 回答 | 发生了什么、按什么顺序 | 每个细节落在哪本账上 |
| 粒度 | 每帧只带摘要 + 关联键(step_id 等) | 完整字段(入参/输出/token/耗时) |
| 结构 | 一张表、一个序列 | 多张表(agent_step/tool_invocation/...) |
| 排序保证 | sequence_no 单调(ConcurrentHashMap 分配) | step_index / id / createdAt 排序 |
| 典型查询 | `findByRunIdOrderBySequenceNoAscIdAsc` | `findByRunIdOrderByStepIndex` 等 |
| 与明细的关系 | 靠 step_id / run_id 互链,不重复存储 | 承载被关联的实体数据 |
### 4.3 真实数据对照(run 1b584a01)
```text
trace(15 帧时序线):
RUN_STARTED → ROUTING_DECISION → AGENT_MODEL_STEP×2 → TOOL_INVOCATION
→ EVIDENCE_GUARD_INITIAL → SEMANTIC_GUARD_DECISION → RELEASE_DECISION → RUN_FINISHED
audit 各账本(同 Run):
agent_step 2 行(token 2461/4415,耗时 1418/8542ms)
agent_reasoning_audit 2 行(step1 = 完整 Draft JSON)
tool_invocation 1 行(lookup_knowledge,step_id=979)
diagnosis_run outcome=SUCCESS + published_result 完整 JSON
```
**关联示例**:trace 第 7 帧 TOOL_INVOCATION 的 details 里 `step_id=979` = agent_step.id=979 = tool_invocation.step_id——时序帧与明细账本通过 id 互链。
## 5. 可回放机制(DiagnosisTraceService)
```text
GET /api/diagnosis/{sessionId}/trace?runId=xxx
→ DiagnosisTraceResponse 七块:runId / chatSession / session / run /
steps[] / toolInvocations[] / timeline[] / summary
GET /api/diagnosis/{sessionId}/trace/reasoning?runId=xxx(受限:runId 必填)
→ agent_reasoning_audit(reasoning + assistantText)
```
三级回放深度:**时间线(timeline)→ 明细(steps/toolInvocations)→ 推理(reasoning,按需受限读取)**。
回放能成立的四个保证:
1. sequence_no 单调(索引 idx_trace_event_run_sequence);
2. 事件与明细靠 step_id 互链;
3. summary 的 persisted vs returned 双计数对账(发现落库不完整);
4. 双查询入口兼容新旧会话(buildLegacyTraceResponse)。
## 6. 面试话术(30 秒)
> "application 域是 Run 的应用所有者:一次请求六步编排——建 Run 边界、意图路由(单次模型调用、输出契约严格为 {intent} 枚举)、按意图分叉执行、路径完成后写终态并持久化发布契约,异常统一走失败出口映射成安全的 ChatFailureCode;取消能力通过 SSE 句柄暴露给客户端(断连即 core.cancel)。**audit 域是可观测账本,trace 是它内部的事件回放子体系**:trace 用一张 diagnosis_trace_event 表按 sequence_no 记录全链路七阶段的事件时序线(每帧只带摘要和关联键),audit 的明细账本(agent_step / tool_invocation / agent_reasoning_audit / diagnosis_run)承载完整字段,两层通过 step_id/run_id 互链不重复存储。三个边界:审计不阻断主流程(fail-safe)、metadata-only(正文只在受限审计表)、Token 三写闭环(ledger 分账 → Run 预算 → 明细回写)。回放由 DiagnosisTraceService 按 runId 聚合排序,三级深度:时间线 → 明细 → 推理。"
## 7. 代码位置索引
| 组件 | 文件 |
|---|---|
| ChatApplicationUseCase / ChatFailureCode / ChatApplicationStatus | `src/main/java/com/superbiz/agent/harness/application/` |
| DiagnosisChatExecutor / 其他 executor | `.../application/executor/` |
| IntentRouter / IntentRouterPrompt | `.../application/routing/` |
| JpaChatRunStore / PublishedResultPolicy / RoutingHistory | `.../application/persistence/` |
| Trace 体系(Recorder/Event/Type/Status/AuditEvents) | `src/main/java/com/superbiz/agent/harness/audit/`(前半) |
| ModelCallLedger / ModelCallAuditor / HarnessAgentAuditHook / RunConclusionExtractor | `.../audit/`(后半) |
| DiagnosisTraceService / DiagnosisTraceController | `src/main/java/com/superbiz/agent/service/` + `controller/` |
| V014 建表(diagnosis_trace_event) | `src/main/resources/db/migration/V014__create_diagnosis_trace_event.sql` |
@@ -0,0 +1,105 @@
# Harness contract 状态流学习笔记:11 个状态枚举的正交全景
**更新日期**:2026-08-06
**主题**:contract 域状态枚举全景——五层状态 / 正交维度 / 纵向映射链 / 真实数据案例 / 面试讲法
**配套**:[证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md)、[application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md)
## 1. 一句话定位
**contract 状态流 = 11 个状态枚举按五层正交组织,每层回答一个独立问题;层间通过显式映射链串联——从技术终态到用户结局,从工具调用到发布裁决,全部类型化,杜绝字符串漂移。**
## 2. 五层状态全景
| 层 | 枚举 | 值 | 回答的问题 |
|---|---|---|---|
| ① 技术层(core) | `RunState` | RUNNING / SUCCESS / FAILED / CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED | Run 技术上是否还允许执行 |
| ② 证据层(tool/guard) | `InvocationStatus` | PROJECTING / READY / ERROR | 调用生命周期走到哪 |
| | `EvidenceStatus` | EVIDENCE_FOUND / NO_EVIDENCE / ERROR | 这次调用有没有拿到可引用证据 |
| | `AnalysisKind` | NORMAL / NEGATIVE_OBSERVATION | 分析条目是正向还是负向观察 |
| ③ 收集层(progress) | `DiagnosisStopReason` | INFORMATION_SATURATED / BUDGET_LIMIT_REACHED / PROTOCOL_VIOLATED | 证据收集为何受控停止 |
| | `SemanticVerdict` | SUPPORTED / UNSUPPORTED | 结论是否被证据支持 |
| ④ 发布层(release) | `ReleaseOutcome` | SUCCESS / FALLBACK / FAILED / CANCELLED | 用户侧内容结局 |
| | `FallbackType` | EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / SEMANTIC_UNAVAILABLE / BUDGET_EXHAUSTED / INSUFFICIENT_EVIDENCE / MISSING_REQUIRED_CONTEXT | 为什么降级 |
| ⑤ 协议层 + 失败层 | `ChatApplicationStatus` | ROUTING / SYSTEM_RESPONDING / KNOWLEDGE_SEARCHING / KNOWLEDGE_ANSWERING / DIAGNOSIS_RUNNING / SAFETY_VALIDATING | 当前走到哪一阶段(进行中) |
| | `SseOutcome` | SUCCESS / FALLBACK / FAILED | done 事件粗粒度结局(**未接线**) |
| | `ChatFailureCode` | ROUTING_UNAVAILABLE / SYSTEM_CHAT_UNAVAILABLE / KNOWLEDGE_UNAVAILABLE / DIAGNOSIS_UNAVAILABLE / RUN_PERSISTENCE_FAILED / RUN_CANCELLED / INTERNAL_FAILURE | 失败时给客户端的粗粒度原因 |
## 3. 正交维度(四个独立轴)
```text
轴 1:RunState(技术终态)⊥ ReleaseOutcome(用户结局)
一个 Run 技术停了,用户看到的可能是降级(有安全进展)或失败(没进展)
轴 2:InvocationStatus(调用生命周期)⊥ EvidenceStatus(证据语义)
注释原话:一个是「投影中/就绪/错误」,一个是「有没有拿到可引用证据」
NO_EVIDENCE 仍可能是 success 的工具执行(查了但空)
轴 3:ChatApplicationStatus(进度,进行中)⊥ 结局(终态)
进度回答「走到哪」,结局回答「最终给什么」
轴 4:IntentType(路由)——每次请求一个,决定走哪条分支
```
## 4. 纵向映射链(代码事实)
```text
RunState.BUDGET_EXHAUSTED ──hasObservedFacts()==true──▶ FALLBACK(INSUFFICIENT_EVIDENCE)
└──无 facts──▶ FAILED(fail closed)
RunState.CANCELLED ──▶ ReleaseOutcome.CANCELLED + ChatFailureCode.RUN_CANCELLED
内部失败 ──▶ RunState.FAILED + ReleaseOutcome.FAILED + INTERNAL_FAILURE
SemanticVerdict.SUPPORTED(guard 全过)──▶ ReleaseOutcome.SUCCESS(唯一出口)
FallbackType 任意值 ──▶ ReleaseOutcome.FALLBACK
证据层内部约束:
AnalysisKind.NORMAL.accepts(EVIDENCE_FOUND)
AnalysisKind.NEGATIVE_OBSERVATION.accepts(NO_EVIDENCE)
InvocationStatus.READY 是 EvidenceGuard 可引用前提(isReferencableBy)
EvidenceStatus.ERROR 不是证据,不能支持分析/结论(prompt 原话)
```
## 5. 真实数据案例(run 1b584a01 的状态流转)
| 阶段 | 状态值(真实 trace 佐证) |
|---|---|
| 路由 | IntentType=DIAGNOSIS(ROUTING_DECISION 事件 details) |
| Agent 执行 | RunState=RUNNING,ChatApplicationStatus: ROUTING → DIAGNOSIS_RUNNING → SAFETY_VALIDATING |
| 工具调用 | InvocationStatus: PROJECTING → READY;EvidenceStatus=EVIDENCE_FOUND |
| 分析 | AnalysisKind=NORMAL × 3(accepts EVIDENCE_FOUND) |
| 验真 | EVIDENCE_GUARD_INITIAL=PASSED(violations=0) |
| 语义裁决 | SEMANTIC_GUARD_DECISION=SUPPORTED |
| 发布 | RELEASE_DECISION=SUCCESS(= ReleaseOutcome.SUCCESS) |
| 终态 | RunState=SUCCESS |
## 6. 面试怎么讲这个状态流设计
### 6.1 叙事模板(① 动机 → ② 决策 → ③ 实现 → ④ 边界 → ⑤ 话术)
**① 动机**:Agent 系统里最容易被搞混的就是「状态」。技术停了(预算耗尽)不等于用户看到失败;查了没查到不等于系统出错;进行中不等于终态。如果所有状态塞进一个枚举,语义就糊了。
**② 决策**:**分层 + 正交 + 显式映射**——每个枚举只回答一个问题(技术/证据/收集/发布/协议各管各的),层间不隐式耦合,用明确的映射链串联。
**③ 实现**:11 个枚举五层,两个最典型的正交轴 + 一条纵向映射链(如上);全部类型化(enum/record),杜绝字符串漂移。
**④ 边界**:SSE 取消场景连接可能已断、发不出 done,所以协议层只有三态;`SseOutcome` 目前未接线(实现直接复用 ReleaseOutcome);`FallbackType.BUDGET_EXHAUSTED` 是命名债务(实际发布 INSUFFICIENT_EVIDENCE)。
**⑤ 话术(30 秒)**:
> "Harness 的状态设计核心是**分层正交**:技术终态(RunState)和用户结局(ReleaseOutcome)是两个正交轴——预算耗尽且有安全进展时用户看到 FALLBACK 降级,没进展才是 FAILED,这样同一个技术终态可以诚实映射到不同用户结局;工具调用也是两个正交轴(InvocationStatus 生命周期 vs EvidenceStatus 证据语义),查了但空结果(NO_EVIDENCE)仍是成功执行。层间是显式映射链:BUDGET_EXHAUSTED+有事实→FALLBACK,CANCELLED→CANCELLED+RUN_CANCELLED,guard 全过→SUCCESS 唯一出口。协议层(SSE)复用 ReleaseOutcome 三态并拒绝 CANCELLED(取消时连接已断),SseOutcome 是预留未启用的抽象。"
### 6.2 高频追问应对
| 追问 | 答 |
|---|---|
| RunState 和 ReleaseOutcome 有什么区别? | 技术 vs 用户:RunState 回答 Run 是否还允许执行;ReleaseOutcome 回答用户侧内容形态。BUDGET_EXHAUSTED 可以有 FALLBACK 或 FAILED 两种用户结局 |
| 为什么预算耗尽既可能 FALLBACK 又可能 FAILED? | `progress.hasObservedFacts()` 安全阀——有已验真事实才能发布降级,没有就 fail closed |
| NO_EVIDENCE 算失败吗? | 不算。NO_EVIDENCE 是成功执行但范围内无证据(查了但空),常记为信息无增益;ERROR 才是工具侧失败 |
| CANCELLED 为什么不在 SSE done 里? | 取消时客户端连接可能已断,发不出 done;所以协议层只有 SUCCESS/FALLBACK/FAILED 三态 |
| 这些状态为什么不直接用一个枚举? | 塞一个枚举语义就糊了——技术、证据、发布、协议回答的是不同问题,正交分层后各层可独立演进,映射显式可审计 |
| InvocationStatus 和 EvidenceStatus 会不会重复? | 分工明确:生命周期(投影中/就绪/错误)vs 证据语义(有没有证据);READY+NO_EVIDENCE 是完全合法的组合 |
## 7. 代码位置索引
| 组件 | 文件 |
|---|---|
| 全部状态枚举与数据契约 | `src/main/java/com/superbiz/agent/harness/contract/` |
| RunState | `.../harness/core/RunState.java` |
| DiagnosisStopReason | `.../harness/progress/DiagnosisStopReason.java` |
| ChatApplicationStatus / ChatFailureCode | `.../harness/application/` |
| SSE 会话(done 事件拒绝 CANCELLED) | `.../controller/sse/ChatSseEvent.java` |
@@ -0,0 +1,415 @@
# Harness progress 代码学习笔记:从拦截器五道门到唯一发布点
**更新日期**:2026-08-03
**主题**:Progress 子系统的代码落地——拦截器五道门、Tracker 状态机、canonical 生命周期、执行门禁、投影与发布闭环
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**设计视角**:[Harness 信息增益停止-让无证据诊断正常收敛](Harness信息增益停止-让无证据诊断正常收敛.md)(讲「为什么这样设计」)
**本文视角**:代码里怎么落地(类地图、调用链、生命周期、状态机、门禁、易错点、面试话术)
## 1. 定位:双停止机制与三方判断权
### 1.1 双停止机制(progress 存在的根本理由)
预算管「能不能花」,progress 管「值不值得继续查」。让预算充当正常停止策略,会把「当前证据不足」错误表达成「系统执行失败」——这是语义错误,不是资源问题。
| 机制 | 问的问题 | 归属 |
|---|---|---|
| RunBudget | 这次 Run 最多允许消耗多少? | core 域 |
| Progress | 继续查询是否仍可能推进当前诊断? | progress 域 |
### 1.2 三方判断权(信任边界)
```mermaid
flowchart LR
T["Tool / Projector<br/>客观结果"] -->|"evidence_status:空不空(代码判)"| H
M["Diagnosis Agent<br/>语义价值"] -->|"information_gain:有没有用(模型判)"| H
H["Harness<br/>最终停止权"] -->|"scope 重复 / 连续 NO_GAIN / 协议合规"| R["停止裁决"]
```
- **谁判空**:`EVIDENCE_FOUND / NO_EVIDENCE` 是 Harness 代码判(证据数组空不空),Projector 投影时客观计算,不需要模型。
- **谁判价值**:`GAINED / NO_GAIN` 的语义价值是模型判——非空结果「有没有用」只有结合诊断上下文才能判断。
- **谁决定停止**:Harness。模型可以主动结束(输出 Draft),但不能用继续发 Tool Call 绕过 Harness 已定的饱和状态。
**两层信任边界分开**:模型输出乱来(瞎报增益)时,Harness 手里仍有 evidenceStatus 这个与模型无关的事实层(canonical 记录、审计、验真都建立在它之上)。
## 2. 类地图与调用链
### 2.1 14 个文件分类
| 类别 | 类 | 作用 |
|---|---|---|
| 状态机核心 | `DiagnosisProgressTracker` | 记账(双计数/pending/identity)+ 停止裁决 |
| 判重 | `ToolScopeNormalizer` + `ToolScopeIdentity` | 业务输入 → 稳定 scope 指纹 |
| 投影 | `DiagnosisProgressProjector` + `DiagnosisProgressProjection` | identity → 回读 canonical → 有界快照 |
| 枚举 | `InformationGain` / `DiagnosisCollectionState` / `DiagnosisStopReason` / `ProgressProtocolViolationType` | GAINED·NO_GAIN / COLLECTING·SATURATED / 三种停止原因 / 五类违规 |
| record | `PreviousObservation` / `CompletedToolCall` / `DiagnosisProgressSnapshot` / `DiagnosisProgressSnapshotState` | 协议字段 / 完成 identity / 对外快照 / 内部快照 |
| 异常 | `ProgressProtocolViolationException` | 受控协议违规 |
### 2.2 一次 Tool Call 的完整链路
```mermaid
sequenceDiagram
participant I as HarnessToolInterceptor(per-Run)
participant ET as HarnessEvidenceTools(注册表)
participant AD as Adapter(接线员)
participant TB as ToolBoundary(门禁)
participant S as CanonicalInvocationStore(真相)
participant P as Projector(投影)
I->>ET: invoke(context, toolName, toolCallId, args)
ET->>AD: bridge 包装的 invoker
AD->>TB: boundary.execute(context, envelope, executor, projector)
TB->>S: begin(PROJECTING)
TB->>TB: executor.execute(requestJson)(backend raw)
TB->>P: projector.project(raw) → agent_result + evidenceStatus
TB->>S: markReady(READY) 或 markError(ERROR)
TB-->>AD: ToolBoundaryResult(READY/ERROR)
AD-->>I: 一路 return
I->>I: 双源交叉验证 → recordCompleted → 返回有界 observation
```
### 2.3 三层关系
**持有关系**:
```
HarnessToolInterceptor ──持有──▶ RunContext(含 Tracker)/ HarnessEvidenceTools
HarnessEvidenceTools ──持有──▶ Map<toolName, EvidenceToolInvoker>(bridge 注册表)
Adapter ──持有──▶ ToolBoundary + 具体 backend + 具体 Projector
ToolBoundary ──持有──▶ DiagnosisHarnessCore / CanonicalInvocationStore / ToolCallKeyFactory
```
**接口-实现关系**:
| 接口 | 实现 |
|---|---|
| `EvidenceToolInvoker`(@FunctionalInterface) | `HarnessEvidenceTools.fromAdapters` 的 bridge |
| `AdapterCall`(内部 @FunctionalInterface) | 三个 Adapter 的 execute 方法 |
| `ToolExecutor`(@FunctionalInterface) | Adapter 里 `ignored -> legacyExecutor.execute(query)` |
| `ToolResultProjector` | `RagResultProjector` / `QueryLogsResultProjector` / `MysqlResultProjector` |
| `CanonicalInvocationStore` | `JpaCanonicalInvocationStore` 等 |
## 3. 生命周期:单例 vs per-Run
```mermaid
flowchart TD
subgraph 应用启动(一次)
A["Spring 容器装配单例 bean"]
A --> B["DiagnosisHarnessCore / ToolBoundary / Adapter / HarnessEvidenceTools / DiagnosisAgentFactory"]
end
subgraph 每次请求(多次)
C["core.startRun() → RunContext(含 new DiagnosisProgressTracker)"]
C --> D["DiagnosisAgentFactory.create(context)"]
D --> E["new HarnessModelInterceptor / HarnessToolInterceptor(绑定本 Run context)"]
E --> F["ReactAgent.call() → ReAct loop"]
F --> G["Run 结束:拦截器/ReactAgent 变成垃圾"]
end
```
| 对象 | 生命周期 | 原因 |
|---|---|---|
| core / ToolBoundary / Adapter / EvidenceTools / AgentFactory | 单例 | 无状态,只存依赖与规则 |
| RunContext | per-Run | 状态容器(Tracker/Budget/Lifecycle) |
| 两个 Interceptor | per-Run | 持有本 Run 的 RunContext |
| ReactAgent | per-Run | 框架有状态对象(loop/memory) |
**无状态体现**:单例 bean 的字段全是构造注入的依赖/配置(创建后不变);可变状态全部外置到 RunContext 和持久化存储。方法一律以 `context` 参数显式传入——同一个 ToolBoundary 实例可并发服务多个 Run,各算各的账,**状态外置是并发正确性的硬要求**(若在 bean 里存「当前预算计数」,并发 Run 会互相覆盖)。
## 4. 拦截器五道门(代码核心)
### 4.1 门卫 + 执行者合一
证据工具的 `ToolCallback` 被**故意定义为直接抛异常**(`"Harness evidence Tools require the framework Tool interceptor"`)——如果框架绕过拦截器执行 `handler.call()`,直接爆炸。这从结构上保证:**执行不可绕过门禁,校验+执行是原子操作**。
```mermaid
flowchart LR
REQ["Tool Call 到达"] --> CHK{"evidenceTools.supports?"}
CHK -->|"非证据工具"| HANDLER["handler.call() 透传"]
CHK -->|"证据工具"| GATES["五道门(校验+执行+记账)"]
```
**为什么不在 definition 里写**(四个原因):
1. 拦截器接管执行时**根本不经过 ToolCallback**——写在 definition 里永远不会执行;
2. ToolCallback 是纯函数(input→output),**拿不到 RunContext**,调不了 `context.progress()`;
3. ToolCallback 本身就是执行,**没有「执行前」时机**——判重必须在 backend 前,只有拦截器同时拥有执行前/后两个观察点;
4. progress 逻辑横跨所有证据工具,写在每工具 callback 会**复制漂移**。
### 4.2 五道门流程
```mermaid
flowchart TD
A["① 已停止检查"] -->|"SATURATED && stopInstructionDelivered"| B["抛 DiagnosisCollectionStoppedException(硬停止)"]
A --> C["② 解析 + 协议校验"]
C -->|"违规"| D["recordProgressProtocolViolation → 可修复反馈 / 达阈值饱和"]
C -->|"评价导致饱和"| E["stopRequired(软停止)"]
C --> F["③ 判重:isDuplicate(toolName, normalizedScope)"]
F -->|"重复"| G["recordDuplicateScope(直接 NO_GAIN)→ 饱和? stopRequired : 返回 DUPLICATE_SCOPE 观察"]
F --> H["④ ToolBoundary 执行(预算/canonical/审计/checkActive)"]
H -->|"READY"| I["双源交叉验证 → recordCompleted → ⑤ 收尾(NO_EVIDENCE 立即 NO_GAIN / FOUND 挂 pending / 饱和交付 stop_required)"]
H -->|"非 READY"| J["error observation(BUDGET_EXHAUSTED 额外 markBudgetLimitReached)"]
```
### 4.3 协议三类违规(第二道门)
校验的是 **Envelope 与 Tracker pending 状态的协议关系**(不是 JSON 结构——JSON 结构在 parse 用严格反序列化做了):
| 违规 | 含义 | 判定条件 |
|---|---|---|
| `UNEXPECTED_PREVIOUS_OBSERVATION` | 没欠账却带评价 | pending == null 且 observation != null |
| `MISSING_PREVIOUS_OBSERVATION` | 欠账不还 | pending != null 且 observation == null |
| `OUT_OF_ORDER_PREVIOUS_OBSERVATION` | 还错账 | observation.toolCallId() != pendingToolCallId |
另两类(parse 阶段):`MISSING_INPUT`(缺业务输入)、`INVALID_ENVELOPE`(JSON/字段非法)。
**可修复反馈**:首次违规(未达阈值)返回 `repair_required:true` 观察——带 `violation_type` / `missing_field` / `expected_previous_tool_call_id` / `allowed_information_gain` / `instruction`,让模型下一轮自愈;**连续**违规达独立阈值才饱和(PROGRESS_PROTOCOL_VIOLATED)。
### 4.4 双源交叉验证(第四道门,READY 后)
```text
源1:result.evidenceStatus() ← Projector 投影时计算并携带的声明值
源2:controlView.evidenceStatus() ← 从 agent_result 内容里解析 "evidence_status" 字段
比较:一致 ? 通过 : OBSERVATION_CONTRACT_MISMATCH 拒绝
```
本质是「自洽性防线」:同一状态被两处描述(结果对象字段 vs 内容 JSON 字段),必须一致——防止投影 bug / 数据损坏 / 构造不一致导致 progress 基于错误状态决策(如声明 FOUND 但内容空 → 挂 pending 等评价不存在的证据;声明 NO_EVIDENCE 但内容有证据 → 误记 NO_GAIN)。
### 4.5 三副 observation 面孔
| 面孔 | 触发 | 关键字段 |
|---|---|---|
| 正常执行结果 | READY 且未饱和 | 有界观察 + 可选 `stop_required` |
| `STOP_REQUIRED` | 饱和后交付一次 | `stop_required:true` + `reason` |
| 可修复协议错误 | 协议违规未达阈值 | `repair_required:true` + violation_type/missing_field/expected id/instruction |
## 5. Tracker 状态机
### 5.1 11 个字段
| 字段 | 含义 |
|---|---|
| `stopAfterConsecutiveNoGain` | 连续 NO_GAIN 阈值(默认 2,yml 可配) |
| `stopAfterConsecutiveProgressProtocolViolations` | 连续协议违规阈值(默认 2) |
| `completedScopes` | `toolName + normalizedScope` 去重集合 |
| `completedToolCalls` | 已完成调用 identity 列表(无 payload) |
| `consecutiveNoGain` | 连续无增益计数(GAINED 清零) |
| `consecutiveProgressProtocolViolations` | 连续协议违规计数(合法评价清零) |
| `collectionState` | `COLLECTING` / `SATURATED` |
| `stopReason` | `INFORMATION_SATURATED` / `BUDGET_LIMIT_REACHED` / `PROGRESS_PROTOCOL_VIOLATED` |
| `pendingToolCallId` | 待评价调用 ID(同一时刻最多一个) |
| `stopInstructionDelivered` | STOP_REQUIRED 是否已交付(只一次) |
### 5.2 两条独立计数(重点:协议违规 ≠ NO_GAIN)
```text
NO_GAIN 路径(applyGain)—— backend 执行了但没增益:
入口:applyPreviousObservation(NO_GAIN) / recordDuplicateScope() / recordCompleted(NO_EVIDENCE)
阈值:stopAfterConsecutiveNoGain(2) → SATURATED + INFORMATION_SATURATED
GAINED 清零连续计数
协议违规路径(recordProgressProtocolViolation)—— backend 根本没执行:
入口:拦截器 catch 分支
阈值:stopAfterConsecutiveProgressProtocolViolations(2) → SATURATED + PROGRESS_PROTOCOL_VIOLATED
一次合法评价(applyPreviousObservation 通过)清零
```
**为什么分开**:`NO_GAIN` 表示「Tool 执行了但没推进诊断」;协议错误表示「模型没守契约,Tool 根本没执行」。混淆会让真实空转无法归因(真实 E2E:9 次协议拒绝 + 13 轮模型调用空转)。
### 5.3 pending 协议(锚点)
```mermaid
flowchart LR
A["recordCompleted(EVIDENCE_FOUND)"] --> B["pendingToolCallId = toolCallId(挂账)"]
B --> C["模型下一轮回带 previous_observation"]
C --> D{"ID == pending ?"}
D -->|"是"| E["清 pending → applyGain"]
D -->|"否"| F["OUT_OF_ORDER 违规"]
```
- 只对**非空成功**结果设 pending;NO_EVIDENCE 不设(Harness 已自己判 NO_GAIN)。
- pending 是「唯一欠账」——保证协议逐轮、不乱序、不重评。
- 模型读完 observation 直接输出 Draft → pending 不消费也合法(不需要评价最后一轮)。
### 5.4 收集状态机
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED / consecutiveNoGain=0
COLLECTING --> COLLECTING: NO_GAIN 未达阈值
COLLECTING --> SATURATED: NO_GAIN 达阈值 / 协议违规达阈值
SATURATED --> SATURATED: claimStopInstruction 交付一次(软停止)
SATURATED --> [*]: 模型再请求 Tool → DiagnosisCollectionStoppedException(硬停止)
```
**软/硬停止两段式**(易错点:stopReason 设置 ≠ 软停止):
```text
饱和瞬间 → stopReason 已设置(状态事实)
软停止 = 第一次交付 stop_required 观察(claimStopInstruction 返回 true,流程不中断)
→ 给模型合法输出 Draft 的机会
硬停止 = 模型无视指令再次请求 Tool → 入口检查 SATURATED && delivered
→ 抛 DiagnosisCollectionStoppedException 穿出框架 loop
```
### 5.5 为什么需要 Tracker(三层)
1. **progress 本身需要**:预算只止损,不能当正常停止策略;
2. **必须单一所有者**:「连续无增益」是跨轮次、跨入口(模型评价/代码判空/重复检测)的全局判断,分散记账会状态漂移;
3. **最小状态**:不存 payload(真相在 Canonical Store),避免第二份真相。
## 6. canonical 生命周期(tool 域衔接)
### 6.1 状态机
```mermaid
stateDiagram-v2
[不存在] --> PROJECTING: store.begin(preflight+预算通过后)
PROJECTING --> READY: store.markReady(backend 成功+投影成功)
PROJECTING --> ERROR: store.markError(任何异常)
note right of PROJECTING: 仅身份+request+startedAt
note right of READY: raw + agent_result + evidence + completedAt
note right of ERROR: raw + error_code + completedAt(禁 agent_result)
```
### 6.2 为什么分 begin/markReady/markError 三段
| 动机 | 说明 |
|---|---|
| 崩溃恢复 | backend 执行可能耗时数秒,一次写入会丢「已开始」的事实;begin 先留痕(PROJECTING = WAL) |
| 审计耗时 | 需要 startedAt(begin 记)和 completedAt(迁移记)算调用耗时 |
| 状态合法 | 每种状态只允许合法字段组合,由构造校验兜底 |
| 防篡改 | record 不可变,迁移生成新实例——READY 一旦写入不可改(证据可验真的前提) |
| 幂等 | begin 抛 `DuplicateInvocationException`(同 key 重复 begin 拒绝)→ `DUPLICATE_TOOL_CALL` |
**两类失败不落库**:requestBytes 超限、preflight 非法、重复 begin——都发生在 begin 之前,canonical 里**无记录**。
### 6.3 Store vs Tracker 分层
```text
Store(事实层):request / raw_response / agent_result / 状态 / 时间戳 —— 完整真相
Tracker(判断层):identity(引用)+ 双计数 + pending + 状态 —— 决策状态
NO_GAIN 计数不是「第二份真相」:它无法从 Store 重建
(模型评价是瞬时的、不落 Store),是独立增量状态
```
## 7. ToolBoundary 执行门禁
### 7.1 executeCanonical 五阶段
```mermaid
flowchart TD
A["阶段一:preflight + Tool 预算 + request bytes 预留 → begin(PROJECTING)"]
B["阶段二:executor.execute(requestJson)(backend raw)"]
C["阶段三:raw 大小校验 + Run bytes 预留"]
D["阶段四:projector.project(raw) → 有界 agent_result + evidenceStatus"]
E["阶段五:agent_result 校验 + Run bytes 预留 → markReady(READY)"]
A --> B --> C --> D --> E
```
**三笔 bytes 预留**:request(阶段一)/ raw(阶段三)/ agent_result(阶段五)——Run 的 bytes 预算分三个时间点消耗,任何一笔超限触发对应错误码。
### 7.2 失败路径错误码
| 失败点 | 是否已 begin | canonical | 错误码 |
|---|---|---|---|
| requestBytes 超限 / preflight 非法 / 重复 begin / Run 终态 | 否 | 无记录 | `RESULT_TOO_LARGE` / 分类错误码 / `DUPLICATE_TOOL_CALL` / `BUDGET_EXHAUSTED`·`RUN_INACTIVE` |
| executor 抛错 | 是 | ERROR | `TOOL_EXECUTION_ERROR` |
| raw 过大 / 预算 / 取消 | 是 | ERROR | `RESULT_TOO_LARGE` / `BUDGET_EXHAUSTED` / `RUN_INACTIVE` |
| 投影失败 | 是 | ERROR | `PROJECTION_ERROR` |
| agent_result 过大 / store 失败 | 是 | ERROR | `RESULT_TOO_LARGE` / `PROJECTION_ERROR` / `STORE_ERROR` |
### 7.3 checkActive 在哪
progress 拦截器流程里看不到显式 checkActive——它在 **ToolBoundary/core 层**隐式执行:`core.beforeToolCall` / `core.reserveRunBytes` 内部若 Run 已终态,抛 `RunAbortedException` → 映射为 `RUN_INACTIVE` 错误码。
## 8. 投影与发布闭环
### 8.1 停止后的数据流
```mermaid
flowchart LR
T["Tracker(identity 列表)"] --> P["DiagnosisProgressProjector"]
P --> S["CanonicalInvocationStore(回读 READY)"]
S --> P
P --> SN["ProgressSnapshot(verifiedSources + observedFacts + limitations + stopReason)"]
SN --> R["DiagnosisReleaseUseCase"]
R --> O["SUCCESS / FALLBACK(INSUFFICIENT_EVIDENCE 等)"]
```
### 8.2 Projector 有界投影
- 三重校验(`isReferencableBy` / toolCallId / toolName)通过才发布;无法验真/不可读/格式非法 → limitation,**绝不输出 raw**;
- 空结果也投影为有界事实(「该范围内未发现」)——限定范围的空查询是「已检查」的证明;
- 硬截断:`MAX_FACTS=12`、summary/scope 320 字符、source 160 字符;
- 去重:`sourceKey = type + source + scope`,`factKey = sourceKey + summary`(LinkedHashMap 保序)。
### 8.3 Release 决策树
```mermaid
flowchart TD
EX["execute(execution)"] --> D1{"draft == null?"}
D1 -->|"是"| CS["releaseControlledStop<br/>需 hasObservedFacts → INSUFFICIENT_EVIDENCE"]
D1 -->|"否"| D2{"conclusion == null?"}
D2 -->|"是"| NC["releaseNoConclusion<br/>有事实 → INSUFFICIENT_EVIDENCE<br/>missing_info → MISSING_REQUIRED_CONTEXT<br/>都没有 → fail closed"]
D2 -->|"否"| C["releaseConclusion<br/>EvidenceGuard 验引用 → repair → SemanticGuard 裁决<br/>SUPPORTED ? SUCCESS : FALLBACK"]
```
## 9. 易错点清单
| 易错 | 正确 |
|---|---|
| EVIDENCE_FOUND 累计 NO_GAIN | **NO_EVIDENCE**(空结果)才立即累计;FOUND 挂 pending 等模型评价 |
| 重复调用计入协议违规 | 重复走 **NO_GAIN** 路径(recordDuplicateScope),不是违规 |
| 软停止 = 设置 stopReason | stopReason 饱和时就设了;软停止是**交付 stop_required 观察** |
| checkActive 应该在拦截器 | 在 ToolBoundary/core 层,`RUN_INACTIVE` 错误码 |
| 拦截器直接 invoke = 绕过设计 | 是**唯一执行通道**(证据工具 handler 必炸) |
| 数据都在 Tracker | 判断层在 Tracker,**事实层在 Canonical Store** |
| 判重对比裸入参 | 对比的是**规范化指纹**(字段顺序/格式/缺省值统一后) |
| 首次协议违规直接终止 | 首次返回**可修复反馈**,连续违规才饱和 |
## 10. 面试话术合集(30 秒)
### 10.1 双停止机制
> "Harness 有两套停止机制。预算是资源门禁,管『能不能花』;Progress 是收敛门禁,管『继续查有没有价值』。核心设计是:Tool 只提供客观结果,模型负责判断语义增益,但停止权归 Harness。信息增益故意只保留 GAINED/NO_GAIN 两值,靠模型在下次 Tool Call 里用 previous_observation 回传评价;连续两次 NO_GAIN 就进入饱和,交付一次 STOP_REQUIRED 给模型合法收尾的机会,再纠缠就抛受控异常穿出框架。"
### 10.2 为什么需要 Tracker(单一所有者)
> "ProgressTracker 是 Run 内收敛控制的单一决策者:它不存证据内容,只保存最小账——已完成调用的 identity、连续 NO_GAIN 和协议违规两条计数、一个待评价的 pending ID、以及 COLLECTING/SATURATED 状态。三路输入(模型评价、代码判空、重复检测)汇入计数,GAINED 清零、NO_GAIN 累加,达阈值进入饱和,交付一次停止指令。所有方法 synchronized,是并发安全的单一所有者。"
### 10.3 拦截器为什么自己执行(必炸路径)
> "Harness 的证据工具不是『拦截后放行』——拦截器对它们既是门卫又是执行者。证据工具的 ToolCallback 被故意定义为直接抛异常,使 handler 路径成为必炸路径,从结构上保证执行不可绕过门禁;校验(判重/协议/预算)和执行(ToolBoundary/canonical)内联在同一个流程里,保证『执行前判重、执行后记录』的时机原子性,且模型拿到的永远是有界观察而不是 raw。"
### 10.4 为什么 begin/markReady/markError 三段式
> "Tool 执行是跨门禁、backend IO、校验、投影多个不可靠阶段的流程,所以 canonical 记录用 begin → markReady/markError 的追加式状态机:begin 先落 PROJECTING 保证已开始的调用必有痕迹,backend 成功且投影成功才 markReady 定案为 READY,任何异常 markError 收尾;三个状态各校验合法字段组合,record 不可变保证一旦写入不可篡改——这样崩溃可恢复、审计可对账(startedAt/completedAt 算耗时)、证据可验真(READY 不可造假),重复 begin 还能幂等拦截。"
### 10.5 谁判空谁判价值
> "空不空是客观事实,代码可判,所以 evidenceStatus 归 Harness 的 Projector;有没有用是语义判断,代码判不了(一段通用知识可能看似相关实则无用),所以 information_gain 归模型。Harness 绝不把客观状态交给模型声明,模型也绝不替 Harness 做停止裁决——两层的信任边界是分开的。"
### 10.6 无状态 bean
> "Harness 的单例 bean(core/ToolBoundary/Adapter)全部无状态:字段只有构造注入的依赖和配置,创建后不变;所有可变状态外置到每个 Run 的 RunContext(预算计数、进度 Tracker、生命周期)和持久化存储里。方法一律以 context 参数显式传入,所以同一个 bean 实例能并发服务多个 Run 而不串账——状态在参数里,不在实例里。"
## 11. 代码位置索引
| 类 | 文件 |
|---|---|
| `DiagnosisProgressTracker` | `src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressTracker.java` |
| `ToolScopeNormalizer` | `src/main/java/com/superbiz/agent/harness/progress/ToolScopeNormalizer.java` |
| `DiagnosisProgressProjector` | `src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressProjector.java` |
| `DiagnosisProgressProjection` | `src/main/java/com/superbiz/agent/harness/progress/DiagnosisProgressProjection.java` |
| progress 枚举/record/异常 | `src/main/java/com/superbiz/agent/harness/progress/`(其余 9 个文件) |
| `HarnessToolInterceptor` | `src/main/java/com/superbiz/agent/harness/agent/HarnessToolInterceptor.java` |
| `HarnessEvidenceTools` | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java` |
| `DiagnosisAgentFactory` | `src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java` |
| `RagToolAdapter` | `src/main/java/com/superbiz/agent/harness/tool/adapter/RagToolAdapter.java` |
| `ToolBoundary` | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundary.java` |
| `ToolBoundaryResult` | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryResult.java` |
| `CanonicalToolInvocation` | `src/main/java/com/superbiz/agent/harness/tool/store/CanonicalToolInvocation.java` |
| `CanonicalInvocationStore` | `src/main/java/com/superbiz/agent/harness/tool/store/CanonicalInvocationStore.java` |
| `DiagnosisReleaseUseCase` | `src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseUseCase.java` |
| 装配 | `src/main/java/com/superbiz/agent/config/HarnessChatConfiguration.java` |
@@ -0,0 +1,382 @@
# Harness tool 域代码学习笔记:工具的注册、调用与执行链路
**更新日期**:2026-08-03
**主题**:tool 域 49 个文件的完整链路——装配 → 注册 → 调用 → 执行 → 返回,拆分阶段讲,最后合并
**设计视角**:[Harness 组件全景-职责-设计原因与边界](Harness组件全景-职责-设计原因与边界.md) §7 Tool
**代码视角**:[Harness progress 代码学习笔记](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md)(progress 域衔接,本笔记是 tool 域)
## 1. 定位:tool 域管什么
**职责**:工具如何安全执行、保存真相并只暴露必要内容。
| 问题 | 不解决会怎样 | 催生的层 |
|---|---|---|
| 每个 Adapter 自己写授权/预算/审计 → 漂移 | 三个工具三种行为 | **Boundary**(统一门禁) |
| raw 结果直接给模型 | 敏感数据泄露、超大响应、无结构 | **Projector**(有界投影) |
| 模型可能编造证据 | 结论无法验真、审计黑洞 | **Store**(canonical 真相) |
| MySQL 查询不可控 | 写库、删库、危险 SQL | **MySQL 沙箱**(只读红线) |
**49 文件分 6 组**:
| 组 | 数量 | 角色 |
|---|---|---|
| Contract | 18 | 跨层类型化语言(Call/Request/Result) |
| Boundary | 7 | 统一门禁(ToolBoundary + 配套) |
| Projection | 3 | raw → 有界 agent 契约 |
| Store | 9 | canonical 真相持久化 |
| Adapter | 3 | 接线(boundary + 后端 + 投影器) |
| MySQL 沙箱 | 9 | 只读执行 + fail-closed 校验 |
**核心设计**:几乎不用继承——用「接口 + 组合 + 函数式接口」三件套解耦。
---
## 2. 阶段一:装配(config → bean 注入链)
### 2.1 注入链
```mermaid
flowchart TD
subgraph 底层
R["RedisCanonicalInvocationStore"]
B["ToolBoundary"]
RP["RagResultProjector"]
LP["QueryLogsResultProjector"]
end
subgraph 中层
RA["RagToolAdapter"]
QA["QueryLogsToolAdapter"]
MA["MysqlToolAdapter"]
end
subgraph 顶层
ET["HarnessEvidenceTools"]
end
R --> B
B --> RA
B --> QA
B --> MA
RP --> RA
LP --> QA
ET --> RA
ET --> QA
ET --> MA
```
注入规律:所有 `@Bean` 方法参数 = 依赖注入点;**没有任何类 extends 别人**。
### 2.2 为什么不用继承
```
❌ 继承方案(没采用):
abstract class BaseToolAdapter { ... }
RagToolAdapter extends BaseToolAdapter { ... }
→ 加一个工具就得改基类,横切逻辑散落
✅ 组合方案(实际):
Adapter = ToolBoundary(门禁) + 后端(执行) + Projector(投影)
↑ 构造注入持有引用,不是继承
→ 每个 Adapter 独立组装,改一个不影响其他
```
组合的优势:
1. **ToolBoundary 对三种工具完全无感知**——只认 `ToolExecutor` / `ToolResultProjector` 两个端口,三个工具共用同一个实例;
2. **后端各不相同**(LookupKnowledgeTool / QueryLogsTools / JDBC),无法抽象成共同基类,用函数式接口适配;
3. **开闭原则**:加新工具 = 新写 Adapter + Projector + config 注册,**不动已有类**(门禁/真相/进度自动继承)。
---
## 3. 阶段二:注册(HarnessEvidenceTools 门面)
### 3.1 两个平行的注册表
```mermaid
flowchart LR
subgraph fromAdapters
RAG["ragAdapter::execute"]
LOGS["logsAdapter::execute"]
MYSQL["mysqlAdapter::execute"]
end
subgraph HarnessEvidenceTools
direction TB
CALL["callbacks(List)<br/>模型可见 Schema + 必炸"]
INV["invokers(Map)<br/>toolName → bridge 闭包"]
end
RAG -->|bridge| INV
LOGS -->|bridge| INV
MYSQL -->|bridge| INV
INV -.同一个工具名串起.-> CALL
CALL --> MODEL["模型(可见工具目录)"]
INV --> INTERCEPTOR["拦截器(执行入口)"]
```
**同一工具名字符串串起两个表**:模型从 callbacks 决定调 `lookup_knowledge` → 拦截器用同一个名字去 invokers 取执行器。
### 3.2 bridge:方法引用绑定实际调用者
```java
private static EvidenceToolInvoker bridge(String toolName, AdapterCall adapter) {
// lambda 闭包捕获 adapter 实例 + 固定的 toolName
return (context, toolCallId, arguments) -> adapter.execute(
context,
new ToolCallRequestEnvelope(
context.runId(), toolCallId, toolName, arguments, true, true));
}
```
关联链(三层绑定):
```
① config:new RagToolAdapter(boundary, mapper, projector, backend)
→ adapter 实例已组合好 boundary + projector + backend
② fromAdapters:ragAdapter::execute 是「绑定实例的方法引用」
→ bridge lambda 捕获它 —— invoker 与 Adapter 的关联在此固化
③ 构造方法:按工具名常量 put 进 invokers —— "lookup_knowledge" → 捕获了 ragAdapter 的 lambda
```
**invoker 与调用者的关联 = 方法引用绑定**:取出来直接 `adapter.execute(...)`,不需要再查表找调用者。
### 3.3 三个关键设计点
**① callbacks 的必炸保护**:
```java
FunctionToolCallback.builder(name, ignored -> {
throw new IllegalStateException(
"Harness evidence Tools require the framework Tool interceptor");
})
```
| 场景 | callback 行为 |
|---|---|
| 正常(拦截器接管) | 不执行(拦截器直接 evidenceTools.invoke) |
| 异常(某处 handler.call / 直接调) | **抛异常** → 暴露「绕过门禁」的 bug |
结构性保证:唯一能执行证据工具的路径 = 拦截器接管 → 门禁永远在线;绕过不可能静默成功(fail-fast)。
**② mysql 条件注册**:
```java
// config
boolean mysqlEnabled = 数据源配置了 jdbcUrl ? true : false;
return fromAdapters(rag, logs, mysqlEnabled ? mysql : null);
// fromAdapters
EvidenceToolInvoker mysql = mysqlAdapter == null ? null : bridge(QUERY_MYSQL, mysqlAdapter::execute);
// 构造方法里 null 也不注册 invokers / callbacks
```
没配数据源 → query_mysql 从模型视野和执行注册表**都消失**(不暴露「必死工具」)。RAG/日志后端内置,无条件注册。
**③ definition 的 typed 输入类**:
```java
definition(AgentToolContracts.LOOKUP_KNOWLEDGE, ..., RagToolCall.class)
```
输入类型 = 模型必须匹配的 Schema——`RagToolCall{previous_observation, input}`。parse 时用 `FAIL_ON_UNKNOWN_PROPERTIES + FAIL_ON_TRAILING_TOKENS` 强制匹配,**模型输出多一个字段都炸**(INVALID_ENVELOPE)。
---
## 4. 阶段三:调用(拦截器 → 注册表)
```mermaid
sequenceDiagram
participant M as 模型
participant F as 框架 ReactAgent
participant I as HarnessToolInterceptor
participant ET as HarnessEvidenceTools
M->>F: 决定调用 lookup_knowledge(输出 tool_call JSON)
F->>I: 回调 interceptToolCall(request, handler)
I->>I: supports(toolName) ? 注册检查
I->>ET: parse(toolName, arguments, mapper) → typed Envelope
ET-->>I: ParsedAgentToolCall(previous_observation + input)
I->>I: 协议校验 / 判重(不通过不执行)
I->>ET: invoke(context, toolName, toolCallId, args)
ET->>I: bridge lambda → adapter.execute
```
调用链要点:
| 点 | 说明 |
|---|---|
| **工具名是模型决定的** | `request.getToolName()` 来自模型输出,拦截器拿它查 invokers |
| **invoke 前有三道门** | supports 分流 → parse 严格契约 → 协议/判重——判重不通过不执行 |
| **envelope 现造** | bridge 里构造,`authorized=true, readOnly=true` 写死——每个进 ToolBoundary 的信封都声明「已授权 + 只读」 |
| **工具名被闭包捕获** | 即使调用方传错名字,envelope 里也是正确的工具名(防混淆) |
---
## 5. 阶段四:执行(Adapter → ToolBoundary → 后端)
### 5.1 Adapter = 接线员
```mermaid
flowchart LR
AD["Adapter.execute"] -->|"boundary.execute(context, envelope,"| TB["ToolBoundary"]
AD -->|"executor = ignored -> legacyExecutor.execute(query)"| TB
AD -->|"projector = raw -> projector.project(...)"| TB
TB -->|"executor 跑 backend"| BK["具体后端<br/>LookupKnowledgeTool / QueryLogsTools / JDBC"]
TB -->|"projector 投影"| PR["RagResultProjector / QueryLogsResultProjector / MysqlResultProjector"]
TB -->|"markReady"| ST["CanonicalInvocationStore"]
```
**两个端口**:executor(跑 backend 拿 raw)+ projector(raw → 有界脱敏契约)。**模型永远看不到 raw**——这是执行链的核心目的。
### 5.2 LegacyExecutor vs 专用 Executor
| | RAG/日志 | MySQL |
|---|---|---|
| 后端来源 | 重构前旧类(LookupKnowledgeTool / QueryLogsTools) | 全新实现(JdbcMysqlReadOnlyExecutor) |
| 端口位置 | Adapter **内部**定义 LegacyExecutor | mysql **包**里定义 MysqlReadOnlyExecutor |
| 注入方式 | `backend::lookupKnowledge` 方法引用 / lambda | 直接注入专用实现 |
| 为什么 | 复用成熟旧代码 | 新工具直接面向沙箱设计 |
```
RAG: Adapter → LegacyExecutor(Adapter内部) → LookupKnowledgeTool(旧后端)
MySQL: Adapter → MysqlReadOnlyExecutor(mysql包) → JdbcMysqlReadOnlyExecutor(新实现)
```
### 5.3 ToolBoundary 五阶段(执行门禁)
```mermaid
flowchart TD
A["① preflight + Tool 预算 + request bytes → begin(PROJECTING)"]
B["② executor.execute(requestJson) → backend raw"]
C["③ raw 大小校验 + Run bytes 预留"]
D["④ projector.project(raw) → 有界 agent_result + evidenceStatus"]
E["⑤ agent_result 校验 + bytes → markReady(READY)"]
A --> B --> C --> D --> E
```
**三笔 bytes 预留**:request(①)/ raw(③)/ agent_result(⑤)。
### 5.4 脱敏与有界(投影器)
- **日志**:sanitize 抹掉密码/token/主机/Pod/IP/PID/SQL 字面量;均匀采样 + 模式聚合;
- **MySQL**:敏感列(password/token/secret 等)单元格 → `[REDACTED]`;行数/字符/字节三重截断;
- **RAG**:chunk 级去重 + 摘录截断 + fitBudget 总字节兜底。
---
## 6. 阶段五:返回(双源校验 → 记账 → 有界观察)
```mermaid
sequenceDiagram
participant TB as ToolBoundary
participant I as HarnessToolInterceptor
participant P as DiagnosisProgressTracker
participant M as 模型
TB-->>I: ToolBoundaryResult(READY/ERROR)
I->>I: 双源交叉验证(声明值 vs 内容重算 evidence_status)
alt 不一致
I->>M: OBSERVATION_CONTRACT_MISMATCH(拒绝)
else 一致
I->>P: recordCompleted(call, evidenceStatus)
P-->>I: 快照(NO_EVIDENCE 立即 NO_GAIN / FOUND 挂 pending)
I->>I: modelObservation 加工(含 stop_required / stopReason)
I-->>M: 有界 observation(模型永远看不到 raw)
end
```
**错误处理三种形态**:
| 场景 | 处理 |
|---|---|
| Adapter 业务/参数异常 | catch → `INVALID_REQUEST`(不泄露内部细节) |
| MySQL 安全异常 | `MysqlSecurityException` 单独 catch → `INVALID_REQUEST` |
| 日志后端缺失 | `ObjectProvider.getIfAvailable()` → 返回空结果 JSON(不炸) |
---
## 7. 全链路合起来
```mermaid
sequenceDiagram
participant C as config(启动)
participant ET as HarnessEvidenceTools(单例)
participant I as 拦截器(per-Run)
participant AD as Adapter(单例)
participant TB as ToolBoundary(单例)
participant ST as CanonicalStore(单例)
participant M as 模型
rect rgb(240, 248, 255)
Note over C,ET: ① 装配(应用启动一次)
C->>C: 建 boundary / 3 个 Adapter(注入 boundary+后端+投影器)
C->>ET: fromAdapters(rag, logs, mysql?)
ET->>ET: bridge → invokers + definition → callbacks(平行,同名串起)
end
rect rgb(255, 250, 240)
Note over M,AD: ② 调用+执行(每次 Tool Call)
M->>I: 模型决定工具名 → 框架回调拦截器
I->>I: supports 分流 → parse(typed 严格契约)→ 协议/判重
I->>ET: invoke → invokers.get(名字) → bridge 闭包
ET->>AD: adapter.execute(context, envelope[现造,授权只读写死])
AD->>TB: boundary.execute(context, envelope, executor, projector)
TB->>ST: begin(PROJECTING) → executor 跑 raw → projector 投影 → markReady(READY)
TB-->>I: ToolBoundaryResult
I->>I: 双源校验 → recordCompleted → modelObservation
I-->>M: 有界 observation(无 raw)
end
```
**四阶段汇总**:
| 阶段 | 做什么 | 关键类 |
|---|---|---|
| 装配 | Spring 组合依赖(无继承) | config / Adapter / ToolBoundary |
| 注册 | bridge 成 invoker + definition 成 callback(平行同名串起) | HarnessEvidenceTools |
| 调用 | supports → parse 严格契约 → 协议/判重 → invoke | Interceptor / HarnessEvidenceTools |
| 执行+返回 | boundary 五阶段 → 投影脱敏 → canonical → 双源校验 → 记账 → 有界观察 | Adapter / ToolBoundary / Projector / Store |
---
## 8. 易错点
| 易错 | 正确 |
|---|---|
| 必炸 = 死工具 | 必炸是**保护**:模型通过拦截器正常执行,只有绕过路径才炸 |
| LegacyExecutor 是通用 executor | 它只服务于「复用旧后端」;新工具直接注入专用 Executor(如 MysqlReadOnlyExecutor) |
| callbacks 是执行器 | 它是「模型可见目录」+ 必炸占位;真执行走 invokers |
| 执行链只有 executor | 还有 **projector**(raw → 有界契约)——模型永远看不到 raw |
| MySQL 没有 tool 类 | `JdbcMysqlReadOnlyExecutor` 就是它的后端执行类,只是不叫 Tool |
| 工具注册是静态列表 | **配置驱动**:没配数据源 → query_mysql 从两表消失 |
| 返回就是 ToolBoundaryResult | 返回后还有双源校验 → recordCompleted → modelObservation |
## 9. 面试话术合集(30 秒)
### 9.1 为什么不用继承
> "tool 域刻意不用继承:ToolBoundary 通过 ToolExecutor/ToolResultProjector 两个函数式端口接收执行和投影逻辑,三个 Adapter 各自用构造注入组合 boundary + 后端 + projector,HarnessEvidenceTools 再用 bridge 把 Adapter 包成统一的 EvidenceToolInvoker 注册表。类图里没有 extends 箭头——全是 has-a(组合)和函数适配(函数式接口),扩展新工具不改任何已有类。"
### 9.2 为什么必炸保护
> "必炸保护是执行不可绕过的结构性保证:证据工具的 ToolCallback 被故意定义为直接抛异常,使 handler 路径成为死路。唯一能执行证据工具的路径就是拦截器接管——预算、canonical、进度协议、脱敏门禁永远在线;任何绕过尝试要么抛异常暴露 bug(fail-fast),要么根本不执行(fail-closed)。非证据工具不需要门禁,所以拦截器放行、callback 正常。"
### 9.3 LegacyExecutor 是什么
> "LegacyExecutor 是 Adapter 内部定义的旧后端端口:RAG 和日志是重构前就有的工具,后端实现(backend::lookupKnowledge)被方法引用注入复用,通过 Adapter 包进 Harness 门禁——『旧后端复用,新门禁外挂』。它和 ToolBoundary 的 ToolExecutor 是两层:LegacyExecutor 是具体后端怎么查,ToolExecutor 是边界统一端口,Adapter 把前者包成后者。MySQL 是全新工具,没有 legacy,直接用新写的 MysqlReadOnlyExecutor。"
### 9.4 模型为什么看不到 raw
> "执行链是两个端口:executor 跑 backend 拿 raw,projector 把 raw 投影成有界脱敏契约(截断 + 脱敏 + 冻结 Schema)。ToolBoundary 只让 READY/ERROR 离开,模型拿到的是 modelObservation 加工后的有界观察——raw 只进 canonical Store 供审计和验真,永远不进入模型上下文。"
### 9.5 新工具怎么加(开闭原则)
> "新工具按 MySQL 模板:写 Contract 三件套 + 专用执行器 + 投影器 + Adapter,config 注册。要改的只有 AgentToolContracts 常量、HarnessEvidenceTools 构造、config;不用改 ToolBoundary、canonical、拦截器——新工具自动获得预算门禁、真相记录、脱敏投影、进度收敛、证据验真全套管控。"
## 10. 代码位置索引
| 类 | 文件 |
|---|---|
| `HarnessEvidenceTools` | `src/main/java/com/superbiz/agent/harness/agent/HarnessEvidenceTools.java`(agent 包,tool 域门面) |
| `RagToolAdapter` / `QueryLogsToolAdapter` / `MysqlToolAdapter` | `.../tool/adapter/` |
| `ToolBoundary` / `ToolBoundaryResult` / `ToolCallRequestEnvelope` / `ToolExecutor` / `ToolResultProjector` / `ProjectedToolResult` | `.../tool/boundary/` |
| `RagResultProjector` / `QueryLogsResultProjector` / `ToolProjectionLimits` | `.../tool/projection/` |
| `CanonicalInvocationStore` / `RedisCanonicalInvocationStore` / `CanonicalToolInvocation` / `ToolCallKeyFactory` / `CanonicalInvocationLimits` | `.../tool/store/` |
| Contract 18 个 | `.../tool/contract/` |
| `MysqlSqlValidator` / `JdbcMysqlReadOnlyExecutor` / `MysqlResultProjector` / `MysqlDataSourceDefinition` 等 | `.../tool/mysql/` |
| 装配 | `src/main/java/com/superbiz/agent/config/HarnessChatConfiguration.java` |
@@ -0,0 +1,228 @@
# Harness 整体架构学习笔记:从装配到入口到记忆到知识库写入
> **现状说明(2026-09-29)**:RAG 模块抽离后,本文「知识库写入链路」章节描述的
> `KnowledgeIndexService` / Milvus 写入路径已删除(入库下沉 py-rag `documents:ingest`);
> 其余装配/入口/记忆章节仍现行。当前架构见
> [../../architecture/RAG知识检索架构.md](../../architecture/RAG知识检索架构.md)。
**更新日期**:2026-08-04
**主题**:整体架构五块补充(了解层面)——配置装配中心 / HTTP 入口层 / 会话系统与记忆体系 / 知识库写入链路
**配套**:九域主线笔记(core/retry/progress/tool/guard/release/agent/audit/contract 全 ✅)
## 1. 配置装配中心(整体怎么搭起来)
### 1.1 装配全景(Bean 拓扑)
```mermaid
flowchart LR
C["ChatHarnessProperties<br/>配置集中(yml)"] --> K["DiagnosisHarnessCore<br/>总闸门:超时/预算/重试/收敛"]
K --> B["ToolBoundary<br/>工具底座:canonical/门禁/投影"]
B --> A1["RagToolAdapter"]
B --> A2["QueryLogsToolAdapter"]
B --> A3["MysqlToolAdapter<br/>(可选装配)"]
A1 --> E["HarnessEvidenceTools<br/>组装注册"]
A2 --> E
A3 --> E
K --> G["GuardModelCall<br/>(守卫/修复/路由共用底座)"]
G --> SG["SemanticGuard"]
G --> ER["EvidenceRepair"]
G --> R["IntentRouter"]
E --> F["DiagnosisAgentFactory"]
F --> UC["DiagnosisAgentUseCase"]
UC --> D["DiagnosisChatExecutor"]
R --> D
R --> S["SystemChatExecutor"]
R --> KQ["KnowledgeQueryExecutor"]
D --> A["ChatApplicationUseCase<br/>应用入口"]
S --> A
KQ --> A
```
### 1.2 三层组织(话术版)
```text
① 总闸门(core):能花多少钱/跑多久/怎么重试/何时停——配置集中,改一处全局生效
② 工具底座:所有工具统一留痕(canonical)/拦截(ToolBoundary)/裁剪(投影)——行为整齐划一
③ 具体工具:RAG/Logs/MySQL 按需装配(没配数据源不装死工具)→ 组装注册给 Agent
串联:先判意图(路由)→ 走对应分支 → 全程在总闸门管辖下
一句话:边界集中、执行统一、工具可插拔
```
### 1.3 关键设计点
| 设计 | 为什么 |
|---|---|
| 单一装配入口(HarnessChatConfiguration) | 读 Bean 签名 = 读架构拓扑 |
| 一切围绕 core | 所有链路共享同一套门禁(超时/预算/重试/收敛) |
| 配置属性集中(@EnableConfigurationProperties) | 一处改全局生效,不会有的环节漏管 |
| 工具可选装配(mysqlEnabled 判断) | 没配置不装死工具;ObjectProvider 可选后端 |
| 守卫/修复/路由共用 GuardModelCall | LLM judge 模式:同一轻量模型底座 |
| Redis 存 canonical | 跨实例共享 + TTL 过期 |
| 线程池 AbortPolicy | 队列满直接拒绝(fail fast) |
## 2. HTTP 入口层(薄协议适配)
### 2.1 请求流时序
```text
POST /api/chat {Id, Question}
→ 校验 → new SseEmitter + ChatSseSession(= ChatApplicationObserver)
→ chatWorkerExecutor.execute(...) ← 异步:HTTP 线程不跑模型
→ 立即返回 200 + TEXT_EVENT_STREAM
→ worker 线程执行编排,经 session 推事件
→ 队列满 → 503(RejectedExecutionException)
```
### 2.2 SSE 状态机 + 五类事件
```text
状态机:NEW → OPEN → TERMINAL(收尾)/ DISCONNECTED(断连)
每个方法 requireState 校验顺序——防乱序推送
事件协议:
metadata → {session_id, run_id}(首推)
status → 编排进度(ROUTING / DIAGNOSIS_RUNNING / SAFETY_VALIDATING…)
content → 最终内容(content_type + payload)
failure → 失败码 + 消息
done → 终态(SUCCESS/FALLBACK/FAILED)★ CANCELLED 对外不可见
```
### 2.3 断连取消链路(贯穿到 Harness)
```text
客户端断开 → emitter.onTimeout/onError/onCompletion → session.disconnect()
→ 状态 DISCONNECTED → runControl.cancelClientDisconnect()
→ Harness 取消机制接管(checkActive / 拦截器 / 线程池 cancel)
onStarted 时若已断连:直接取消——不白跑
```
### 2.4 失败两层出口
```text
SSE 通道:ChatApplicationException → session.fail(failure + done(FAILED))
其他 RuntimeException → INTERNAL_FAILURE 通用信息(不泄露细节)
REST 通道:GlobalExceptionHandler → 404(SessionNotFound)/ 400(参数/文档/文件超限)/ 500(兜底)
→ 编排异常走 SSE failure,REST 异常走 HTTP 状态码——都不暴露内部细节
```
## 3. 会话系统与记忆体系(术语精确校准)
### 3.1 会话存储:不存历史,存「可重放的发布结果」
```text
ChatSession(chat_session 表):只存元数据(status/messagePairCount/时间戳)
——「message history is not persisted here」
DiagnosisSession:诊断快照(query/answer/selfEvaluation/feedback)
真正的历史:DiagnosisRun(每次运行一行)+ PublishedResult(JSON 落库)
```
### 3.2 PreviousTurn 注入链路(短期记忆)
```text
ChatApplicationUseCase 开头读 findPreviousTurn(sessionId)
→ 查最近 SUCCESS+DIAGNOSIS+publishedResult 非空的 Run
→ 反序列化 PublishedResult → PublishedResultPolicy 生成【有界】摘要
(limitations 10 条×500 字 / 源文档 10 个 / 字段限长——有界在生成时)
→ 传 executePath → DiagnosisChatExecutor
→ new DiagnosisAgentInput(query, previous_turn)
→ 序列化成输入 JSON → agent.call(inputJson) → 模型从输入读到
设计三决策(话术版):
诊断短流程 → 只取上一轮(更早记忆靠多轮逐层传递)
非 SUCCESS 误导 → SUCCESS 才准入(且非 SUCCESS 轮次根本没写 PublishedResult)
token 爆炸 → 有界摘要(PreviousTurnLimits)
补充:可回放——模型看有界摘要,审计看全量 JSON(两层分离)
```
### 3.3 记忆术语校准(重要认知)
```text
判定标准:记忆 = 会被【注入 prompt/上下文】的东西(不是存了什么)
PreviousTurn → 注入输入 JSON(user message)→ ✅ 短期记忆(当前会话)
lookup_knowledge → 工具调用动态获取(tool result)→ ❌ 记忆,是检索增强(RAG)
知识库 → 检索源,规模太大无法全量注入 → 归检索侧(工具检索是正确形态)
案例库 → 有结构有写入、缺检索注入 → 长期记忆的【候选原料】
运行档案 → 元数据层永不注入 → 审计数据
项目真实情况:只有短期记忆(PreviousTurn)+ 检索增强(RAG),【没有】长期记忆层
```
### 3.4 长期记忆设计路径(如果要做)
```text
筛选标准:规模可控 + 跨会话价值 + 可注入形态
案例库最符合:root_cause+solution 结构化摘要、注入 top 2-3 条、相似故障复用解法
PreviousTurn 扩展:最近 N 轮结论摘要(短期 → 中期记忆)
Feedback 偏好:用户偏好摘要
知识库不符合:全量太大 → 保持工具检索
案例 → skill 提炼(项目已实现):
6 个 SKILL.md(diagnose-mysql-connection-pool 等)
结构:Workflow / Required Evidence / Stop Conditions / Report Rules / Eval Anchor
注入:ClasspathSkillRegistry → SystemPromptTemplate → system prompt(程序性长期记忆)
情景记忆(案例)→ 程序记忆(skill)→ 常驻注入 ✅ 长期记忆的正确形态
现有缺口:单技能激活(只放行 1 个)/ 静态加载(无按 query 自动匹配)/ skill 与案例库断开
```
## 4. 知识库写入链路(RAG 写半边)
```text
上传(/api/documents/upload)
→ TextExtractorService(文本提取)
→ DocumentChunkService.chunkDocument(分块)★
→ VectorEmbeddingService(dense embedding)
→ VectorIndexService.indexDocumentChunks(写 Milvus)★
→ 检索侧(lookup_knowledge)读同一份索引
```
### 关键设计
```text
① 分块:按章节分块(非定长硬切)+ 相邻 chunk 保留 overlap(减轻边界断裂)
双条件限制:maxSize(字符)+ maxTokens(token)
② hybrid 写入:dense(应用侧 embedding → vector 字段)
+ BM25(buildSearchText → search_text 字段)
★ 关键:dense embedding 输入 与 BM25 search_text 【同源】——
同一个「增强文本」既喂 embedding 又写 BM25 字段
→ 两路召回看到完全一致的文档内容,混合检索才公平
③ 弃用:legacy MilvusServiceClient(旧 SDK)/ Spring AI VectorStore#add(无 hybrid schema)
唯一后端:MilvusHybridKnowledgeStore(Milvus SDK v2)
```
## 5. 易错点
| 易错 | 正确 |
|---|---|
| Controller 做业务编排 | 薄适配层:校验+开 SSE+异步+写回,编排在 Application |
| SSE 顺序不重要 | requireState 状态机校验——防乱序推送 |
| 取消对外可见 | Done 拒绝 CANCELLED——客户端只看到 SUCCESS/FALLBACK/FAILED |
| 知识库 = 长期记忆 | 是检索源(工具动态获取);记忆 = prompt 注入——项目无长期记忆层 |
| 案例 = 长期记忆 | 是候选原料——缺检索注入;skill 才是程序性长期记忆(已实现) |
| 工具预算在工具层 | maxToolCalls/收敛参数都在 core(总闸门) |
| 分块定长硬切 | 按章节 + overlap + 双条件限制 |
## 6. 面试话术(30 秒)
### 6.1 整体架构怎么组织
> "整个系统从下往上三层:最底层一套全局规则(超时/预算/重试/收敛,配置集中改一处全局生效);中间一层工具共用的底座(调用留痕、统一拦截、结果裁剪);上层按需装配具体工具(知识库/日志/数据库,配了才装)。最后串成应用入口——先判意图再走分支,全程在总闸门管辖下。一句话:边界集中、执行统一、工具可插拔。"
### 6.2 记忆体系
> "记忆的判定标准是会不会被注入上下文:PreviousTurn 是短期记忆(注入输入 JSON,上一轮 SUCCESS 的有界摘要);知识库是检索增强不是记忆(工具动态获取);项目没有长期记忆层——skill(案例提炼的诊断方法)注入 system prompt 是程序性长期记忆的正确形态;案例库是候选原料,缺检索注入。"
## 7. 代码位置索引
| 块 | 文件 |
|---|---|
| 装配中心 | `config/HarnessChatConfiguration.java`(+ `config/ChatHarnessProperties.java`) |
| HTTP 入口 | `controller/ChatController.java` + `controller/sse/ChatSseSession.java` / `ChatSseEvent.java` / `SseEmitterChatSink.java` |
| 异常映射 | `exception/GlobalExceptionHandler.java` |
| 会话存储 | `harness/application/persistence/JpaChatRunStore.java` / `PreviousTurnLimits.java` / `PublishedResultPolicy.java` |
| 记忆注入 | `harness/application/ChatApplicationUseCase.java` + `harness/application/executor/DiagnosisChatExecutor.java` + `harness/agent/DiagnosisAgentUseCase.java` |
| skill 机制 | `config/SkillConfig.java` + `src/main/resources/skills/*/SKILL.md` |
| 知识库写入 | `service/DocumentChunkService.java` / `VectorIndexService.java` / `VectorEmbeddingService.java` / `KnowledgeBaseInitService.java` + `controller/DocumentController.java` / `KnowledgeBaseController.java` |
@@ -0,0 +1,164 @@
# Harness 证据安全链学习笔记:从收敛控制到唯一发布点
**更新日期**:2026-08-06
**主题**:progress → guard → release 三域联动——双通道验证架构 + 状态流转全景 + 关键字段来源与使用
**配套**:[progress 代码学习笔记](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md)、[Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md)、[执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md)
## 1. 一句话定位
**证据安全链 = progress(执行期收敛控制)→ guard(验证)→ release(唯一发布点)**:
任何对外发布的内容,必须能追溯到 canonical 账本的已验证事实;任何无法证明的内容,只能以有界、诚实的 SafeFallback 降级形态出现。
## 2. 主链路图
```mermaid
flowchart LR
subgraph 执行期["Agent 执行期(core 控资源 + progress 控收敛)"]
TC["HarnessToolInterceptor<br/>工具调用完成"]
TC -->|"完整记录"| CS["Canonical Store<br/>(唯一真相源, TTL 2h)"]
TC -->|"identity"| TR["Progress Tracker<br/>(toolCallId+toolName+scope)"]
end
subgraph 停止["Agent 结束(所有出口触发投影)"]
TR -->|"回读验真"| PP["DiagnosisProgressProjector"]
PP --> PS["ProgressSnapshot<br/>(observedFacts+limitations+stopReason)"]
end
subgraph 裁决["Release 裁决(唯一发布点)"]
EX["DiagnosisAgentExecution<br/>(draft / stopped)"]
EX --> RC["releaseConclusion<br/>(有结论)"]
RC -->|"tool_call_ids"| EG["EvidenceGuard<br/>机械验引用"]
EG -->|"失败"| ER["EvidenceRepair<br/>只修引用→复查"]
EG -->|"通过"| VES["VerifiedEvidenceSnapshot"]
VES --> SG["SemanticGuard<br/>判支持度"]
SG -->|"SUPPORTED"| SUCCESS["SUCCESS<br/>(唯一出口)"]
SG -->|"UNSUPPORTED/不可用"| FB["SafeFallback 降级"]
EG -->|"仍失败"| FB
EX -->|"stopped / 无结论"| FB
PS -->|"降级原料"| FB
end
CS -.->|"回读"| EG
CS -.->|"回读"| PP
```
## 3. 双通道验证架构(核心)
两条并行的「账本背书」通道,合起来覆盖所有结局:
| | progress 快照 | guard 快照 |
|---|---|---|
| 类 | `DiagnosisProgressSnapshot` | `VerifiedEvidenceSnapshot` |
| 组装时机 | agent 结束那一刻(所有出口) | release 验引用通过后 |
| 原料 | Tracker 的调用 identity | draft 里模型写的 tool_call_ids |
| 回读者 | `DiagnosisProgressProjector` | `EvidenceGuard` |
| 需要 draft | **不需要** | **必须** |
| 服务谁 | 受控停止/无结论/非法 draft 降级 | 有结论 draft 证据链 + SemanticGuard |
| 共同点 | 都从 canonical 回读、三重校验、去重有界、绝不输出 raw | 同左 |
**设计意义**:agent 没给出合法 draft(预算打断、输出烂)时,靠 progress 快照降级;给出合法 draft 时,靠 guard 快照支撑。**任何情况下对外发布都有据可依。**
## 4. 状态流转全景(技术终态 vs 用户终态)
### 4.1 六层状态(从内到外)
| 层 | 类型 | 值 | 回答的问题 |
|---|---|---|---|
| core 生命周期 | `RunState` | RUNNING / SUCCESS / FAILED / CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED | Run 技术上是否还允许继续执行 |
| progress 收集停止 | `DiagnosisStopReason` | INFORMATION_SATURATED / BUDGET_LIMIT_REACHED / PROGRESS_PROTOCOL_VIOLATED | 证据收集为何受控停止 |
| release 发布裁决 | `ReleaseOutcome` | SUCCESS / FALLBACK / FAILED / CANCELLED | 用户侧内容结局是什么 |
| release 降级细分 | `FallbackType` | EVIDENCE_VALIDATION_FAILED / SEMANTIC_UNSUPPORTED / SEMANTIC_UNAVAILABLE / BUDGET_EXHAUSTED / INSUFFICIENT_EVIDENCE / MISSING_REQUIRED_CONTEXT | FALLBACK 为什么降级 |
| SSE 进度 | `ChatApplicationStatus` | ROUTING / DIAGNOSIS_RUNNING / SAFETY_VALIDATING ... | 当前走到哪一阶段(不是结局) |
| 对外失败码 | `ChatFailureCode` | RUN_CANCELLED / INTERNAL_FAILURE / ... | 失败时给客户端的粗粒度原因 |
### 4.2 关键映射规则(代码事实)
```text
RunState.BUDGET_EXHAUSTED + progress.hasObservedFacts()==true
→ ReleaseOutcome.FALLBACK(FallbackType.INSUFFICIENT_EVIDENCE)
RunState.BUDGET_EXHAUSTED + 无 facts
→ FAILED(fail closed:没有安全内容可发布)
RunState.CANCELLED → ReleaseOutcome.CANCELLED + ChatFailureCode.RUN_CANCELLED
内部失败(completeFailure)→ RunState.FAILED + ReleaseOutcome.FAILED + INTERNAL_FAILURE
guard 全过 → RunState.SUCCESS + ReleaseOutcome.SUCCESS(唯一出口)
预算耗尽且子路径已产出 FALLBACK → 不二次 completeSuccess
```
### 4.3 两个正交维度的区分(高频面试点)
- **RunState** 回答「Run 技术上是否还在跑、因何技术终态停下」;
- **ReleaseOutcome** 回答「用户侧内容形态:正常报告 / 安全降级 / 失败 / 取消」;
- 常见组合:`RunState=BUDGET_EXHAUSTED` 且有安全进展 → `ReleaseOutcome=FALLBACK`;无进展 → `FAILED`。
### 4.4 终态异常透传
`DiagnosisReleaseUseCase.propagateTerminal`:`RunAbortedException` / `BudgetExceededException` / `RetryFailure.CANCELLED|BUDGET_EXHAUSTED` **原样上抛**,不吞、不伪装成业务 FALLBACK——取消和预算耗尽是 Run 的技术终态事实,用户侧必须知道「被取消了」而不是「诊断结论是不足」。
## 5. 关键字段的来源与使用
### 5.1 CanonicalToolInvocation(唯一真相源,执行时落库)
| 字段 | 来源 | 使用 |
|---|---|---|
| tool_call_id + run_id | ToolBoundary 执行时生成 | key = runId + toolCallId(两个投影器都按它回读) |
| request / raw_response | 工具请求与原始返回 | 审计/验真,不外发 |
| agent_result | Projector 投影后的有界结果 | Agent 可见的唯一形态;EvidenceGuard 重读它 |
| status | PROJECTING → READY / ERROR(单向迁移) | isReferencableBy 要求 READY |
| evidence_status | FOUND / NO_EVIDENCE / ERROR | kind.accepts() 匹配、投影一致性校验 |
| error_code | 仅 ERROR 携带 | 稳定错误码 |
| started_at / completed_at | 生命周期时间戳 | TTL / 审计 |
### 5.2 DiagnosisProgressSnapshot(progress 快照,agent 结束时组装)
| 字段 | 来源 | 使用 |
|---|---|---|
| verifiedSources | Projector 回读 canonical,去重 | 降级时展示「查过哪些来源」 |
| observedFacts | 同上(≤12 条,摘要 320 字,空查询也算) | 「查了查到什么」;`hasObservedFacts()` 安全阀 |
| limitations | 无法验真/截断的诚实说明 | 降级时展示限制 |
| stopReason | Tracker 状态 | release 检查白名单后决定降级 |
### 5.3 VerifiedEvidenceSnapshot(guard 快照,验真通过后组装)
| 字段 | 来源 | 使用 |
|---|---|---|
| analyses[] | EvidenceGuard 重读 canonical agent_result 重建 | SemanticGuard.review 输入 |
| verifiedSources() | 方法去重(sourceType+source+scope) | SUCCESS 的 published_result.source_documents、FALLBACK 的来源 |
### 5.4 SafeFallback(降级载荷,release 降级出口构造)
| 字段 | 来源 | 使用 |
|---|---|---|
| type | SafeFallbackFactory 按场景 | 用户/审计区分降级原因 |
| conclusion | 恒 null | 降级绝不发布根因结论 |
| verified_sources / observed_facts | progress 或 guard 快照投影 | 保留已验证事实供用户继续排查 |
| limitations / next_steps | 工厂按场景拼装 | 诚实说明 + 下一步 |
| failure_stage | DIAGNOSIS_INPUT / DIAGNOSIS_COLLECTION / EVIDENCE_VALIDATION / SEMANTIC_VALIDATION | 定位失败阶段 |
| validation_issues | EvidenceViolation 映射 | EVIDENCE_VALIDATION_FAILED 时的违规明细 |
## 6. 设计要点(贯穿全链的规律)
1. **fail closed 贯穿每一层**:guard 验引用默认拒绝、读投影严格反序列化、release 无 facts 抛异常、SafeFallback 无事实拒绝构造——「不确定 → 默认拒绝,没查到永不伪装成结果」。
2. **负向证据被完整建模**:progress 空查询投影成事实、NEGATIVE_OBSERVATION 配 NO_EVIDENCE、降级诚实声明「查到了但不够」。
3. **canonical 唯一真相源**:链上不存在第二份工具真相;两个投影器共用同一套三重校验。
4. **全链路可审计回放**:EVIDENCE_GUARD_INITIAL/RECHECK、SEMANTIC_ATTEMPT/DECISION、EVIDENCE_REPAIR_ATTEMPT、RELEASE_DECISION 全落 trace。
5. **语义不变性**:EvidenceRepair 只修引用不修结论(prompt 锁死 + hasSameUserVisibleSemantics 校验)。
6. **命名债务**:`FallbackType.BUDGET_EXHAUSTED` 枚举保留,但预算停实际发布 `INSUFFICIENT_EVIDENCE`(注释自认)。
7. **重复验证**:同一 tool_call_id 被多条 analysis 引用会重复验 N 次(引用级独立校验的代价,账本查询便宜可接受)。
## 7. 面试话术(30 秒)
> "Harness 的证据安全链是 progress → guard → release 三段联动。**执行期**:progress 管信息增益收敛(预算归 core),工具调用实时落 canonical 账本、Tracker 只记 identity;**验证期**:agent 结束后 release 编排——有结论的 draft 先进 EvidenceGuard 机械验引用(每个 tool_call_id 从账本回读、READY 且 kind 匹配,失败则 EvidenceRepair 只修引用再验),通过后 SemanticGuard 判结论是否被证据支持;**发布期**:SUPPORTED 是唯一 SUCCESS 出口,其余全部经 SafeFallbackFactory 构造有界诚实的降级。关键设计是**双通道**:没有合法 draft 时靠 progress 快照(observedFacts)降级,有 draft 时靠 guard 快照支撑——任何情况对外发布都有据可依;以及**状态正交**:RunState 回答技术终态(预算耗尽/取消),ReleaseOutcome 回答用户结局(降级/失败),取消和预算耗尽经 propagateTerminal 原样上抛,绝不伪装成业务降级。"
## 8. 代码位置索引
| 组件 | 文件 |
|---|---|
| EvidenceGuard / EvidenceViolationCode | `src/main/java/com/superbiz/agent/harness/guard/evidence/` |
| SemanticGuard / GuardModelCall | `src/main/java/com/superbiz/agent/harness/guard/semantic/` |
| DiagnosisReleaseUseCase / EvidenceRepair / SafeFallbackFactory | `src/main/java/com/superbiz/agent/harness/release/` |
| DiagnosisProgressProjector / Tracker / Snapshot | `src/main/java/com/superbiz/agent/harness/progress/` |
| CanonicalToolInvocation / Store | `src/main/java/com/superbiz/agent/harness/tool/store/` |
| RunState | `src/main/java/com/superbiz/agent/harness/core/RunState.java` |
| DiagnosisStopReason | `src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java` |
| ReleaseOutcome / FallbackType / SafeFallback | `src/main/java/com/superbiz/agent/harness/contract/` |
| ChatApplicationStatus / ChatFailureCode | `src/main/java/com/superbiz/agent/harness/application/` |
@@ -0,0 +1,307 @@
# Harness 面试复习笔记:五步复习与白板图沉淀(详细版)
**更新日期**:2026-08-06
**主题**:面试总复习成果固化——30 秒电梯陈述 / 三张白板图 / 九域五段式面试讲法 / 六个易错点 / 追问应对大全 / 支付超时案例
**配套**:[面试速查](Harness面试速查-一张图讲清设计.md)、[设计演进](Harness设计演进-从多Agent编排到确定性控制边界.md)、各域学习笔记
## 1. 五步复习路径
```text
① 30 秒电梯陈述 + 一张图
② 默画三张白板图(主链路 / 职责迁移 / 数据三层)
③ 六个易错点
④ 2 分钟真实案例(支付超时)
⑤ 每域面试话术背诵(九域五段式)
```
## 2. 30 秒电梯陈述(详细版)
### 2.1 一句话版本
> "Harness 是包围非确定性 Agent 的确定性控制边界。Diagnosis Agent 负责提出假设、选择 Tool、解释观察并生成 Draft;Harness 负责一次 Run 的身份、deadline、预算、取消、Tool 权限和事实保管,发布前再验证引用真实性与结论支持度。它不保证 Agent 每次都找到根因,但保证执行过程有边界、失败能够收敛,并且只有可验证的内容能够发布。"
### 2.2 逐句展开(面试官追问「具体怎么做」时用)
```text
「确定性控制边界」展开为三层边界:
运行边界:RunContext(身份/截止/预算/取消)+ 唯一终态(first-terminal-wins)
事实边界:ToolBoundary(权限/只读/容量)+ canonical 真相 + 有界观察
发布边界:EvidenceGuard 验引用 → SemanticGuard 判支持度 → Release 唯一出口
```
### 2.3 三个重点(背的时候盯住)
```text
Agent 负责业务推理(出草稿,不是出报告)
Harness 负责确定性约束(真相与观察分离)
Release 决定什么可以公开(引用真实与结论支持是两个独立门禁)
```
### 2.4 不要一开始列 10 个职责域
先给一句定义 + 三句话,面试官追问「具体怎么做」再沿三张白板图展开。
## 3. 三张白板图(详细版)
### 3.1 图一:主链路(含分支,不是单轮)
```text
chat 接口
→ 创建 RunContext(core.startRun:runId/deadline/budget/cancel/lifecycle)
→ 意图识别(IntentRouter,单次模型调用,输出契约恰好 {intent} 枚举)
→ 诊断 Agent(ReAct 多轮循环)
├─ agent 决策 → ToolBoundary
│ ├─ preflight(run 匹配/授权/只读/JSON/key)→ 失败不落库
│ ├─ 预算门禁(beforeToolCall + bytes 三笔预留)
│ ├─ canonical 状态机(begin PROJECTING → READY/ERROR)
│ ├─ 执行 + Projector 投影(严格校验 + 脱敏 + 截断)
│ └─ 有界观察返回 Agent(模型永远看不到 raw)
├─ progress 判 GAINED/NO_GAIN(连续 NO_GAIN → 饱和停止)
└─ ↺ 循环直到:出草稿 或 受控停止(预算/饱和/协议违规)
→ 分支 A(有结论草稿):
EvidenceGuard 验引用(key=runId+toolCallId 查账本,READY + kind 匹配)
├─ 失败 → EvidenceRepair 只修引用(prompt 锁死 + 语义不变性)→ 复查
│ └─ 仍失败 → SafeFallback(EVIDENCE_VALIDATION_FAILED)
└─ 通过 → SemanticGuard 判支持度(隔离守卫模型)
├─ SUPPORTED → Release → SUCCESS(唯一出口)
└─ UNSUPPORTED/不可用 → SafeFallback(SEMANTIC_UNSUPPORTED/UNAVAILABLE)
→ 分支 B(无草稿/无结论):Release 凭 progress 快照 → SafeFallback(INSUFFICIENT_EVIDENCE 等)
└─ 无安全进展 → fail closed 抛异常 → FAILED
```
**讲解要点**(讲主链路时抓四个时间点):
1. Agent 运行前先建立 Run 边界;
2. Tool 调用经过 Harness,但 Tool 选择仍由 Agent 决定;
3. Agent 只看到有界观察,完整事实由系统独立保管;
4. Draft 必须经过唯一发布出口,不能直接发送给用户。
**三个词记忆**:循环(多轮 ReAct + 收敛)/ 分支(验真失败有修复、无草稿也能降级)/ 分离(真相在 canonical,Agent 只见有界观察)。
### 3.2 图二:职责迁移(推理合并,权力拆分)
**为什么迁移**:早期多 Agent(Planner/Executor/Verifier/Composer)加上 Gatekeeper、StateGraph,代价是四套 Prompt/JSON/上下文策略。后来发现四个角色**加起来正好是一次完整 ReAct**:
```text
思考 → Planner
行动观察 → Executor + Tool
自我检查 → Verifier
最终回答 → Composer
```
外层在重复实现框架已有的循环。于是收敛成单个 React Agent + 控制面拆分:
| 早期角色 | 干什么 | 现在迁移到哪 |
|---|---|---|
| Planner | 制定排查计划 | Diagnosis Agent(ReAct 思考) |
| Executor | 调用 Tool 收集证据 | Diagnosis Agent(ReAct tool 调用) |
| Gatekeeper | 机械验真证据引用 | EvidenceGuard(真理源改为 canonical store,对象改为 DiagnosisDraft) |
| Verifier | 判断 Claim 是否可信 | SemanticGuard(隔离,无 Tool 无记忆) |
| Composer | 组织最终回答 | Release(唯一发布点) |
| StateGraph | 显式状态/分支/终态 | Harness 状态分层(正交枚举 + first-terminal-wins) |
| 预算止损 | 防止空转 | Progress Control(信息增益收敛,从止损升级为正常收敛) |
**两个洞察**:
1. 代码能机械证明的事实,不交给模型判断(Gatekeeper → EvidenceGuard 的思想延续);
2. 只有拥有不同数据权限、不同工具或真正独立业务目标的角色,拆成多 Agent 才值得——把一次 ReAct 的内部步骤外置成多角色,只会放大协议成本。
**结果**:业务推理合并回单个 Agent,但安全权力拆得更清楚——Agent 没有 canonical 读取权、不能自证引用、没有发布权。
### 3.3 图三:数据三层(一份工具结果,三个职责)
```text
┌─ 第 1 层:canonical(Redis,TTL 2h,key=runId+toolCallId)────────┐
│ request + raw_response + agent_result + status + evidenceStatus │
│ 用途:EvidenceGuard 回读验真 / 短期完整真相 / 排查 │
├─ 第 2 层:Model Observation(Projector 有界投影,冻结契约)──────┤
│ 白名单 / 脱敏 / 截断,只含 Agent 下一步推理所需字段 │
│ 用途:服务模型推理(不给 raw,防上下文膨胀/prompt injection) │
├─ 第 3 层:Metadata Audit(MySQL 长期)────────────────────────────┤
│ 只存 identity/状态/耗时/token/bytes,无正文无 raw 副本 │
│ 用途:长期回放(配合 trace 时序线) │
└───────────────────────────────────────────────────────────────────┘
```
**三分字段**(一次调用):
```text
request = 我问了什么(模型入参)
raw_response = 工具回了什么(完整返回,存 canonical 不外发)
agent_result = 我能信什么 / 模型能看到什么(投影后有界,供推理 + 验真)
```
**为什么不能共用一份数据**:raw 直接给模型 → 上下文膨胀 + 敏感泄漏 + prompt injection;只存裁剪观察 → EvidenceGuard 无法独立验真;raw 永久进审计 → 制造敏感副本。三个目标冲突,所以真相、观察、元数据各存各的。
**关键事实**:MySQL 不存完整工具返回和 agent_result——`JpaToolInvocationAuditSink` 落库时只提取 `output_preview`(默认仅 status/evidence_status)和 `output_length`(字节长度)。TTL 过期后只能元数据回放。
## 4. 九域五段式面试讲法
每域固定叙事结构:**① 动机 → ② 决策 → ③ 实现 → ④ 边界 → ⑤ 话术**,外加高频追问。
### 4.1 core:执行控制
- **① 动机**:非确定性 Agent 执行时,身份归属、截止时间、预算、取消、终态必须确定——异步调用和多轮会话中,事实到底属于哪次执行?迟到结果能不能发布?
- **② 决策**:RunContext 显式传递(不用 ThreadLocal);唯一终态 first-terminal-wins。
- **③ 实现**:`checkActive` 三道闸(模型调用前/工具调用前/工具执行中逐行);取消广播(onCancel → future.cancel);deadline;RunBudget;预算耗尽/取消走终态。
- **④ 边界**:协作式取消——同步 Provider 计算未必立即停止;终态防迟到发布但不物理强杀。
- **⑤ 话术**:
> "core 管一次 Run 的确定性边界:显式 RunContext 跨线程传递(挂进 config metadata,防并发串线),first-terminal-wins 保证唯一终态——第一个写入的终态不可被迟到结果覆盖;checkActive 在模型前、工具前、工具执行中逐行检查,预算/取消/超时到点即 abort;取消是协作式的,逻辑终态和发布被保护,但同步 Provider 计算未必立即停。"
- **追问**:取消是强杀吗?(协作式,检查点中止 + 终态防迟到)RunContext 为什么显式?(跨线程 + 并发隔离)预算和 Ledger 区别?(Run 资源门禁 vs 审计账本)
### 4.2 retry:显式可计量重试
- **① 动机**:框架/Agent 自带的重试是盲目重试——同一请求无限重试、不计量、不可审计,一个不可靠的工具能把整个 Run 预算耗光。
- **② 决策**:重试权从框架收归 Harness,做成显式可计量的 attempt 循环。
- **③ 实现**:分类裁决(技术性失败可重试 / 业务性失败不重试);次数/时间/成本三重封顶;剩余超时递减(总超时耗尽不再重试);attempt 可审计。
- **④ 边界**:业务性失败不重试(重试也没用);SDK 关闭后由 Harness 全权控制。
- **⑤ 话术**:
> "重试归 Harness 因为它是成本行为:框架自带的盲目重试不可计量不可审计,Harness 做成显式 attempt 循环——技术性失败才重试、业务性失败不重试,次数/时间/成本三重封顶,每次尝试有分类有记录可审计。Agent 和 Tool 不重试,它们只负责执行,要不要再来一次由 Harness 裁决。"
- **追问**:为什么 Agent/Tool 不重试?(重试是成本裁决权,执行层只管执行)
### 4.3 progress:信息增益收敛
- **① 动机**:预算只能止损(不能继续消耗资源),不能判断「继续查是否有价值」——Agent 可能拿着通用知识、相似查询、空日志反复空转,最后撞预算。
- **② 决策**:预算之外的第二套停止机制——信息增益控制。
- **③ 实现**:GAINED/NO_GAIN 判定(结果是否推进诊断);重复检测;Tracker 双计数/pending;连续 NO_GAIN → SATURATED 饱和停止(软/硬停止);拦截器五道门。
- **④ 边界**:SATURATED 只停收集,不是 Run 终态;`READY + NO_EVIDENCE` 是成功执行但空结果,不是技术异常。
- **⑤ 话术**:
> "progress 是预算之外的第二套停止机制:预算管能不能继续消耗资源,progress 管继续查是否推进诊断。它用信息增益判定(GAINED/NO_GAIN)+ 重复检测 + 饱和停止——连续 NO_GAIN 就停,防止 Agent 拿通用知识或空日志空转;空查询(NO_EVIDENCE)也是被完整建模的负向观察,不是技术异常。"
- **追问**:Agent 为什么不会无限调用 Tool?(预算止损 + 信息增益收敛双保险)
### 4.4 tool:事实边界
- **① 动机**:工具是证据边界——能查什么、查到多少、看到什么必须封死;工具直接连数据库/检索库有破坏面。
- **② 决策**:ToolBoundary 统一执行规则 + canonical 存真相 + Projector 有界投影;数据三层分离。
- **③ 实现**:preflight 五项(run 匹配/授权/只读/JSON/key)失败不落库;预算门禁(beforeToolCall + bytes 三笔);canonical 状态机(PROJECTING→READY/ERROR);审计 best-effort;每类工具一个 Projector(严格校验 + 脱敏 + 截断)。
- **④ 边界**:不理解业务内容(投影交给 ToolResultProjector);不做信息增益判断(progress 的事);只允许 READY/ERROR 离开。
- **⑤ 话术**:
> "tool 域是证据边界:ToolBoundary 统一四项职责——preflight(run 匹配/授权/只读/JSON 合法性/key 生成,失败不落库)、预算门禁(Tool 预算 + request→raw→agent_result 三笔字节预留)、canonical 状态机(PROJECTING→READY/ERROR)、审计。执行结果分三层:完整真相存 Redis canonical(2h),有界投影给模型,长期审计只留元数据。它明确不做业务投影和信息增益判断——那是 Projector 和 progress 的事。"
- **追问**:Tool 结果为什么不直接给模型?(三层数据责任:推理/验真/留存冲突);MySQL 沙箱怎么防?(语义/连接/输出三层防线)
### 4.5 guard:证据安全链双闸
- **① 动机**:Agent 会撒谎——编造工具调用、夸大结论。不信任模型自述。
- **② 决策**:双闸分离——EvidenceGuard 机械验引用真实(规则、可审计、不调模型),SemanticGuard 隔离判结论支持度(无工具无记忆的单轮二值判断)。
- **③ 实现**:EvidenceGuard 三层校验(结构校验 → 逐条引用验真 key=runId+toolCallId 查账本 + isReferencableBy + kind 匹配 → 重读投影重建证据,20 个违规码);SemanticGuard 严格 schema(恰好 {verdict, reason})+ 预算/重试/取消全栈衔接。
- **④ 边界**:EvidenceGuard 只问引用真不真,不问语义;SemanticGuard 不能探索事实、不能改写报告。
- **⑤ 话术**:
> "防编造证据用双闸:EvidenceGuard 是机械验真——模型草稿里每个 tool_call_id 都要去 canonical 账本查到真实记录(key 绑定 runId 防跨 Run、记录必须 READY、kind 与证据语义匹配、投影内部自洽),纯规则可审计不调模型;SemanticGuard 是隔离语义审查——无工具、无记忆、单轮二值判断,只判结论是否被已验证证据支持,输出硬校验为 {verdict, reason} 两个字段。先机械后语义:引用假的直接拦,不浪费模型调用。"
- **追问**:为什么 EvidenceGuard 通过还要 SemanticGuard?(引用真实 ≠ 结论被支持:一个防编造证据,一个防夸大结论)
### 4.6 release:唯一发布点
- **① 动机**:模型输出的是未经证明的断言,不能直接当答案返回。
- **② 决策**:唯一发布点——SUCCESS 只有一条路径(验真过 + 语义支持),其余全降级 SafeFallback。
- **③ 实现**:三分支决策树(受控停止凭 progress 快照 / 无结论验引用按进展降级 / 有结论走 Evidence→Repair→Semantic 链);fail closed(无安全进展抛异常);EvidenceRepair 只修引用(prompt 锁死 + 语义不变性检查);SafeFallbackFactory 五种降级(有界/去重/诚实)。
- **④ 边界**:终态异常透传(取消/预算耗尽不伪装成业务 FALLBACK);SafeFallback conclusion 恒 null。
- **⑤ 话术**:
> "release 是唯一发布点:任何对外内容必须经过验证。它按 Draft 形态三分支——受控停止(无草稿)凭 progress 快照发布 INSUFFICIENT_EVIDENCE,且必须有已验真事实否则 fail closed;无结论只验引用按进展降级;有结论走完整链——EvidenceGuard 验引用,失败则 EvidenceRepair 只修引用(prompt 锁死只能改引用字段 + 语义不变性保证用户可见内容不变)再复查,仍失败降级 EVIDENCE_VALIDATION_FAILED;验真通过后 SemanticGuard 判支持度,SUPPORTED 是唯一 SUCCESS 出口,其余降级。所有降级走 SafeFallbackFactory:有界、去重、诚实,保留已验证事实但不发布未证明的根因。"
- **追问**:FALLBACK 算成功还是失败?(正交:可 RunState.SUCCESS + FALLBACK,是安全发布结果不是失败)
### 4.7 application:Run 应用所有者
- **① 动机**:一次请求从创建到公开结果需要编排:建 Run、路由、执行分支、持久化、SSE 输出。
- **② 决策**:ChatApplicationUseCase 六步编排,不做业务判断,不把 HTTP/SSE 细节塞 Core。
- **③ 实现**:startRun → 读会话上下文(RoutingHistory + PreviousTurn)→ 路由 → executePath 分支 → completePath + persistFinish;统一失败出口(terminalOutcome + safeFailure);取消句柄(CoreRunControl → core.cancel)。
- **④ 边界**:路由只给枚举不执行;预算耗尽的 FALLBACK 不二次 completeSuccess。
- **⑤ 话术**:
> "application 是 Run 的应用所有者:六步编排——建 Run 边界(core.startRun)、意图路由(单次模型调用、输出契约严格为 {intent} 枚举)、按意图分叉执行、路径完成后写终态并持久化发布契约,异常统一走失败出口映射成安全的 ChatFailureCode;取消能力通过 SSE 句柄暴露给客户端(断连即 core.cancel)。路由只回答走哪条分支,分支执行权在 executePath。"
- **追问**:多轮记忆怎么实现?(RoutingHistory + PreviousTurn,只传发布后的安全摘要)
### 4.8 audit:可观测账本(含 trace)
- **① 动机**:要能回放决策过程,又不永久保存敏感正文——两个目标冲突。
- **② 决策**:metadata-only + trace 时序线 + Token 对账账本;audit 是域,trace 是域内子体系。
- **③ 实现**:trace 17 种事件按 sequence_no 单调落 diagnosis_trace_event(七阶段:RUN/ROUTING/AGENT/TOOL/EVIDENCE/SEMANTIC/RELEASE);明细账本(agent_step/tool_invocation/agent_reasoning_audit/diagnosis_run)承载完整字段;Token 三写闭环(ledger 分账 → Run 预算 → agent_step 回写);DiagnosisTraceService 三级回放。
- **④ 边界**:审计不阻断主流程(fail-safe);正文只在受限审计表;TTL 过期后只能元数据回放。
- **⑤ 话术**:
> "audit 是可观测账本,trace 是它内部的事件回放子体系。trace 用一张表按 sequence_no 记录全链路七阶段的事件时序线(每帧只带摘要和关联键),明细账本(agent_step/tool_invocation/agent_reasoning_audit/diagnosis_run)承载完整字段,两层通过 step_id/run_id 互链不重复存储。三个边界:审计不阻断主流程(fail-safe)、metadata-only(正文只在受限审计表)、Token 三写闭环(ledger 分账→Run 预算→明细回写)。回放三级:时间线→明细→推理。"
- **追问**:audit 和 trace 什么关系?(包含关系:trace 是 audit 域内的时序事件流,audit 还含 ledger/工具审计/推理审计)
### 4.9 contract:状态流正交
- **① 动机**:技术停了不等于用户看到失败;查了没查到不等于系统出错——状态语义混在一个枚举里就糊了。
- **② 决策**:分层 + 正交 + 显式映射。
- **③ 实现**:11 个状态枚举五层(技术 RunState / 证据 InvocationStatus+EvidenceStatus / 收集 StopReason / 发布 ReleaseOutcome+FallbackType / 协议 ChatApplicationStatus+SseOutcome+ChatFailureCode);四个正交轴;纵向映射链。
- **④ 边界**:SSE 只有三态(取消连接已断发不出 done);SseOutcome 未接线;FallbackType.BUDGET_EXHAUSTED 命名债务。
- **⑤ 话术**:
> "状态设计核心是分层正交:RunState(技术终态)和 ReleaseOutcome(用户结局)是正交轴——预算耗尽有安全进展→FALLBACK、没进展→FAILED,同一技术终态诚实映射到不同用户结局;工具调用也是两个正交轴(InvocationStatus 生命周期 vs EvidenceStatus 证据语义),查了但空仍是成功执行。映射链:CANCELLED→CANCELLED+RUN_CANCELLED,SUPPORTED→SUCCESS 唯一出口;协议层(SSE)复用 ReleaseOutcome 三态拒绝 CANCELLED。"
- **追问**:这些状态为什么不合并成一个枚举?(不同层回答不同问题,正交后独立演进、映射显式可审计)
## 5. 六个易错点(带「为什么」)
| # | ❌ 不说 | ✅ 应说 | 为什么 |
|---|---|---|---|
| 1 | Harness 安排 Agent 执行步骤 | ReAct Agent 自己选 Tool 和下一步;Harness 只管边界 | 边界 ≠ 编排:Harness 不替 Agent 决定查什么 |
| 2 | SemanticGuard 是第二个诊断 Agent | 无 Tool、无记忆、单轮二值判断的隔离审查器 | 职责 ≠ 角色:它没有探索能力,判完就走 |
| 3 | Tool 返回 SUCCESS 就找到证据 | READY 只表示调用完成,还要看 EvidenceStatus | 生命周期 ≠ 证据:调完成功和有没有证据是两回事 |
| 4 | FALLBACK 就是 Run 失败 | Fallback 是安全发布结果,可 RunState.SUCCESS + FALLBACK | 技术 ≠ 用户:两个正交维度 |
| 5 | Redis 是长期审计库 | canonical 只存当前 Run 短期真相(TTL 2h);长期审计只留元数据 | 短期真相 ≠ 长期审计:可验真 vs 不留敏感副本 |
| 6 | 取消能立刻杀死所有模型调用 | 协作式取消:终态防迟到发布,同步 Provider 未必立即停 | 逻辑保护 ≠ 物理强杀:终态定了但线程未必立刻停 |
## 6. 高频追问应对大全
| 追问 | 回答主线 |
|---|---|
| 什么是 Harness?30 秒讲清 | 确定性控制边界:运行/事实/发布三层边界 |
| 为什么不用多 Agent? | 四角色加起来是一次 ReAct;推理合并、权力拆分 |
| 为什么 Harness 不是工作流引擎? | Agent 选择下一步,Harness 只检查边界和发布资格 |
| RunContext 为什么显式传递? | 跨线程异步链 + 并发隔离,ThreadLocal 会丢/串 |
| 取消是强杀吗? | 协作式:检查点中止 + first-terminal-wins 防迟到 |
| 预算和 Ledger 区别? | Run 资源门禁 vs 审计账本(不同域) |
| FALLBACK 算成功还是失败? | 正交:技术终态(RunState)与用户结局(ReleaseOutcome) |
| 重试为什么归 Harness? | 盲目重试不可计量;显式可计量 attempt 循环 |
| 为什么 Agent/Tool 不重试? | 重试是成本裁决权,执行层只管执行 |
| 如何防止 Agent 编造证据? | framework Tool ID + canonical store + EvidenceGuard |
| 为什么双闸? | 引用真实(机械)≠ 结论被支持(语义) |
| Tool 结果为什么不直接给模型? | 真相/观察/审计三个数据责任冲突 |
| Agent 为什么不会无限调 Tool? | 预算止损 + 信息增益收敛双保险 |
| Tool 报错是不是 Run 就失败? | 局部失败先看是否可继续及是否已有安全进展 |
| 如何回放决策? | metadata audit + trace 时序线 + 三级回放 |
| 当前还有什么限制? | 语义去重、TTL、同步取消、SemanticGuard 不确定性、阈值校准 |
## 7. 支付超时案例(2 分钟完整版)
> "用户要求诊断支付服务超时。Application 先创建独立 Run,为 Router、Agent、Tool、Guard 共享同一套 deadline、预算和取消能力。Diagnosis Agent 自主调用知识库和日志 Tool;ToolBoundary 执行调用并把完整事实保存为当前 Run 的 canonical invocation,只把有界 Observation 返回给 Agent。Agent 根据两个 Tool 结果生成 Draft,但 Draft 没有直接发给用户。EvidenceGuard 回读 canonical store 后发现引用无法完成真实性校验,因此 Release 没有继续让模型润色或猜测,而是发布 EVIDENCE_VALIDATION_FAILED SafeFallback。最终数据库记录请求处理成功、ReleaseOutcome 为 FALLBACK,SSE 也完整结束,但未经验证的根因没有离开系统。"
**三个不等于**:Tool READY ≠ 引用已验真 / 引用已验真 ≠ 结论被支持 / Agent 生成 Draft ≠ 报告允许发布。
## 8. 复习中纠正的认知清单(最容易踩的坑)
| 错误认知 | 纠正为 |
|---|---|
| Harness 顶层所以控制 retry/progress | 不只是位置——重试是成本行为必须可计量封顶;progress 解决「预算不能判断价值」 |
| RunContext 因为「回调」显式传 | 跨线程异步链 + 并发隔离;显式挂 config metadata |
| ToolBoundary 管 token/收敛/重试次数 | 那些归 core/retry/progress;ToolBoundary 只四项(preflight/预算/状态机/审计) |
| 工具失败抛异常 | ToolBoundary 转 ERROR 状态 + 错误观察,Agent 可继续换工具;Run 是否失败看 hasObservedFacts |
| FALLBACK 可能因超时 | 超时(TIMED_OUT)通常走 FAILED;FALLBACK 前提是有已验证事实 |
| agent_result 也存 MySQL | 不存——MySQL 只留 output_preview + output_length;完整 agent_result 在 Redis canonical(2h) |
| 注入 skill/知识域给 agent | 注入的是 query + PreviousTurn + 系统 prompt;知识靠工具主动查 |
| 非法 draft 由 release 捕捉 | 反序列化在 agent 出口(recoverInvalidDraft);release 只做验证+裁决 |
| preflight 失败也落库 | 失败不落库(errorAndNoRecord)——只有 preflight 全过才写 PROJECTING |
| 多 Agent 一定不好 | 只有不同数据权限/独立业务目标的角色才值得拆;拆 ReAct 内部步骤只会放大协议成本 |
## 9. 面试前一天 Checklist
```text
□ 30 秒电梯陈述背熟(§2.1),三个重点不丢(§2.3)
□ 默画三张白板图(§3):主链路含分支 / 职责迁移对照 / 数据三层
□ 六个易错点扫一遍(§5)——重点看「为什么」列
□ 九域话术:挑 3 个最可能被追问的(guard/release/contract)背熟
□ 2 分钟支付超时案例 + 三个不等于(§7)
□ 过一遍纠正认知清单(§8)——这些是踩过的坑
□ 读一遍面试速查 §7 追问表,心里有数
```
## 10. 代码位置索引
| 组件 | 文件 |
|---|---|
| ToolBoundary(preflight/预算/状态机/审计) | `src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundary.java` |
| JpaToolInvocationAuditSink(output_preview 落库) | `.../audit/JpaToolInvocationAuditSink.java` |
| DiagnosisAgentUseCase(RunContext 挂 config metadata) | `.../agent/DiagnosisAgentUseCase.java` |
| EvidenceGuard / SemanticGuard | `.../guard/evidence/` + `.../guard/semantic/` |
| DiagnosisReleaseUseCase / EvidenceRepair / SafeFallbackFactory | `.../release/` |
| DiagnosisProgressProjector / Tracker / Snapshot | `.../progress/` |
| ChatApplicationUseCase / IntentRouter | `.../application/` |
| DiagnosisTraceService / DiagnosisTraceController | `.../service/` + `.../controller/` |
| 状态枚举(RunState/ReleaseOutcome/FallbackType/...) | `.../contract/` + `.../core/RunState.java` |
@@ -0,0 +1,280 @@
# Harness Tool 双视图:从原始结果到可验证证据
**更新日期**:2026-07-29
**主题**:ToolBoundary、Canonical Invocation、Harness Control View 与 Agent Observation
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
## 1. 要解决的不是 Tool 调用,而是 Tool 结果的所有权
Agent 调用 Tool 后,最直接的实现是把 backend 返回的 JSON 原样放进模型上下文。这在 Demo 中可以工作,但进入可验证的诊断系统后会出现一个根本矛盾:同一份结果需要同时服务推理、控制、验真和审计,而这些消费者需要的数据范围完全不同。
以日志查询为例:
- Agent 需要少量匹配事件、模式、实际时间范围和是否截断;
- Harness 需要返回数量、规范化 scope、重复身份和客观证据状态;
- EvidenceGuard 需要证明这份结果确实来自当前 Run 的某次 READY 调用;
- 审计需要 Tool 名、调用 ID、状态、耗时和字节数;
- 安全边界又要求密码、Token、主机、IP、SQL 字面量和无限日志正文不能进入模型或长期审计。
如果只保留一份 JSON,只能在两个错误方向中选择:要么信息过多导致泄露和上下文膨胀,要么信息过少导致后续无法验真。
因此这项设计的核心不是“增加一个 Redis Store”,而是重新定义 Tool 数据所有权:
> backend raw 属于 Harness;模型只能获得为推理目的生成的有界观察;长期审计只保留允许运营保存的元数据。
## 2. 三个消费者,三种数据责任
虽然实现中常称为“Tool 双视图”,完整的数据分层实际上包含三种用途:
| 数据形态 | 消费者 | 解决的问题 | 生命周期 |
|---|---|---|---|
| Canonical Invocation | ToolBoundary、EvidenceGuard、ProgressProjector | 这次 Tool 真实执行了什么、属于哪个 Run、能否引用 | Redis 短 TTL |
| Harness Control View / Agent Observation | Harness / Diagnosis Agent | 是否重复、是否为空、模型下一步需要看到什么 | 当前 Run / 模型上下文 |
| Durable Audit | Trace、运营排障 | 何时调用、状态、耗时、大小、关联 Step | MySQL 长期 metadata |
所谓“双视图”,特指 canonical 标准结果被进一步分成:
1. Harness 使用的 Control View;
2. 模型使用的 Agent Observation。
Durable Audit 是第三个持久化面,但它不是 Tool 内容视图,不参与推理或证据验真。
```mermaid
flowchart LR
A["Typed Tool Request"] --> B["ToolBoundary"]
B --> C["Backend Raw Response"]
C --> D["Tool-specific Projector"]
D --> E["Canonical agent_result"]
B --> F["Redis Canonical Invocation<br/>request + raw + agent_result"]
E --> F
E --> G["Harness Control View<br/>count / status / scope"]
E --> H["Agent Observation<br/>白名单、有界、脱敏"]
B --> I["Durable Audit<br/>identity / status / latency / bytes"]
G --> J["重复检测、NO_GAIN、停止"]
H --> K["Diagnosis Agent Context"]
F --> L["EvidenceGuard / ProgressSnapshot"]
```
## 3. 为什么不能让 Agent Observation 充当真相源
Agent Observation 是为了控制上下文而生成的投影,它可能:
- 只保留前 N 条结果;
- 截断单条文本;
- 对敏感列和日志字段做脱敏;
- 把大量事件聚合成模式;
- 只暴露粗粒度相关度,不暴露检索轨迹和原始分数。
这意味着它适合帮助模型推理,却不适合作为“后台真实返回”的完整证明。如果 EvidenceGuard 反过来验证 Agent 自己收到的 observation,就相当于用被审查对象提供的摘要证明其自身真实性。
Canonical Invocation 解决的正是这个独立性问题。它以 exact `runId + tool_call_id` 建立记录,并保存:
```text
tool_call_id
run_id
tool_name
request
raw_response
agent_result
invocation status
evidence status
error code
started_at / completed_at
```
EvidenceGuard 不信任 Draft 中的引用,也不从模型历史反推 Tool 结果,而是重新按当前 Run 构造 key,读取 canonical record 并验证状态和投影内容。
## 4. ToolBoundary 为什么必须是统一入口
RAG、日志和 MySQL 的业务执行方式不同,但以下控制规则完全相同:
- Run 必须仍然 active;
- envelope 的 runId 必须等于当前 Run;
- `tool_call_id` 必须合法且不能重复;
- Tool 必须已授权并声明只读;
- 调用前必须预占 Tool 预算和 request bytes;
- raw response 和 agent result 必须分别检查容量;
- canonical 状态只能按合法路径迁移;
- 对 Agent 只返回稳定错误码;
- durable audit 失败不能改变已经得到的 Tool 结果。
如果把这些逻辑复制到三个 Adapter,任何新增 Tool 都可能漏掉其中一项。因此统一由 `ToolBoundary` 编排一次调用,具体 Adapter 只负责 typed request、backend 和 projector 的连接。
```mermaid
sequenceDiagram
participant I as Tool Interceptor
participant B as ToolBoundary
participant C as Harness Core
participant S as Canonical Store
participant T as Backend
participant P as Projector
participant A as Audit Sink
I->>B: RunContext + ToolCallRequestEnvelope
B->>B: run / id / authorization / readonly / JSON preflight
B->>C: reserve Tool call + request bytes
B->>S: begin PROJECTING
B->>T: execute typed request
T-->>B: raw response
B->>C: reserve raw bytes
B->>P: project raw response
P-->>B: bounded agent_result + evidence_status
B->>C: reserve projection bytes
B->>S: mark READY
B-->>I: ToolBoundaryResult
B-->>A: best-effort metadata audit
```
错误发生时,已经创建的 canonical record 会尽力迁移到 ERROR;如果连 Store 都不可用,则向上只返回 `STORE_ERROR` 等稳定码,不把 Redis 或 backend 异常正文交给模型。
## 5. 两套状态为什么不能合并
Tool 调用同时有两个正交维度:
| 维度 | 状态 | 回答的问题 |
|---|---|---|
| Invocation lifecycle | `PROJECTING / READY / ERROR` | 这次调用是否完成并形成了可引用记录 |
| Evidence semantics | `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | 客观结果中是否存在候选证据 |
例如日志查询成功返回 0 条:
```text
InvocationStatus = READY
EvidenceStatus = NO_EVIDENCE
```
它不是 `ERROR`。这条负向观察可以支持“在指定时间、服务和查询条件下没有匹配日志”,但不能支持“故障不存在”。
合法组合被类型约束为:
| InvocationStatus | EvidenceStatus | 是否允许引用 |
|---|---|---:|
| `PROJECTING` | `null` | 否 |
| `READY` | `EVIDENCE_FOUND` | 是 |
| `READY` | `NO_EVIDENCE` | 是,但只能作为限定范围负向观察 |
| `ERROR` | `ERROR` | 否 |
把两者合成一个 `SUCCESS / FAILED` 会丢失最重要的信息:技术成功但业务范围内没有证据。
## 6. 三类 Tool 如何投影
### 6.1 RAG
`RagResultProjector` 从 backend 结果中提取有界 evidence block,稳定文档身份,限制 excerpt 数量和长度,并兼容 `relevanceLevel / relevance_level` 后统一为 `relevance_level`。
这里有一个刻意保留的区分:
```text
evidence 非空 -> EVIDENCE_FOUND
relevance_level=REFERENCE -> 相关度一般
```
`EVIDENCE_FOUND` 只说明存在候选内容,`REFERENCE` 也不自动表示 `NO_GAIN`。内容是否推进当前诊断假设,需要模型结合上下文判断。
### 6.2 Logs
`QueryLogsResultProjector` 不只是截取前几条日志,它还会:
- 脱敏 password、token、secret、API key;
- 脱敏 pod、host、PID、IP 和 SQL literal;
- 去掉堆栈尾部噪声;
- 对数字做模式归一化并聚合重复事件;
- 对事件做跨范围采样,而不是只保留开头;
- 保存实际时间、topic、query scope 和截断标记。
如果 backend 执行成功且日志数组为空,投影结果是 READY + NO_EVIDENCE。backend 明确返回失败,才是 Tool 执行或投影错误。
### 6.3 MySQL
MySQL 在投影之前还有独立的只读安全链:`MysqlSqlValidator` 根据逻辑数据源、schema、table 和 column allowlist 生成 `MysqlQueryPlan`,`JdbcMysqlReadOnlyExecutor` 只执行该 Plan。
`MysqlResultProjector` 再负责:
- 限制最大行数、单元格长度和总 bytes;
- 保持 number、boolean 和 null 类型;
- 对 password、token、secret、credential 等敏感列强制脱敏;
- 结果缩减时标记 `truncated=true`。
Validator 负责“能不能执行”,Projector 负责“模型能看到什么”,两者不能合并。
## 7. Control View 与 Model Observation 的字段边界
| 字段 | Harness | Agent | 原因 |
|---|---:|---:|---|
| `tool_call_id` | 是 | 是 | 引用和上一轮评价都需要 |
| 实际 scope | 是 | 是 | Harness 去重;模型理解负向观察边界 |
| 有界 evidence/events/rows | 是 | 是 | 模型推理所需事实 |
| `evidence_status` | 是 | 是 | 区分候选证据、空结果和错误 |
| `relevance_level` | 是 | 是 | 给模型粗粒度检索语义 |
| `truncated` | 是 | 是 | 防止模型误以为结果完整 |
| `returned_count` | 是 | 否 | Harness 统计,不必消耗模型上下文 |
| normalized scope / duplicate identity | 是 | 否 | 内部控制实现 |
| 连续 NO_GAIN、阈值和剩余预算 | 是 | 否 | 防止模型围绕限制博弈 |
| raw response / 检索轨迹 / 原始分数 | 是 | 否 | 敏感且体积不可控 |
只有 Harness 必须改变模型行为时,才注入 `STOP_REQUIRED`、错误码或已检查范围等有限控制信息。
## 8. 存储决策:为什么是 Redis canonical + MySQL metadata
### 8.1 不把完整 raw 长期写 MySQL
完整 Tool 结果可能包含日志、SQL 查询结果和内部知识内容。长期保存会扩大泄露半径,也会让 JPA audit 成为第二个事实源。MySQL 只保存运营和对账需要的字段,可以长时间保留而不复制正文。
### 8.2 Canonical 读取不续期
Redis record 在创建时设置 TTL,读取或状态更新不恢复初始 TTL。原因是:如果一次历史查询就能续期,敏感 raw 可能因审计访问而永久存在。
状态更新保留当前剩余 TTL,代价是 read-TTL-write 存在小的并发窗口,但它比无界续期更符合数据治理目标。
### 8.3 raw 超限为什么不截断
raw 是内部事实。如果静默截断后仍标记 READY,系统无法区分“backend 只返回这些内容”和“Harness 丢掉了内容”。因此 raw 或完整 record 超限时调用进入 ERROR。
Agent projection 可以截断,因为它本来就是面向消费的摘要,但必须通过 `truncated=true` 明示不完整。
## 9. 真实问题如何改变设计
| 真实问题 | 暴露的错误假设 | 最终修正 |
|---|---|---|
| Tool raw 直接进入 Agent | backend 输出天然适合模型消费 | 增加 Tool-specific projector 和白名单 observation |
| Trace、raw、ContextPack、重复正文同时存在 | 多保存几份可以提高可观测性 | canonical、model view、metadata audit 明确分层 |
| Mock 日志 0 命中返回 `success=false` | 没数据等于调用失败 | 技术执行状态与 NO_EVIDENCE 分离 |
| RAG 的 `REFERENCE` 在投影中丢失 | 只要 evidence 非空就够了 | 保留 relevance_level,但不提升为根因证据 |
| 生产 ObjectMapper 未注册 Java Time 模块,Redis 全部 STORE_ERROR | 单元测试序列化环境等同生产 | 使用生产装配验证 canonical record,并增加 live E2E |
| backend 日志打印 raw/rewritten query | 排障信息可以直接进入普通日志 | 普通日志和 durable audit 只保留安全摘要 |
## 10. 代价与边界
这项设计不是免费的:
1. 每增加一种 Tool,都要同时定义 typed request/result、Adapter、Projector 和 EvidenceGuard 读取规则。
2. Redis 在 TTL 内成为证据校验依赖;不可用时系统应 fail closed,而不是相信 Draft。
3. Agent 看到的是有损投影,Projector 设计不当可能丢掉模型真正需要的诊断信号。
4. durable audit 不能替代 canonical replay;TTL 到期后只能解释调用元数据,无法恢复完整正文。
但这些成本换来了明确的责任:Backend 决定原始事实,Projector 决定模型可见范围,Canonical Store 决定短期验真事实,Audit 决定长期允许保存什么。
## 11. 如何验证
| 验证内容 | 代表性测试或证据 |
|---|---|
| Run mismatch、未授权、非只读、重复 ID、预算和超限 | `ToolBoundaryTest` |
| PROJECTING / READY / ERROR 和 TTL 行为 | `CanonicalInvocationStoreTest` |
| RAG evidence identity、relevance 和 bytes | `RagResultProjectorTest` |
| 日志脱敏、模式聚合、空结果和采样 | `QueryLogsResultProjectorTest` |
| MySQL 行列/敏感字段/容量边界 | `MysqlResultProjectorTest` |
| typed Tool contract 不漂移 | 三类 `*ToolContractTest` |
| 生产 Redis serializer 与 ObjectMapper | single-react cleanup live E2E |
单元测试只能证明数据变换和状态机;生产 Redis serializer、实际 Tool Calling ID 和 backend 返回格式仍必须通过 live E2E 验证。
## 12. 与后续机制的关系
Tool 双视图只解决“什么是可验证的 Tool 事实、模型允许看到什么”,并不解决:
- 这些事实是否支持最终结论:见[Harness证据安全链-从引用真实到结论可发布.md](Harness证据安全链-从引用真实到结论可发布.md);
- Agent 是否应该继续查询:见[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)。
@@ -0,0 +1,302 @@
# Harness 信息增益停止:让无证据诊断正常收敛
**更新日期**:2026-07-29
**主题**:Information Gain、Progress Tracker、STOP_REQUIRED 与过程型 Fallback
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
## 1. 预算能限制损失,不能判断何时完成
在知识库只有通用说明、日志持续为空或查询条件不足时,ReAct Agent 容易出现一种看似合理的行为:不断修改关键词、时间范围或查询表达,再调用一次 Tool。
每一次调用单独看都可能合法,但整个 Run 没有获得新信息。旧系统只能等到模型次数、Tool 次数、Token 或 deadline 耗尽,再以 `BUDGET_EXHAUSTED` 或 `INTERNAL_FAILURE` 结束。
这暴露了两个被混淆的问题:
```text
资源预算:这次 Run 最多允许消耗多少?
业务收敛:继续查询是否仍可能推进当前诊断?
```
预算是最后一道资源保护,不能承担正常停止策略。否则“当前证据不足”会被错误表达成“系统执行失败”。
信息增益停止契约的目标是:在硬预算之前识别连续无进展,使 Agent 停止调用 Tool,并把已经完成的有限检查发布为诚实、可验证的业务结果。
## 2. 先划清三方判断权
设计停止机制时最危险的做法,是让某一层承担它无法可靠完成的判断。
| 参与者 | 能可靠判断什么 | 不能判断什么 |
|---|---|---|
| Tool / Projector | 是否执行成功、结果是否为空、返回数量、实际 scope、是否截断 | 内容是否支持当前诊断假设 |
| Diagnosis Agent | 非空内容是否确认、排除或缩小当前假设 | 是否还能绕过预算、重复和饱和门禁 |
| Harness | scope 是否重复、连续 NO_GAIN、协议是否合规、是否允许继续 | 业务根因是什么 |
| Release | 已有进展可以发布成哪种安全结果 | 是否应该重新调用 Tool |
最终原则是:
> Tool 提供客观结果,模型判断语义价值,Harness 拥有最终停止权。
模型可以选择主动结束,但不能通过继续发 Tool Call 绕过 Harness 已经确定的饱和或预算状态。
## 3. 为什么信息增益只有两个值
当前契约只保留:
```text
information_gain = GAINED | NO_GAIN
```
没有 `HIGH / MEDIUM / LOW`,也没有置信分数。停止控制只需要知道结果是否推进了当前诊断;引入更多等级会带来阈值解释、跨 Tool 标定和模型输出不稳定,却不一定改变最终动作。
判定标准是:
| 结果 | Information Gain | 生产者 |
|---|---|---|
| Tool 执行失败 | 不产生 | 进入技术失败流程 |
| `evidence_status=NO_EVIDENCE` | `NO_GAIN` | Harness |
| 相同 `tool_name + normalized_scope` | `NO_GAIN` | Harness,并拒绝重复执行 |
| 成功非空,确认/排除/缩小假设 | `GAINED` | Diagnosis Agent |
| 成功非空,但只是通用知识、重复内容或无关事实 | `NO_GAIN` | Diagnosis Agent |
RAG 的 `REFERENCE` 不能自动映射为 `NO_GAIN`。它只表示检索相关度一般;一段一般相关的资料可能仍排除一个假设,也可能完全无用,需要模型结合诊断上下文判断。
## 4. 为什么模型在下一次 Tool Call 中评价上一轮
模型只有在读到 Tool Observation 后,才能判断它是否有语义增益。但如果要求模型单独输出一条 progress 消息,就必须增加新的协议轮次或 Progress Judge。
当前设计利用模型已经要做的下一步行为:
- 输出 DiagnosisDraft,表示主动结束;
- 发起下一次 Tool Call,表示希望继续。
当模型选择继续时,下一次 Tool Call Envelope 必须携带对上一轮的评价:
```json
{
"previous_observation": {
"tool_call_id": "call-123",
"information_gain": "NO_GAIN"
},
"input": {
"query": "新的业务查询参数"
}
}
```
Interceptor 在调用业务 Tool 之前完成三件事:
1. 校验 `previous_observation.tool_call_id` 是否正好是当前 pending 调用;
2. 应用 `GAINED / NO_GAIN`,更新连续计数和收集状态;
3. 剥离 `previous_observation`,只把 `input` 传给原业务 Tool。
这是 Agent-facing Tool Schema 的协议变化,但 RAG、日志和 MySQL 的业务 request 并没有被控制字段污染。
```mermaid
sequenceDiagram
participant M as Diagnosis Agent
participant I as Tool Interceptor
participant P as Progress Tracker
participant T as Business Tool
M->>I: Tool Call #1 + input
I->>P: no pending observation
I->>T: business input
T-->>I: successful non-empty observation
I->>P: mark call #1 pending evaluation
I-->>M: bounded observation
M->>I: Tool Call #2 + previous_observation(#1, NO_GAIN)
I->>P: validate ID and apply NO_GAIN
alt 仍为 COLLECTING
I->>T: strip control fields, execute input #2
else 达到 SATURATED
I-->>M: STOP_REQUIRED
end
```
如果模型读完 observation 后直接输出 Draft,就不需要额外评价最后一轮。因为它已经通过实际行为表达“停止”,Harness 也不需要为收集一个统计字段强迫模型再调用 Tool。
## 5. ProgressTracker 保存什么,不保存什么
`DiagnosisProgressTracker` 是 `RunContext` 中的线程安全状态句柄,保存:
- 已完成的 `tool_name + normalized_scope`;
- 已完成 Tool Call identity;
- 当前等待模型评价的 `pendingToolCallId`;
- 连续 `NO_GAIN` 次数;
- 连续 progress protocol violation 次数;
- `COLLECTING / SATURATED` 状态;
- stop reason;
- STOP_REQUIRED 是否已经交付。
它不保存 request、raw response、agent result 或模型 thought。完整 Tool 事实仍属于 Canonical Store。Tracker 只保存作出停止决策需要的最小 identity 和计数,避免出现第二份 Tool 真相。
结束时,`DiagnosisProgressProjector` 根据 completed identity 回读 canonical READY records,投影成有界 `ProgressSnapshot`。无法验证、不可读取或格式非法的记录不会被发布,只会形成安全 limitation。
## 6. 重复检测为什么只做参数级
Harness 使用 `ToolScopeNormalizer` 将业务输入转换成稳定 scope,再比较:
```text
tool_name + normalized_scope
```
这可以识别字段顺序、格式差异下的完全相同查询,并在调用 backend 前拒绝重复执行。
首版没有做自然语言语义去重,例如以下两条 query 可能语义相同,但不会被代码证明为同一 scope:
```text
“查询支付超时日志”
“查找支付请求 timeout 记录”
```
原因是语义去重需要 embedding、模型判断或跨 Tool 指纹,会引入新的不确定性和误杀风险。当前边界选择了可以确定性证明的参数重复;语义近似由模型的 `NO_GAIN` 义务约束。
## 7. 收集状态机
连续无增益阈值由配置控制,当前默认值为 2。`GAINED` 会清零连续计数,避免一次早期空查使后续有效取证被过早停止。
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED / consecutiveNoGain=0
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
COLLECTING --> SATURATED: NO_GAIN / 达到阈值
SATURATED --> STOP_CHANCE: 交付一次 STOP_REQUIRED
STOP_CHANCE --> RELEASE: 模型输出 Draft
STOP_CHANCE --> TERMINATED: 模型再次请求 Tool
```
当某次 READY 结果是 NO_EVIDENCE 时,Harness 可以立即累计 `NO_GAIN`。当成功非空结果需要模型评价时,Tracker 设置 pending ID;上一轮未评价前,新的 Tool Call 不会被执行。
达到 `SATURATED` 后,Harness 给模型一次合法完成机会,而不是在 Tool response 中立刻抛出通用错误。STOP_REQUIRED 只交付一次;模型仍继续请求 Tool 时,`DiagnosisCollectionStoppedException` 将受控停止穿出框架 ReAct loop。
## 8. 协议错误为什么不能计为 NO_GAIN
真实 E2E 曾出现 9 次 `INVALID_PROGRESS_PROTOCOL` Tool 请求拒绝和 13 轮 Diagnosis Agent 模型调用。模型没有正确回传上一轮评价,但这些拒绝也没有增加 NO_GAIN,最终持续空转。
一种看似简单的修复是把协议错误也计为 NO_GAIN。但这会混淆两个事实:
- `NO_GAIN` 表示 Tool 结果没有推进诊断;
- 协议错误表示模型没有遵守控制契约,Tool 根本没有执行。
因此引入独立的协议错误计数和 `PROGRESS_PROTOCOL_VIOLATED`:
1. 第一次错误返回可修正 observation,包含 violation type、缺失字段、期望的上一轮 ID 和允许值;
2. 连续错误达到配置阈值后进入 SATURATED;
3. 交付一次 `STOP_REQUIRED / PROGRESS_PROTOCOL_VIOLATED`;
4. 再次请求 Tool 时受控停止。
协议错误 Trace 使用 `TOOL_REQUEST_REJECTED`,不能记录成 `TOOL_INVOCATION`,因为 backend 从未被调用,Tool 预算和实际执行数也不应被污染。
## 9. 三种停止原因必须分开
| Stop Reason | 含义 | 是否等于技术失败 |
|---|---|---:|
| `INFORMATION_SATURATED` | 连续结果没有推进诊断 | 否 |
| `BUDGET_LIMIT_REACHED` | 达到模型、Tool、Token 或 bytes 预算边界 | 不一定;有安全进展时可发布过程 Fallback |
| `PROGRESS_PROTOCOL_VIOLATED` | 模型连续违反 progress Envelope | 协议失败;有安全进展时仍可保留过程价值 |
信息饱和不能伪装成预算耗尽,否则无法判断阈值是否合理;预算耗尽也不能伪装成信息饱和,因为可能是在持续获得有效证据时资源不足。
这些 stop reason 是 Harness 内部控制语义,不直接作为第二套公开生命周期。最终仍由 Release 映射为 `SUCCESS / FALLBACK / FAILED / CANCELLED`。
## 10. 停止以后如何形成用户结果
停止本身不是答案。系统需要把已经完成的检查转换成可发布内容,又不能依赖预算耗尽后额外调用模型。
`DiagnosisProgressProjector` 从 canonical READY results 生成:
- verified sources;
- observed facts,包括限定范围的空结果;
- 实际查询 scope;
- 投影失败、截断或不可读取形成的 limitations;
- stop reason。
Release 根据现有进展决定:
```mermaid
flowchart TD
S["Agent 主动无结论<br/>或 Harness 受控停止"] --> P["ProgressSnapshot"]
P --> Q{"存在已验真 observed facts?"}
Q -->|"是"| I["INSUFFICIENT_EVIDENCE<br/>展示已检查内容和下一步"]
Q -->|"否"| M{"Draft 声明 missing_info?"}
M -->|"是"| C["MISSING_REQUIRED_CONTEXT"]
M -->|"否"| F["FAILED / fail closed"]
```
`conclusion=null` 是合法 Draft。它允许 Agent 在缺少企业、时间、服务或错误信息时零次调用 Tool,直接报告 `missing_info`,避免为了表现“已经排查”而执行无明确范围的查询。
## 11. 为什么没有引入更多控制字段
### 11.1 不使用 `new_count`
“新记录数量”需要跨 RAG 文档、日志事件和数据库行建立稳定指纹,而且新数据不等于对当前假设有用。它增加了复杂度,却不能替代语义增益判断。
### 11.2 不使用 `next_action`
模型发起 Tool Call已经表示继续,输出 Draft 已经表示结束。再要求 `CONTINUE / STOP` 只会形成一套可能与实际行为冲突的声明状态。
### 11.3 不增加 Progress Judge
独立 Judge 会为每轮 Tool 结果增加模型调用、延迟和失败面。空结果和完全重复 scope 本可由代码判断;其他内容由正在做诊断的 Agent 评价即可。
### 11.4 不向模型公开剩余预算和阈值
模型只需要知道是否必须停止,不需要围绕“还剩几次”规划消耗。阈值、计数和预算属于 Harness Control View,只有 STOP_REQUIRED 等必要指令进入模型上下文。
## 12. Prompt 与硬门禁如何分工
Prompt 仍然需要告诉模型:
- 不必须得出根因;
- `conclusion=null` 是合法完成;
- 缺少必要上下文时可以零 Tool 结束;
- 正确但不能推进假设的内容也是 NO_GAIN;
- 不要通过改写相似关键词重复查询;
- 收到 STOP_REQUIRED 后必须停止。
但 Prompt 只是帮助模型做出正确选择,不构成系统保证。重复 scope、pending evaluation、饱和状态、预算和一次性 STOP_REQUIRED 都由代码门禁执行。
## 13. 真实问题如何改变设计
| 真实现象 | 被证伪的假设 | 设计修正 |
|---|---|---|
| 空日志、通用知识仍不断改写查询 | 模型会自然意识到没有进展 | 明确信息增益义务和 Harness 饱和状态 |
| 最终以 BUDGET_EXHAUSTED 结束 | 硬预算可以充当正常停止 | 预算与信息饱和分离 |
| `REFERENCE` 非空结果持续触发查询 | 非空候选就是有价值证据 | 检索相关度与诊断增益分离 |
| 相同 scope 被反复执行 | Prompt 足以禁止重复 | Harness 参数级去重并在 backend 前拒绝 |
| 9 次协议拒绝仍消耗 13 轮模型 | 错误 observation 会让模型自修复 | 可修正反馈 + 独立协议阈值 + STOP_REQUIRED |
| 非法 Draft 使已完成检查丢失 | 只有合法最终 Draft 才有用户价值 | 已验真 ProgressSnapshot 可形成过程型 Fallback |
| 缺少企业/时间仍被迫调用 Tool | Tool 调用次数大于零才算诊断 | 允许零 Tool、missing context 合法结束 |
## 14. 代价与当前边界
1. 模型侧 Tool Schema 增加了 `previous_observation + input`,这是明确的 Agent-facing 协议变化。
2. 首版只能确定性识别参数相同的重复 scope,不能阻止所有自然语言近义改写。
3. 默认连续 NO_GAIN 阈值 2 是工程起点,需要依靠固定评测集校准;太小会过早停止,太大会增加空转。
4. 最后一轮非空 Tool 结果如果模型直接输出 Draft,Tracker 不强制收集其 information gain;这是减少无意义协议轮次的主动取舍。
5. ProgressSnapshot 依赖 canonical record 仍在 TTL 内且可解析,无法验真的进展不会被发布。
6. 受控预算停止只有在已有安全进展时才能转为 Fallback;没有可验证内容仍然 fail closed。
## 15. 如何验证
| 需要证明 | 代表性测试或 E2E |
|---|---|
| NO_EVIDENCE 自动累计 NO_GAIN,GAINED 清零 | `DiagnosisProgressTrackerTest` |
| 重复 scope 在 backend 前被拒绝 | `HarnessToolInterceptorTest`、`ToolScopeNormalizerTest` |
| pending evaluation 的缺失、乱序和意外回传被拒绝 | Interceptor protocol focused cases |
| 连续协议错误达到阈值并只交付一次 STOP_REQUIRED | Tracker + Interceptor tests |
| 模型主动停止、饱和停止和预算停止都能进入 Release | `DiagnosisAgentUseCaseTest`、`DiagnosisReleaseUseCaseTest` |
| 非法 Draft 只有在存在安全进展时降级 | `DiagnosisChatExecutorTest` |
| Tool 拒绝与实际 Tool 执行分开审计 | exact-run Trace |
| 未知 Query 不再以通用内部错误结束 | ISS-016 named SSE E2E |
其中一条 live E2E 曾准确暴露“协议拒绝不累计 NO_GAIN”的盲区。这说明停止机制不能只验证最终 SSE,还要核对模型轮次、Tool 实际执行数、Tool 拒绝数、Token 和 Timeline 序列。
## 16. 与另外两项设计的关系
信息增益依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供客观 evidence status、scope 和有界 observation;停止后的 ProgressSnapshot 和最终 Fallback 依赖[证据安全链](Harness证据安全链-从引用真实到结论可发布.md)确保只发布当前 Run 可验证的事实。
三者组合后,Harness 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。
@@ -0,0 +1,535 @@
# Harness 失败图谱:异常、停止、降级与终态如何对应
**更新日期**:2026-07-30
**主题**:失败分类、停止决策、安全降级、Run 终态与发布结果
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
在 Harness 中,“没有找到根因”“Tool 报错”“证据不够”“超时”和“客户端断开”不是同一种失败。如果把它们都压成一个 `FAILED`,用户无法知道系统是否完成了有效检查,开发者也无法判断应该重试、换 Tool、发布 Fallback,还是立即终止。
这篇文章不从状态枚举开始,而是用三个问题建立一张失败图谱:
1. 问题发生后,这次 Run 还能继续吗?
2. 已经产生的内容中,有没有可验证的安全事实?
3. 最终能公开正常报告、安全说明,还是只能失败?
第一次阅读只看第 1、2、8、10 和 13 节即可。先掌握判断方法和几个典型场景,不需要记住全部状态。
## 1. 先记住:Harness 的失败处理不是“捕获异常”
假设一次支付超时诊断中连续发生这些事情:
```text
日志 Tool 查询成功,但指定时间段没有记录
RAG Tool 第一次调用网络超时
Agent 换了一个查询范围,找到一条可验证的配置事实
Agent 据此声称“数据库连接池耗尽”
EvidenceGuard 发现报告引用的证据并不支持这个结论
```
如果只看局部,既出现了空结果、技术异常、有效事实,又出现了发布门禁失败。系统不能把其中任何一个事件直接等同于整次 Run 的终态。
Harness 真正要做的是分层决策:
```mermaid
flowchart TB
X["某个问题发生"] --> Q1{"Run 是否仍可继续?"}
Q1 -->|"可以"| C["继续诊断或改查其他 Tool"]
Q1 -->|"不可以"| Q2{"是否已有可验证的安全进展?"}
C --> Q3{"最终报告能否通过发布门禁?"}
Q3 -->|"可以"| S["ReleaseOutcome.SUCCESS<br/>发布诊断报告"]
Q3 -->|"不可以,但有安全说明"| F["ReleaseOutcome.FALLBACK<br/>发布 SafeFallback"]
Q3 -->|"没有任何安全内容"| E["ReleaseOutcome.FAILED<br/>发布 failure"]
Q2 -->|"有"| F
Q2 -->|"无"| E
```
这张图背后的核心决策是:
> 局部事件决定下一步动作,只有 Run Lifecycle 和 Release 才能决定整次请求如何结束、什么可以公开。
因此,失败处理不是一个全局 `try/catch`,而是执行控制、安全事实和发布策略共同完成的结果。
## 2. 第一类:没有答案,但系统没有坏
最容易被误判为失败的场景,是 Tool 正常执行却没有返回证据。
当前 Tool 结果使用两个正交状态:
```text
InvocationStatus:PROJECTING / READY / ERROR
EvidenceStatus:EVIDENCE_FOUND / NO_EVIDENCE / ERROR
```
它们回答不同问题:
| 组合 | 含义 | 是否是技术失败 |
|---|---|---:|
| `READY + EVIDENCE_FOUND` | Tool 成功,当前 scope 有候选证据 | 否 |
| `READY + NO_EVIDENCE` | Tool 成功,当前 scope 没有候选证据 | 否 |
| `ERROR + ERROR` | Tool 执行、投影或 canonical 保存失败 | 是 |
`READY + NO_EVIDENCE` 只能证明“本次查询范围为空”,不能证明“整个系统不存在该问题”。例如查询 10:00 至 10:10 的支付日志为空,不代表全天没有超时,也不代表日志系统之外没有故障。
```mermaid
flowchart LR
T["执行 Tool"] --> I{"InvocationStatus"}
I -->|"ERROR"| X["技术失败路径"]
I -->|"READY"| E{"EvidenceStatus"}
E -->|"EVIDENCE_FOUND"| G["交给 Agent 判断信息增益"]
E -->|"NO_EVIDENCE"| N["记录限定 scope 的空事实<br/>累计 NO_GAIN"]
N --> Q{"还值得继续查询吗?"}
Q -->|"值得"| R["调整假设或 scope"]
Q -->|"连续无增益"| P["CollectionState.SATURATED"]
```
类似的正常无结果还包括:
- 用户没有提供企业、服务、时间范围等必要上下文;
- 多次查询都合法,但连续没有推进诊断;
- Agent 遵循协议主动输出 `conclusion=null`;
- 已完成有限检查,但证据只够描述现象,不够支持根因。
这些场景不应该伪装成系统异常。只要请求被正常处理并形成安全说明,就可以是:
```text
RunState.SUCCESS + ReleaseOutcome.FALLBACK
```
### 为什么不把 NO_EVIDENCE 设计成异常
如果空结果抛异常,系统会产生三个问题:
- Agent 无法区分“查询为空”和“查询服务不可用”;
- 监控会把正常业务分布统计成技术故障;
- Release 无法向用户解释已经检查的范围。
因此放弃了“Tool 无数据即失败”的简化方案,代价是状态维度增加,但换来了可解释的控制语义。
## 3. 第二类:技术异常可能可以重试
并非所有技术异常都应立即结束 Run。对于瞬时、明确、可能恢复的 Provider 故障,Harness 可以执行有限的显式重试。
当前重试策略遵循两个原则:
```text
只有稳定分类为可恢复的技术失败才重试
所有 attempt 都由 Harness 计数、计费并记录
```
典型策略如下:
| 组件 | 可重试场景 | 最大 attempt | 不重试场景 |
|---|---|---:|---|
| Intent Router | timeout、transport、非法输出 | 2 | 已得到合法路由结果 |
| SemanticGuard | timeout、transport、parse/schema failure | 2 | `UNSUPPORTED` |
| Diagnosis Agent | 不执行隐藏 retry | 1 个受控业务循环 | 正常下一轮 ReAct 不是 retry |
| Tool | 不执行隐藏 retry | 每个 Tool Call 一次 | Agent 可以基于结果选择其他 Tool |
| EvidenceRepair | 一次显式修复机会 | 1 | 不是无限修复循环 |
```mermaid
sequenceDiagram
participant H as Harness
participant P as Router / Semantic Provider
participant B as Budget & Trace
H->>B: reserve attempt #1
H->>P: request #1
P-->>H: timeout / transport / invalid output
H->>B: record classified failure
H->>B: reserve attempt #2
H->>P: request #2
alt 得到合法结果
P-->>H: valid result
H->>B: record success
else 再次技术失败
P-->>H: unavailable
H->>B: record final failure
H-->>H: 进入 Fallback 或 FAILED 决策
end
```
### 为什么不使用 SDK 的默认隐藏重试
隐藏重试会让系统无法准确回答:
- 这次请求实际调用了 Provider 几次;
- Token、deadline 和 attempt 消耗在哪里;
- Trace 中的一次调用为什么延迟异常;
- 客户端取消后是否还在后台继续重试。
所以重试必须是 Harness 的控制行为,而不是各组件自行决定。代价是需要维护失败分类和 attempt 协议,但预算与审计仍然闭合。
### `UNSUPPORTED` 为什么不重试
SemanticGuard 返回 `UNSUPPORTED`,表示它成功完成了判断,只是证据不支持报告。这是业务结果,不是技术不可用。重试同一份报告只是在要求模型重新投票,既不能创造新证据,也会削弱门禁的一致性。
## 4. 第三类:一个 Tool 失败,不等于整个 Run 失败
Diagnosis Agent 通常拥有多个只读 Tool。某个 Tool 出现 `ERROR` 后,Agent 可能仍然可以:
- 改查另一个数据源;
- 缩小或调整查询范围;
- 使用已经取得的其他 canonical 事实;
- 明确说明某个数据源不可用,并结束为 Fallback。
```mermaid
flowchart TB
E["单个 Tool ERROR"] --> A{"Run 仍 active 且预算允许?"}
A -->|"否"| T["进入终止决策"]
A -->|"是"| O{"是否还有合法替代动作?"}
O -->|"换 Tool / 换 scope"| C["Agent 继续诊断"]
O -->|"没有"| P{"已有安全进展?"}
P -->|"有"| F["ReleaseOutcome.FALLBACK"]
P -->|"无"| X["ReleaseOutcome.FAILED"]
C --> D{"最终是否形成可发布报告?"}
D -->|"是"| S["ReleaseOutcome.SUCCESS"]
D -->|"否"| P
```
这里不能建立一条简单映射:
```text
Tool ERROR -> RunState.FAILED
```
真正必须终止的情况,是错误破坏了 Harness 的安全前提,或者已经没有合法的恢复路径。例如:
- canonical store 无法保存或回读 Tool 真相;
- Tool raw 或 Agent result 超过硬 bytes 上限;
- 路由经过允许的 attempts 后仍不可用;
- Agent 输出非法 Draft,且不存在可验证的 ProgressSnapshot;
- Harness 自身出现无法分类、无法形成安全响应的内部错误。
### 为什么不让 Tool 自己决定 Run 失败
Tool 只知道一次 invocation 是否成功,不知道整个诊断还拥有多少预算、其他 Tool 是否可用、是否已有安全事实,也不知道最终发布策略。让 Tool 抛出全局终止异常,会把局部职责扩大成 Run 决策权。
当前设计的代价是 Agent 和 Application 必须处理结构化 Tool 错误,而不是依赖异常一路冒泡;收益是局部故障不会无条件摧毁整次诊断。
## 5. 第四类:执行成功,发布仍可能被拒绝
Agent 完成 Draft 并不表示用户一定能看到这份报告。发布前还要经过两类门禁:
```text
EvidenceGuard:引用是否真实、是否属于当前 Run、是否来自 READY invocation
SemanticGuard:这些真实证据是否支持用户可见结论
```
```mermaid
flowchart LR
D["DiagnosisDraft"] --> E{"EvidenceGuard"}
E -->|"通过"| S{"SemanticGuard"}
E -->|"失败"| EF["EVIDENCE_VALIDATION_FAILED<br/>SafeFallback"]
S -->|"SUPPORTED"| R["发布 Diagnosis Report"]
S -->|"UNSUPPORTED"| SU["SEMANTIC_UNSUPPORTED<br/>SafeFallback"]
S -->|"技术不可用"| SA["SEMANTIC_UNAVAILABLE<br/>SafeFallback"]
```
对应的典型结果是:
| 发布门禁结果 | RunState | ReleaseOutcome | FallbackType |
|---|---|---|---|
| 两层门禁通过 | `SUCCESS` | `SUCCESS` | 无 |
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
| SemanticGuard 判断不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
| SemanticGuard 技术不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
这里最反直觉的一点是:Guard 拒绝发布原报告,通常仍然是 `RunState.SUCCESS`。因为 Harness 成功执行了安全策略,并向用户发布了诚实的降级结果;失败的是“原报告获得发布资格”,不是“控制系统无法完成请求”。
### 为什么 Guard 不能直接改写结论
项目放弃了让 Verifier 或 SemanticGuard 顺手生成“更正确答案”的方案。Guard 没有业务 Tool、完整 ReAct 上下文和重新取证能力,改写报告会让审查者同时成为证据生产者。
因此 Guard 只能批准或拒绝,Release 只能发布原报告或确定性 SafeFallback。代价是部分“看起来只差一点”的报告也会降级,但发布责任保持清晰。
## 6. 第五类:停止收集,不等于 Run 已终止
当连续查询没有信息增益,或 Agent 持续违反 progress 协议时,Harness 会把证据收集状态切换为:
```text
CollectionState.SATURATED
```
可能的停止原因包括:
```text
INFORMATION_SATURATED
PROGRESS_PROTOCOL_VIOLATED
BUDGET_LIMIT_REACHED
```
`SATURATED` 只表示不再允许新的 evidence Tool,不是 Run 终态。Harness 仍然要给 Agent 一次机会输出 Draft,或者根据 canonical records 投影 `ProgressSnapshot`。
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
COLLECTING --> SATURATED: 连续 NO_GAIN 或协议违规
SATURATED --> DRAFTING: 交付一次 STOP_REQUIRED
DRAFTING --> RELEASE: Agent 正常结束
DRAFTING --> TERMINATED: Agent 再次请求 Tool
RELEASE --> [*]
TERMINATED --> [*]
```
这项区分解决了一个早期问题:系统过去只能依靠预算把空转“撞停”,最终把证据不足表达成 `BUDGET_EXHAUSTED` 或内部失败。引入 Collection 状态后,业务收敛可以发生在硬资源终止之前。
## 7. 第六类:超时、预算和取消是三种不同终止
Run 的终态只有:
```text
RUNNING
SUCCESS
FAILED
CANCELLED
TIMED_OUT
BUDGET_EXHAUSTED
```
`RunLifecycle` 使用 first-terminal-wins:第一个成功设置的终态不可被后来返回的 Provider、Tool 或异步回调覆盖。
### Deadline 到期
deadline 回答“这次 Run 还能否继续占用时间”。到期后终态是 `TIMED_OUT`。如果没有可发布的安全内容,Release 为 `FAILED`;当前实现不会为了美化结果在超时后继续调用模型生成说明。
### Budget 耗尽
预算可能限制模型 attempt、Tool 调用、Token 或 bytes。终态是 `BUDGET_EXHAUSTED`,但发布结果取决于是否已有安全进展:
```text
有 ProgressSnapshot -> ReleaseOutcome.FALLBACK
没有安全进展 -> ReleaseOutcome.FAILED
```
这说明 `RunState` 和 `ReleaseOutcome` 不能一一对应。
### 客户端断开
客户端断开意味着公开通道已经不存在,Run 转为 `CANCELLED`。取消是协作式的:已经发出的同步 Provider 调用可能无法被物理中止,但返回后必须经过 active check 和 SSE state 检查,迟到结果不能再发布。
```mermaid
sequenceDiagram
participant C as Client
participant S as ChatSseSession
participant H as Harness Core
participant P as Provider / Tool
H->>P: 已发出的同步调用
C--xS: 断开连接
S->>H: cancel Run
H->>H: first terminal = CANCELLED
P-->>H: 迟到结果
H->>H: checkActive 拒绝继续
H-->>S: 不发送 content / done
S->>S: DISCONNECTED 拒绝迟到事件
```
客户端断开时不可靠地发送 `done(CANCELLED)`,因为连接已经不可用。取消事实应由服务端 Trace 和 Run 状态观察,而不是假设客户端还能收到终止事件。
## 8. “安全进展”决定 FALLBACK 还是 FAILED
停止之后,系统不能把 Agent 的未验证草稿直接当作降级内容。所谓安全进展,必须来自当前 Run 的 canonical READY records,并经过有界投影。
`ProgressSnapshot` 可以包含:
- 已实际执行的查询 scope;
- 可验证的 observed facts;
- 限定范围内的 `NO_EVIDENCE`;
- 数据源不可用、结果截断或投影失败等 limitations;
- 推荐补充的上下文或下一步检查。
它不能包含未经支持的根因结论。
```mermaid
flowchart TD
T["Run 无法继续或 Agent 无结论"] --> P["从 canonical records<br/>构建 ProgressSnapshot"]
P --> V{"存在可验证的 observed facts?"}
V -->|"有"| F["FALLBACK<br/>说明已检查内容、限制和下一步"]
V -->|"没有"| M{"是否明确缺少必要上下文?"}
M -->|"是"| C["FALLBACK<br/>MISSING_REQUIRED_CONTEXT"]
M -->|"否"| E["FAILED<br/>不公开未验证内容"]
```
因此,下面两次预算耗尽可以有不同结果:
| 场景 | RunState | ReleaseOutcome | 用户看到什么 |
|---|---|---|---|
| 已验证支付服务在目标时段无日志,随后预算耗尽 | `BUDGET_EXHAUSTED` | `FALLBACK` | 已检查范围、空结果限制和下一步 |
| 第一次模型调用前预算检查失败,没有任何安全事实 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
### 为什么不为所有失败生成一段“友好回答”
如果失败后再调用模型组织解释,会继续消耗已经耗尽的预算,也可能根据异常文本编造业务结论。确定性模板虽然表达能力有限,但不会把未知包装成答案。
这项设计选择了 fail closed:有安全事实才降级,没有就失败。代价是用户体验不总是“自然语言很完整”,但不会为了完整感牺牲可信度。
## 9. 同一个结果,要从四个视图理解
Harness 没有一条能容纳全部语义的总状态机。一次请求结束后,至少要区分四个观察面:
```mermaid
flowchart LR
R["RunState<br/>执行为什么停止"] --> X["一次请求"]
O["ReleaseOutcome<br/>最终公开什么"] --> X
D["diagnosis_run.status<br/>请求是否被安全处理"] --> X
S["SSE 事件<br/>客户端实际收到什么"] --> X
```
### RunState:执行为什么停止
它由 Harness Core 拥有,表达正常完成、内部失败、取消、超时或预算耗尽。
### ReleaseOutcome:最终公开什么
```text
SUCCESS / FALLBACK / FAILED / CANCELLED
```
它由 Release 决定,表达正常诊断报告、安全降级、失败或取消。
### 数据库 status:请求是否被安全处理
`JpaChatRunStore` 当前映射为:
| ReleaseOutcome | diagnosis_run.status |
|---|---|
| `SUCCESS` | `SUCCESS` |
| `FALLBACK` | `SUCCESS` |
| `FAILED` | `FAILED` |
| `CANCELLED` | `CANCELLED` |
因此:
```text
status=SUCCESS + release_outcome=FALLBACK
```
表示请求被正常、安全地处理并发布了降级内容,不表示系统找到了根因。
只有 `DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result` 才能进入下一轮 `PreviousTurn`。Fallback 不进入后续上下文,避免把“证据不足”当作已确认结论继续传播。
### SSE:客户端实际收到什么
公开事件顺序是:
```text
metadata -> status* -> content | failure -> done
```
- `content` 最多一次;
- `content` 与 `failure` 互斥;
- `TERMINAL / DISCONNECTED` 后拒绝迟到结果;
- 当前 `done` 使用 `ReleaseOutcome`,不是另一套遗留终态。
四个视图回答不同问题,排障时不能拿数据库 `SUCCESS` 推断用户收到了一份成功诊断报告。
## 10. 典型场景总表
| 场景 | RunState | ReleaseOutcome | 公开内容或 FallbackType |
|---|---|---|---|
| EvidenceGuard、SemanticGuard 全部通过 | `SUCCESS` | `SUCCESS` | Diagnosis report |
| 缺少企业、服务、时间等必要上下文 | `SUCCESS` | `FALLBACK` | `MISSING_REQUIRED_CONTEXT` |
| 有限检查后仍然证据不足 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| 信息饱和且有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| Progress 协议停止且有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
| SemanticGuard 判断不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
| SemanticGuard attempts 后不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
| 预算耗尽但有安全进展 | `BUDGET_EXHAUSTED` | `FALLBACK` | 当前发布为 `INSUFFICIENT_EVIDENCE` |
| 超时且没有安全内容 | `TIMED_OUT` | `FAILED` | failure |
| 预算耗尽且没有安全进展 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
| 不可恢复内部失败 | `FAILED` | `FAILED` | failure |
| 客户端断开 | `CANCELLED` | `CANCELLED` | 不可靠发送 `done` |
这张表不是一个双向转换规则。例如看到 `ReleaseOutcome.FALLBACK`,不能单独推断 Run 是正常完成还是预算耗尽;仍要结合 Run termination 和 Trace。
## 11. 排障时按什么顺序看
面对“用户为什么没有看到诊断结论”,不要先搜索所有异常日志。按发布结果向前追踪更容易定位:
```mermaid
flowchart LR
U["用户实际收到的 SSE"] --> O["release_outcome<br/>fallback_type"]
O --> R["Run termination<br/>deadline / budget / cancel"]
O --> G["Evidence / Semantic<br/>Release Trace"]
R --> T["Tool Invocation<br/>Progress / canonical record"]
G --> T
```
推荐顺序:
1. 看 SSE 是 `content`、`failure`,还是连接提前断开。
2. 看 `release_outcome` 和 `FallbackType`,确认是正常报告、降级还是失败。
3. 看 Run termination,区分正常完成、超时、预算耗尽和取消。
4. 看 Release、EvidenceGuard、SemanticGuard Trace,确认原报告为什么不能发布。
5. 看 Tool 的 `InvocationStatus + EvidenceStatus`,不要只看一个 `success`。
6. 最后查看 canonical record、scope、bytes、budget usage 和 progress stop reason。
这个顺序从“用户看到什么”追到“内部为什么这样决定”,比从第一条异常开始阅读整条 Timeline 更容易建立因果关系。
## 12. 这套设计放弃了哪些更简单的方案
### 一个 `status` 表示一切
放弃原因:`SUCCESS` 无法同时表达 Tool 执行、Run 终止、报告发布和数据库处理结果。单状态简单,但必然丢失原因。
### 任意异常直接终止整个 Run
放弃原因:局部 Tool 故障仍可能有替代路径;空结果也不是异常。这样做会降低系统可用性并掩盖有限检查的价值。
### 所有错误都自动重试
放弃原因:业务拒绝、容量上限、非法输入和证据不支持不会因为重试自动恢复;隐藏重试还会破坏预算和审计。
### Agent 自己决定何时降级
放弃原因:Agent 无法读取 canonical truth,也不能验证自己的引用。让它同时生成结论和批准发布,会形成自证循环。
### 失败后继续调用模型润色 Fallback
放弃原因:终止后继续消耗资源,且无法保证新文本只包含已验证事实。当前使用确定性 Release Policy 和安全模板。
### 取消时强制等待所有底层调用结束
放弃原因:同步 Provider 未必支持真正中断,等待会延长资源占用。当前采用协作式取消、active check、first-terminal-wins 和 SSE 状态拒绝迟到发布。
## 13. 面试时如何讲这套失败设计
可以用下面这段话概括:
> 我们没有把 Harness 的失败处理设计成一个全局异常捕获器,因为 Agent 系统里“没有证据、单个 Tool 失败、证据不支持结论、预算耗尽和客户端取消”代表完全不同的控制语义。系统先判断 Run 是否还能继续,再从当前 Run 的 canonical records 判断是否已有可验证进展,最后由唯一的 Release Policy 决定发布正常报告、SafeFallback 还是 failure。Tool 使用 InvocationStatus 和 EvidenceStatus 区分技术失败与正常空结果;RunState 解释执行为什么停止,ReleaseOutcome 解释用户最终看到什么,两者不做一一映射。可恢复的 Provider 故障只允许 Harness 做有限、可审计的显式重试,Guard 拒绝发布通常降级而不是把整次请求标成失败,超时、预算和取消则通过 first-terminal-wins 与 SSE 状态阻止迟到结果。这样做的目标不是让每次诊断都成功,而是保证任何结束方式都可解释、可审计,并且不会把未经验证的内容发布给用户。
这段回答要表达的不是“系统定义了很多状态”,而是:**每个状态都对应不同的决策权和失败责任。**
## 14. 当前边界与代价
1. 多套正交状态提高了准确性,也增加了学习成本,必须通过 Context、映射表和 Trace 查询规范维持统一口径。
2. 内存中的 `RunTermination.state/reason` 当前没有完整独立持久化;精确判断 `TIMED_OUT / BUDGET_EXHAUSTED` 仍需结合 Trace、异常路径和预算记录。
3. 协作式取消能阻止迟到发布,但不保证立即终止已经发出的同步 Provider 计算。
4. Tool ERROR 后是否继续由 Agent 在 Harness 门禁内选择,因此模型可能做出次优恢复动作;硬预算和停止协议负责限制损失。
5. 有安全进展才能 Fallback 的策略会让部分请求直接失败,但这是避免发布未验证内容的主动取舍。
6. `FallbackType` 枚举仍保留 `BUDGET_EXHAUSTED`,但当前预算受控停止实际统一发布 `INSUFFICIENT_EVIDENCE`;这是已知命名债务。未来若要区分资源不足与业务证据不足,需要先明确对外语义和兼容策略,不能只替换当前映射。
## 15. 事实来源与延伸阅读
本文对应的主要实现边界:
- `ChatApplicationUseCase`:Application 生命周期、Router、Run 终止和 Release 协作;
- `DiagnosisHarnessCore`:Run active check、预算、终态与控制边界;
- `DiagnosisAgentUseCase`:Tool loop、受控停止和 Draft 形成;
- `DiagnosisReleaseUseCase`:EvidenceGuard、SemanticGuard 与唯一发布策略;
- `JpaChatRunStore`:ReleaseOutcome 到数据库 status 的映射;
- `ChatSseSession`:SSE 单内容、终态和断开规则。
继续阅读:
- [Harness 入门](README.md)
- [支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md)
- [Harness 生命周期与状态](Harness生命周期与状态.md)
- [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md)
- [证据安全链](Harness证据安全链-从引用真实到结论可发布.md)
- [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)
@@ -0,0 +1,468 @@
# Harness 异常处理:Loop 内 vs Loop 外
**日期**:2026-07-30
**范围**:诊断路径(`intent=DIAGNOSIS`)上 ReactAgent ReAct 循环内外的异常、受控停止、降级与终态
**读者**:已读 E2E 全流程 / 运行控制,需要把「失败时系统到底怎么走」串成一张图
**关联文档**:
- [Harness 失败图谱](./Harness失败图谱-异常-停止-降级与终态.md)(失败类型总览)
- [Harness 生命周期与状态](./Harness生命周期与状态.md)(RunState / CollectionState)
- [信息增益停止](./Harness信息增益停止-让无证据诊断正常收敛.md)(SATURATED / STOP_REQUIRED)
---
## 0. 先记住一张总图
```mermaid
flowchart TB
subgraph app["Application 编排"]
UC[ChatApplicationUseCase]
EX[DiagnosisChatExecutor]
end
subgraph agent["Agent 用例"]
UC2[DiagnosisAgentUseCase]
CE[controlledExecution]
RF[recoverInvalidDraft]
end
subgraph loop["ReactAgent ReAct Loop"]
MI[HarnessModelInterceptor]
LLM[ChatModel]
TI[HarnessToolInterceptor]
TB[ToolBoundary]
end
subgraph release["Release"]
REL[DiagnosisReleaseUseCase]
FB[SafeFallback / FALLBACK]
OK[SUCCESS 报告]
end
UC --> EX
EX --> UC2
UC2 -->|agent.call| loop
MI --> LLM
LLM -->|tool_call| TI
TI --> TB
TB -->|observation| LLM
loop -->|受控异常穿出| CE
CE -->|stopped draft=null| REL
loop -->|正常返回坏 draft| UC2
UC2 -->|DiagnosisAgentOutputException| RF
RF -->|有 observedFacts| REL
REL --> FB
REL --> OK
CE -->|取消/超时再抛| UC
RF -->|无 facts 再抛| UC
```
**一句话**:
| 区域 | 异常怎么处理 |
|------|----------------|
| **Loop 内** | 多数变成 **tool/model 侧 observation 或拦截**;少数 **受控异常穿出 loop** |
| **Loop 边界** | `controlledExecution`:受控停止 → `stopped`;不可控 → 包装/再抛 |
| **Loop 外(Executor)** | `recoverInvalidDraft`:坏 draft + 有事实 → FALLBACK;否则失败 |
| **Release** | 有 draft 走 Guard;无 draft / 非法 draft 走专用降级 |
| **Application** | 未消化异常 → `ChatFailureCode` + Run 终态落库 |
---
## 1. 状态有三层,不要混
异常处理会同时碰到三套状态,职责不同:
```mermaid
flowchart LR
subgraph core["Core 执行面"]
RS[RunState<br/>RUNNING / SUCCESS / FAILED<br/>CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED]
end
subgraph prog["Progress 收集面"]
CS[CollectionState<br/>COLLECTING / SATURATED]
SR[DiagnosisStopReason]
end
subgraph out["对外发布面"]
RO[ReleaseOutcome<br/>SUCCESS / FALLBACK / FAILED / CANCELLED]
FT[FallbackType]
CF[ChatFailureCode]
end
RS -.->|"预算耗尽可并存"| RO
SR -.->|"有 facts 常映射"| FT
RO -.->|"失败粗码"| CF
```
| 层 | 枚举 | 回答的问题 |
|----|------|------------|
| Core | `RunState` | 这次 Run 技术上还能不能继续? |
| Progress | `DiagnosisStopReason` / `CollectionState` | 证据收集为何停、是否已饱和? |
| 发布 | `ReleaseOutcome` / `FallbackType` | 用户看到报告、降级还是失败? |
| 协议失败 | `ChatFailureCode` | SSE failure 的粗粒度原因? |
**典型组合**:
| 场景 | RunState | StopReason | ReleaseOutcome |
|------|----------|------------|----------------|
| 正常成功 | SUCCESS | — | SUCCESS |
| 信息饱和且有事实 | 常仍可 SUCCESS 收尾* | INFORMATION_SATURATED | FALLBACK / INSUFFICIENT_EVIDENCE |
| 预算耗尽且有事实 | BUDGET_EXHAUSTED | BUDGET_LIMIT_REACHED | FALLBACK / INSUFFICIENT_EVIDENCE |
| 预算耗尽且无事实 | BUDGET_EXHAUSTED | BUDGET_LIMIT_REACHED | FAILED |
| 客户端断开 | CANCELLED | — | CANCELLED / 可能无 done |
\*饱和后若 Agent 合法写完 draft 并过 Guard,也可能 SUCCESS;若 stopped 无 draft 则走 FALLBACK。
---
## 2. 架构位置:异常在哪一层被「接住」
```mermaid
flowchart TB
subgraph L0["L0 协议"]
CTRL[ChatController / SSE]
end
subgraph L1["L1 Application"]
APP[ChatApplicationUseCase<br/>统一 catch → ChatFailureCode]
DEX[DiagnosisChatExecutor<br/>recoverInvalidDraft]
end
subgraph L2["L2 Agent 用例"]
DAU[DiagnosisAgentUseCase<br/>controlledExecution]
end
subgraph L3["L3 框架 Loop"]
RA[ReactAgent.call]
end
subgraph L4["L4 Interceptor / Boundary"]
MI[ModelInterceptor]
TI[ToolInterceptor]
TB[ToolBoundary]
CORE[DiagnosisHarnessCore]
end
CTRL --> APP --> DEX --> DAU --> RA
RA --> MI --> CORE
RA --> TI --> TB --> CORE
MI -.->|BudgetExceeded / RunAborted 上抛| DAU
TI -.->|多数 error observation 留在 loop| RA
TI -.->|CollectionStopped 上抛| DAU
DAU -.->|stopped| DEX
DAU -.->|Draft 契约异常| DEX
DEX -.->|未恢复| APP
```
---
## 3. Loop 内:一次 ReAct 轮次里发生什么
### 3.1 正常成功路径(对照)
```mermaid
sequenceDiagram
participant DAU as DiagnosisAgentUseCase
participant RA as ReactAgent
participant MI as ModelInterceptor
participant LLM as ChatModel
participant TI as ToolInterceptor
participant TB as ToolBoundary
DAU->>RA: agent.call(input, config)
RA->>MI: interceptModel
MI->>MI: beforeModelCall 预算闸
MI->>LLM: handler.call
LLM-->>MI: tool_call
MI-->>RA: ModelResponse
RA->>TI: interceptToolCall
TI->>TI: 协议/重复/饱和检查
TI->>TB: invoke
TB-->>TI: READY + agentResult
TI-->>RA: 投影 observation
RA->>MI: interceptModel 第 2 轮
MI->>LLM: 写 Draft
LLM-->>MI: 文本 JSON
MI-->>RA: ModelResponse
RA-->>DAU: AssistantMessage
DAU->>DAU: 解析 DiagnosisDraft
DAU-->>DAU: completed(draft, progress)
```
### 3.2 Loop 内:Model 路径异常(会穿出 loop)
**触发点**:`HarnessModelInterceptor.interceptModel`
```text
beforeModelCall / handler.call / checkActive
→ BudgetExceededException
→ RunAbortedException(已终态、超时、取消)
→ 其它 RuntimeException(供应商错误等)
```
```mermaid
sequenceDiagram
participant RA as ReactAgent
participant MI as ModelInterceptor
participant Core as DiagnosisHarnessCore
participant DAU as DiagnosisAgentUseCase
RA->>MI: interceptModel(第 N 次模型)
MI->>Core: beforeModelCall
Core-->>MI: throw BudgetExceeded / RunAborted
Note over MI: 不吞异常,不转 observation
MI-->>RA: 异常上抛
RA-->>DAU: agent.call 失败
DAU->>DAU: controlledExecution(e)
```
| 异常 | Loop 内是否消化 | 穿出后 |
|------|-----------------|--------|
| `BudgetExceededException` | 否 | → `stopped(BUDGET_LIMIT_REACHED)` |
| `RunAbortedException(BUDGET_EXHAUSTED)` | 否 | → 同上 |
| `RunAbortedException(CANCELLED/TIMED_OUT/…)` | 否 | controlledExecution **再抛** → Application |
| 其它未识别 | 否 | → `DiagnosisAgentOutputException(EXECUTION_FAILED)` |
### 3.3 Loop 内:Tool 路径(多数不穿出)
**触发点**:`HarnessToolInterceptor.interceptToolCall` + `ToolBoundary`
```mermaid
flowchart TD
TC[收到 tool_call] --> SUP{是否证据工具?}
SUP -->|否| H[handler.call 旁路]
SUP -->|是| SAT{已 SATURATED 且 stop 已交付?}
SAT -->|是| EX[throw DiagnosisCollectionStoppedException]
SAT -->|否| PARSE[解析 Envelope / previous_observation]
PARSE -->|协议违规| REP[可修复 error observation<br/>或连续违规后 stop]
PARSE --> DUP{重复 scope?}
DUP -->|是| DUPR[DUPLICATE_SCOPE observation]
DUP -->|否| INV[ToolBoundary.invoke]
INV -->|READY| OBS[投影 modelObservation 回注]
INV -->|BUDGET_EXHAUSTED 等 error| ERR[error observation<br/>markBudgetLimitReached]
EX --> OUT[穿出 ReactAgent loop]
OBS --> LOOP[留在 loop,模型继续]
ERR --> LOOP
REP --> LOOP
DUPR --> LOOP
```
**设计选择**:
| 情况 | 策略 | 原因 |
|------|------|------|
| 工具执行失败、投影失败、结果过大 | **error observation 留在 loop** | 给模型一次感知/改写机会,不立刻整 run 崩 |
| 预算在 ToolBoundary 触顶 | **先 error observation** + progress 标记预算 | 本轮不强制撕开 loop;下一轮 model 的 `beforeModelCall` 会硬闸 |
| 信息饱和后仍要工具 | **`DiagnosisCollectionStoppedException` 穿出** | 收集已无价值,禁止空转 |
| 协议违规(未达阈值) | **repairable error observation** | 要求模型补 previous_observation 等 |
`ToolBoundary` 对预算的处理(**吞异常 → 错误码**,不抛给框架):
```text
catch (RunAbortedException | BudgetExceededException)
→ ToolBoundaryResult.error(BUDGET_EXHAUSTED | RUN_INACTIVE)
```
---
## 4. Loop 边界:`controlledExecution`
**位置**:`DiagnosisAgentUseCase`,包住 `agent.call(...)`。
**职责**:把「Harness 约定的受控停止」从异常栈(含 cause 链)捞出来,转成 **`DiagnosisAgentExecution.stopped`**;其余失败交给外层。
```mermaid
flowchart TD
E[catch Exception from agent.call] --> F1{cause 含 CollectionStopped?}
F1 -->|是| S1[stopped progress + stopReason]
F1 -->|否| F2{cause 含 RunAborted?}
F2 -->|是且 BUDGET_EXHAUSTED| S2[markBudgetLimitReached<br/>stopped BUDGET_LIMIT_REACHED]
F2 -->|是且其它终态| R1[再抛 RunAborted]
F2 -->|否| F3{BudgetExceeded 或 lifecycle 已预算耗尽?}
F3 -->|是| S2
F3 -->|否| N[return null]
N --> W[包装 DiagnosisAgentOutputException EXECUTION_FAILED]
S1 --> RET[正常 return 给 Executor]
S2 --> RET
```
| 输入信号 | 输出 |
|----------|------|
| `DiagnosisCollectionStoppedException` | `stopped(stopReason)`,`draft=null` |
| 预算耗尽类 | `stopped(BUDGET_LIMIT_REACHED)` |
| 取消 / 超时类 `RunAborted` | **不转 stopped,再抛** |
| 未知 | `null` → 包装执行失败异常 |
**结果形态**:
```text
completed(draft, progress) // 有合法草稿
stopped(progress, stopReason) // 无草稿,仅有进度快照
```
---
## 5. Loop 外:坏 Draft 与 `recoverInvalidDraft`
**时机**:`agent.call` **已经返回**(或解析阶段),最终文本 **不是**合法 `DiagnosisDraft`。
**位置**:`DiagnosisChatExecutor` catch `DiagnosisAgentOutputException`。
```mermaid
flowchart TD
A[DiagnosisAgentOutputException] --> K{isDraftContractFailure?}
K -->|否 EXECUTION_FAILED| P1[再抛 → Application FAILED]
K -->|是 EMPTY/INVALID_JSON/SCHEMA| H{hasObservedFacts?}
H --> AUD[审计 agentDraftInvalid]
AUD --> H2{hasObservedFacts?}
H2 -->|否| P1
H2 -->|是| SSE[status SAFETY_VALIDATING]
SSE --> RID[releaseInvalidDraft progress]
RID --> FB[FALLBACK INSUFFICIENT_EVIDENCE]
```
要点(与常见误解对照):
| 误解 | 实际 |
|------|------|
| recover 里再验 draft 完不完整 | draft 合法性已在 Agent 用例判定;此处只看 **异常 Kind** |
| hasProgress = 有过 tool_call | = **`progress.observedFacts` 非空**(可发布观察事实) |
| 降级会带上坏 JSON | **丢弃非法正文**,只根据 progress 生成 SafeFallback |
**专用发布**:`DiagnosisReleaseUseCase.releaseInvalidDraft`
- 不再跑 Evidence/Semantic Guard(没有可校验 draft)
- 要求 `hasObservedFacts()`,否则 `IllegalStateException`
- 产出 `FallbackType.INSUFFICIENT_EVIDENCE`
---
## 6. Release:按 execution 形态分叉
```mermaid
flowchart TD
EX[DiagnosisAgentExecution] --> D{draft == null?}
D -->|是 stopped| CS[releaseControlledStop<br/>需有 observedFacts]
D -->|否| C{conclusion == null?}
C -->|是| NC[releaseNoConclusion<br/>部分 Evidence + progress]
C -->|否| RC[releaseConclusion<br/>Evidence → 可选 Repair → Semantic]
CS --> FB[FALLBACK]
NC --> FB
RC -->|SUPPORTED| OK[SUCCESS]
RC -->|其它| FB
```
| 入口 | 典型来源 |
|------|----------|
| `releaseControlledStop` | `controlledExecution` → stopped |
| `releaseInvalidDraft` | `recoverInvalidDraft`(loop 结束坏 draft) |
| `releaseConclusion` | 正常 completed + 有结论 |
| `releaseNoConclusion` | draft 合法但无 conclusion |
---
## 7. Application 统一失败出口
未被转成 FALLBACK / SUCCESS 的异常,进入 `ChatApplicationUseCase`:
```mermaid
sequenceDiagram
participant EX as DiagnosisChatExecutor
participant APP as ChatApplicationUseCase
participant Core as DiagnosisHarnessCore
participant Store as ChatRunStore
participant SSE as ChatSseSession
EX-->>APP: 抛 ChatApplicationException 或 RuntimeException
APP->>Core: terminalOutcome / completeFailure
APP->>Store: finish(terminal, ...)
APP->>APP: safeFailure → ChatFailureCode
APP-->>SSE: fail(code, 安全文案)
```
常见映射直觉:
| 情况 | ChatFailureCode 倾向 |
|------|----------------------|
| 取消 | `RUN_CANCELLED` |
| 路由失败 | `ROUTING_UNAVAILABLE` |
| 落库失败 | `RUN_PERSISTENCE_FAILED` |
| 其它 | `INTERNAL_FAILURE` |
---
## 8. 端到端对照表(按场景)
| # | 场景 | 发生位置 | 关键信号 | 第一处理点 | 用户侧 |
|---|------|----------|----------|------------|--------|
| 1 | 第 N 次模型前预算没了 | Loop 内 Model | `BudgetExceeded` | controlledExecution → stopped | 有 facts→FALLBACK;无→FAILED |
| 2 | 工具执行失败 | Loop 内 Tool | `TOOL_EXECUTION_ERROR` observation | 留在 loop | 模型可能改写或再试 |
| 3 | 工具触顶预算 | Loop 内 ToolBoundary | error + markBudget | 常留在 loop,下轮 model 硬闸 | 同预算结局 |
| 4 | 饱和后仍要工具 | Loop 内 Tool | `DiagnosisCollectionStopped` | controlledExecution → stopped | 有 facts→FALLBACK |
| 5 | 取消/超时 | Core → 任意 checkActive | `RunAborted` | controlledExecution **再抛** | CANCELLED / FAILED |
| 6 | 模型返回烂 JSON | Loop 外解析 | `INVALID_JSON` 等 | recoverInvalidDraft | 有 facts→FALLBACK;无→FAILED |
| 7 | 执行崩溃未识别 | Loop 边界 | `EXECUTION_FAILED` | recover 不恢复,上抛 | FAILED |
| 8 | Guard 引用失败 | Loop 外 Release | Evidence 违规 | repair 或 FALLBACK | FALLBACK EVIDENCE_* |
| 9 | 语义不支持 | Loop 外 Release | UNSUPPORTED | FALLBACK | FALLBACK SEMANTIC_* |
---
## 9. 两条主恢复路径对比(必记)
```mermaid
flowchart LR
subgraph mid["Loop 中途打断"]
A1[Interceptor / Core 异常] --> B1[controlledExecution]
B1 --> C1[stopped draft=null]
C1 --> D1[releaseControlledStop]
end
subgraph end["Loop 正常结束但输出坏"]
A2[解析 Draft 失败] --> B2[DiagnosisAgentOutputException]
B2 --> C2[recoverInvalidDraft]
C2 --> D2[releaseInvalidDraft]
end
D1 --> E[INSUFFICIENT_EVIDENCE FALLBACK<br/>前提 hasObservedFacts]
D2 --> E
```
| | `controlledExecution` | `recoverInvalidDraft` |
|--|----------------------|------------------------|
| 时机 | loop **中途** | loop **结束后** |
| 输入 | `Throwable` cause 链 | `DiagnosisAgentOutputException` |
| 成功产物 | `Execution.stopped` | 直接 `DiagnosisExecutionResult(FALLBACK)` |
| 发布入口 | `releaseControlledStop` | `releaseInvalidDraft` |
| 共同点 | 都依赖 **可发布 observedFacts**;都 **fail closed** |
---
## 10. 代码索引
| 组件 | 路径 |
|------|------|
| Model 拦截 | `harness/agent/HarnessModelInterceptor.java` |
| Tool 拦截 | `harness/agent/HarnessToolInterceptor.java` |
| 工具边界 | `harness/tool/boundary/ToolBoundary.java` |
| 受控停止转换 | `harness/agent/DiagnosisAgentUseCase#controlledExecution` |
| 坏 Draft 恢复 | `harness/application/executor/DiagnosisChatExecutor#recoverInvalidDraft` |
| 非法 draft 发布 | `harness/release/DiagnosisReleaseUseCase#releaseInvalidDraft` |
| 受控停止发布 | `DiagnosisReleaseUseCase#releaseControlledStop` |
| 应用失败出口 | `harness/application/ChatApplicationUseCase` |
| 错误码注释 | `ChatFailureCode` / `ReleaseOutcome` / `FallbackType` / `RunState` / `DiagnosisStopReason` / `ToolBoundaryErrorCode` 等 |
---
## 11. 阅读检查清单
1. 这个失败是 **还在 loop 里**,还是 **已经穿出 agent.call**?
2. 是 **资源/取消**(Core),还是 **收集收敛**(Progress),还是 **输出契约**(Draft Kind)?
3. 最终有没有 **`observedFacts`**?有才能谈 FALLBACK。
4. `RunState` 与 `ReleaseOutcome` 是否一致理解(预算耗尽 ≠ 一定 FAILED)?
5. Tool 错误是 **observation** 还是 **异常穿出**?不要默认「有 error 就崩 run」。
---
## 12. 一句话总结
> **Loop 内:能安全回注的变成 observation,必须停收集或硬预算的穿出异常。**
> **Loop 边界:controlledExecution 把受控停止收成 stopped。**
> **Loop 外:坏 draft 用 recoverInvalidDraft,只认 observedFacts 做降级。**
> **全程 fail closed:没有可验证事实,就不发「看起来友好」的假成功。**
@@ -0,0 +1,165 @@
# Harness 执行控制笔记:终态检查与取消广播
**用途**:面试复习用。回答"一次 Run 的执行如何被控制、取消如何生效、为什么是协作式"。
**代码基线**:`com.superbiz.agent.harness.core` + `guard/semantic/GuardModelCall` + `tool/mysql/JdbcMysqlReadOnlyExecutor`
**配套**:[RunBudget 预算流程时序图](RunBudget预算流程-一次Run的资源门禁时序图.md)
## 1. 一句话核心
> 执行控制由三个句柄组成:RunBudget 管"还能不能花"、RunCancellation 管"要不要停"、RunLifecycle 管"最终怎么定"。判断停止的机制有两套:**checkActive 轮询检查点**(读终态)和 **onCancel 订阅广播**(推送中断)——前者让"到了检查点的调用"被拒绝,后者让"正在阻塞的操作"被实时打断。
## 2. 执行控制三件套
| 句柄 | 回答的问题 | 关键机制 | 本质 |
|---|---|---|---|
| RunBudget | 还能不能继续消耗 | 调用前预扣,超限抛异常 | 门禁(止损) |
| RunCancellation | 是否要求停止 | first-reason-wins + 回调广播 | 事件(信号) |
| RunLifecycle | 最终哪个终态生效 | first-terminal-wins(CAS) | 事实(结果) |
**预扣 vs 记账 vs 落定**:预算在调用前拦,取消在运行中广播,终态在结束时定死。
## 3. checkActive:三层闸门(轮询)
`DiagnosisHarnessCore.checkActive` 在**每次模型/Tool 调用前**执行,顺序固定:
```java
① termination 已存在 → 抛 RunAbortedException // 已终态,无论什么原因
② deadline 已过 → finish(TIMED_OUT) + cancel(DEADLINE_EXCEEDED) + 抛异常
// 超时是主动动作:自己写终态、自己广播,不是等别人来
③ cancellation.isCancelled() → 抛 RunAbortedException
```
- 调用点:`beforeModelCall`(每轮模型)、`beforeToolCall`(每次 Tool)、`reserveRunBytes`(canonical 体积)
- 它是**轮询**:只在检查点生效。正在阻塞的操作(模型等待、SQL 查询)不会自己撞上它。
## 4. termination:终结事实快照
`RunLifecycle` 持有 `AtomicReference<RunTermination>`:
```java
record RunTermination(RunState state, String reason, Instant completedAt)
// 构造校验:state 必须 isTerminal(),reason 非空
// null = 还在 RUNNING;非 null = 已终结,不可变
```
- 终态只有 5 个:`SUCCESS / FAILED / CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED`(`RUNNING` 非终态)
- 写入后永远定格,只能靠 CAS 换整个引用 → first-terminal-wins 的物理基础
- checkActive 第一道闸就是读它
## 5. 两个正交的 CAS
| 门 | 保护什么 | 语义 |
|---|---|---|
| `RunCancellation.reason`(AtomicReference) | **原因**:谁要求停、为什么停 | first-reason-wins |
| `RunLifecycle.termination`(AtomicReference) | **结果**:最终终态 | first-terminal-wins |
```java
// cancel() 两件事:写原因(CAS)+ 遍历 callbacks 广播
public boolean cancel(RunCancellationReason reason) {
if (!this.reason.compareAndSet(null, reason)) return false; // first-reason-wins
callbacks.forEach(...); // 广播给订阅者
return true;
}
// finish() 一件事:写终态(CAS)
public boolean finish(RunState state, String reason) {
return termination.compareAndSet(null, new RunTermination(state, reason, now));
}
```
**为什么不能合并**:
- 取消是"意图/原因"(可被多线程同时请求、可被订阅),终态是"结果/事实"(只读、不可变)
- 原因→终态是**多对一**映射:`DEADLINE_EXCEEDED→TIMED_OUT`、`INTERNAL_FAILURE→FAILED`、`CLIENT_DISCONNECTED/USER_REQUESTED→CANCELLED`
- 完成路径(`completeSuccess`/`completeFailure`)根本不经过 cancel;run 已终态时取消请求被拒绝(不翻案)
## 6. 取消广播:为什么必须有它(推送 vs 轮询)
只设置终态,只能让**下一次 checkActive** 拒绝——正在阻塞的操作不会自己醒来。广播通过 **onCancel 回调**直接打断阻塞中的操作。
代码里真实的订阅者只有三处:
| 订阅者 | 回调动作 | 打断机制 |
|---|---|---|
| Core `startRun` | `lifecycle.finish(terminalState(reason))` | 终态联动 |
| `GuardModelCall` | `future.cancel(true)` | 线程 interrupt |
| `JdbcMysqlReadOnlyExecutor` | `statement.cancel()` | JDBC 协议取消 |
## 7. 打断机制:两种物理中断
**机制一:Java 线程中断(`Future.cancel(true)`)**
```java
Future<String> future = executor.submit(() -> invoke(...)); // Guard 任务在独立线程池
context.cancellation().onCancel(ignored -> future.cancel(true)); // 订阅
return future.get(timeout, TimeUnit.NANOSECONDS); // 业务线程阻塞等待
```
取消线程执行回调 → `future.cancel(true)` → 向执行任务的线程发 `Thread.interrupt()` → 目标线程若阻塞在可中断等待则立刻抛 `InterruptedException` / `CancellationException` 醒来 → catch 后 `checkActive` → `RunAbortedException`。
**机制二:JDBC 协议取消(`Statement.cancel()`)**
```java
AtomicReference<Statement> statementRef = new AtomicReference<>(statement);
context.cancellation().onCancel(ignored -> cancel(statementRef.get())); // 订阅
... executeQuery() ...
finally { statementRef.set(null); }
```
取消线程 → `statement.cancel()` → 向 MySQL 服务器发取消请求 → 服务器终止查询 → 客户端 `executeQuery` 抛 `SQLException` 醒来。不走线程中断,走数据库协议,更"物理"。
**细节**:
- `statementRef` 用 AtomicReference 包:回调可能在执行前/中/后触发,`finally` 里 `set(null)`,读到 null 说明已结束、跳过 cancel
- `future.cancel()` 幂等,对已完成 Future 调用无害,Guard 侧无需此保护
- 被打断不是"裸死":Guard catch `CancellationException` → `checkActive`;MySQL 抛 `SQLException` → ToolBoundary 记 ERROR——**醒来后仍走统一状态机**,取消不会产生绕过 Harness 的野异常
## 8. 协作式取消的精确边界
能否推送中断,取决于 **Harness 是否持有该调用的执行句柄**:
| 调用 | 谁发起 | Harness 有句柄吗 | 取消时 |
|---|---|---|---|
| Agent 模型调用 | 框架 ReAct 内部 | 无(拦截器只环绕) | 只能等下一次 checkActive 轮询 |
| Guard 模型调用 | Harness `executor.submit` | 有 Future | `future.cancel(true)` 推送中断 |
| MySQL 查询 | Harness 自己执行 | 有 Statement | `statement.cancel()` 推送中断 |
> "协作式" = 愿意被打断的(订阅了 onCancel 且持有句柄)实时打断;框架持有的 Agent loop 物理上无法打断,只能等检查点。但无论哪种,最终结果都受 first-terminal-wins 保护。
## 9. 为什么"先 finish 再 cancel"(二次 finish 无害)
`exhaustBudget` / 超时路径都执行"自己设终态 + 自己广播":
```java
lifecycle.finish(BUDGET_EXHAUSTED, ...); // ① 先固化事实(主操作,不依赖回调)
cancellation.cancel(BUDGET_EXHAUSTED); // ② 再广播信号(副作用)
```
- cancel 触发回调 → 回调里再次 `finish` → **CAS 失败返回 false** → 无害、被静默吸收
- 顺序意义:终态不依赖回调注册/执行;即使回调异常或重复触发,终态都已正确
- 两个 CAS 各自 first-wins,最终状态永远一致(见下表,任何组合都无害):
| finish CAS | cancel CAS | 结果 |
|---|---|---|
| 成功 | 成功 | 正常 |
| 成功 | 失败 | 终态正确,广播已由先前 cancel 触发过 |
| 失败 | 成功 | 已有更早终态,回调里 finish 失败无害 |
| 失败 | 失败 | 早已终结,本次调用本就不该发生 |
## 10. 面试话术(三段式)
**执行控制**:
> 执行控制由预算、取消、终态三个句柄组成,统一走 Core 的检查链:每轮模型或 Tool 调用前先 checkActive——终态存在就拒绝,超时就主动写 TIMED_OUT 并广播取消,已取消就拒绝;然后预算预扣,超限时把 BUDGET_EXHAUSTED 固化为终态并广播取消。预算管"能不能花",取消管"要不要停",终态管"最终怎么定"。
**取消广播**:
> 取消是协作式的。取消信号可能由容器线程注入(SseEmitter 断连回调调 core.cancel),CAS 写原因后同步遍历订阅者回调:Guard 模型调用中断自己的 Future、MySQL 中断自己的 Statement,让业务线程从阻塞中立刻醒来,再在下一个检查点被 RunAbortedException 拒绝。能否推送中断取决于 Harness 是否持有该调用的句柄——自己提交的调用(Guard/MySQL)能打断,框架持有的 Agent 模型调用只能等下一次 checkActive。
**为什么两个 CAS**:
> cancel 和 finish 是两条正交的通道:cancel 传原因和广播信号,finish 落最终事实。取消必须走 cancel 是因为要保留原因维度、要广播给运行中的组件;但终态又必须由 finish 直接保证,不能依赖回调。所以预算耗尽时两个都调——事实先定,信号随后,重复写终态被 CAS 吸收。
## 11. 代码位置索引
| 内容 | 位置 |
|---|---|
| RunContext 九成员 | `harness/core/RunContext.java` |
| checkActive / startRun / exhaustBudget | `harness/core/DiagnosisHarnessCore.java` |
| 终态 CAS | `harness/core/RunLifecycle.java` + `RunTermination.java` |
| 原因 CAS + 广播 | `harness/core/RunCancellation.java` |
| 预算预扣/记账 | `harness/core/RunBudget.java` + `RunCapacityCounter.java` |
| Guard 模型取消订阅 | `harness/guard/semantic/GuardModelCall.java:58` |
| MySQL 查询取消订阅 | `harness/tool/mysql/JdbcMysqlReadOnlyExecutor.java:52` |
| 断连取消入口 | `controller/sse/ChatSseSession.java:44` → `harness/application/ChatApplicationUseCase.java:340` |
@@ -0,0 +1,440 @@
# Harness 生命周期与状态
**更新日期**:2026-07-29
**状态**:当前实现口径
**术语前置**:[CONTEXT.md](CONTEXT.md)
## 1. 先澄清:系统不存在一条包含所有状态的总状态机
当前 Harness 有多组正交状态:
- RunState 控制内存执行终态;
- ChatApplicationStatus 表示用户可见处理阶段;
- InvocationStatus 表示单次 Tool invocation 生命周期;
- EvidenceStatus 表示 Tool 客观结果;
- CollectionState 表示能否继续收集证据;
- StopReason 表示停止收集的内部原因;
- SemanticVerdict 表示结论支持度;
- ReleaseOutcome 表示最终发布结果;
- SSE Session State 表示连接能否继续发送。
它们在一次请求中并行演进,只在少数边界发生映射。生命周期文档的目标不是把它们合并,而是说明谁先发生、由谁拥有、在哪里汇合。
## 2. 一次 Run 的主时间线
```mermaid
sequenceDiagram
participant C as Client / SSE
participant A as ChatApplicationUseCase
participant H as Harness Core
participant R as Intent Router
participant D as Diagnosis Runtime
participant L as Release Pipeline
participant P as Persistence / Trace
C->>A: query + optional sessionId
A->>A: 读取 routing history / PreviousTurn
A->>H: startRun(sessionId)
H-->>A: RUNNING RunContext
A->>P: diagnosis_run=RUNNING + RUN_STARTED
A-->>C: metadata(sessionId, runId)
A->>R: route(query, bounded history)
R-->>A: IntentType
alt SYSTEM_CHAT
A->>A: 单轮受控模型回答
else KNOWLEDGE_QUERY
A->>A: 一次 RAG + 单轮答案
else DIAGNOSIS
A->>D: Agent ReAct / Tool / Progress
D-->>A: Draft 或 Controlled Stop
A->>L: release(Draft/Progress/StopReason)
L-->>A: SUCCESS Draft 或 SafeFallback
end
alt 正常完成
A->>H: completeSuccess(预算 Fallback 特例除外)
A->>P: 持久化 release outcome / safe content / usage
A->>P: RUN_FINISHED
A-->>C: content + done
else 失败
A->>H: completeFailure 或读取既有终态
A->>P: 持久化 FAILED/CANCELLED
A->>P: RUN_FINISHED
A-->>C: failure + done(FAILED)
end
```
创建顺序很重要:Application 先读取会话上下文,再创建 RunContext、持久化 RUNNING、记录 RUN_STARTED,之后才进入路由和执行。SSE 的 metadata 在 `onStarted` 中发布 exact sessionId/runId,后续结果必须匹配这组身份。
## 3. Run 生命周期
### 3.1 状态
```mermaid
stateDiagram-v2
[*] --> RUNNING
RUNNING --> SUCCESS: 正常路径完成
RUNNING --> FAILED: 不可恢复内部失败
RUNNING --> CANCELLED: 客户端断开或用户取消
RUNNING --> TIMED_OUT: deadline 到达
RUNNING --> BUDGET_EXHAUSTED: 任一硬预算超限
SUCCESS --> [*]
FAILED --> [*]
CANCELLED --> [*]
TIMED_OUT --> [*]
BUDGET_EXHAUSTED --> [*]
```
`RunLifecycle.finish` 使用原子 compare-and-set:只有从“尚无 termination”到某个终态的第一次转换成功。所有终态都不可再次转换。
### 3.2 状态含义
| RunState | 含义 | 常见来源 |
|---|---|---|
| `RUNNING` | 尚未产生 RunTermination | `startRun` 后默认状态 |
| `SUCCESS` | Application 已形成正常可持久化结果 | 正常内容,或非预算类 SafeFallback |
| `FAILED` | 不可恢复执行失败 | 路由不可用、模型失败、无安全进展的非法 Draft 等 |
| `CANCELLED` | 协作取消已成为第一个终态 | 客户端断开、用户请求 |
| `TIMED_OUT` | `checkActive` 发现 deadline 已到 | 模型/Tool/Guard 边界前后检查 |
| `BUDGET_EXHAUSTED` | 模型、Tool、Token 或 bytes 预算超限 | `DiagnosisHarnessCore.exhaustBudget` |
### 3.3 RunState 与 ReleaseOutcome 不一一对应
最重要的反例是 Fallback:
- 证据不足、缺少上下文、语义不支持等正常降级,RunState 最终为 `SUCCESS`,ReleaseOutcome 为 `FALLBACK`;
- 预算耗尽后已经有安全 ProgressSnapshot,RunState 保持 `BUDGET_EXHAUSTED`,但 ReleaseOutcome 可以为 `FALLBACK`;
- 超时或预算耗尽且无法形成安全内容时,RunState 分别保持 `TIMED_OUT / BUDGET_EXHAUSTED`,ReleaseOutcome 映射为 `FAILED`。
所以不能从 ReleaseOutcome 反推出精确 RunState,也不能把 `FALLBACK` 当作 RunState。
## 4. 取消生命周期
`RunCancellation` 使用 first-reason-wins。取消原因包括:
| Reason | RunState 映射 |
|---|---|
| `CLIENT_DISCONNECTED` | `CANCELLED` |
| `USER_REQUESTED` | `CANCELLED` |
| `DEADLINE_EXCEEDED` | `TIMED_OUT` |
| `BUDGET_EXHAUSTED` | `BUDGET_EXHAUSTED` |
| `INTERNAL_FAILURE` | `FAILED` |
生命周期和取消句柄互相配合:取消回调尝试完成 RunLifecycle;deadline 和预算路径也会先确定终态,再触发取消阻止后续工作。
取消语义是协作式的:
1. 后续模型、Tool、Guard 边界调用 `checkActive` 时立即失败;
2. SSE 断开后不再发送 content;
3. first-terminal-wins 阻止迟到成功覆盖 CANCELLED/TIMED_OUT;
4. 已经进入 Provider 的同步调用是否物理停止,取决于底层客户端,Harness 不作虚假保证。
## 5. Application 阶段不是生命周期状态
`ChatApplicationStatus` 用于 SSE status 事件:
```text
ROUTING
SYSTEM_RESPONDING
KNOWLEDGE_SEARCHING
KNOWLEDGE_ANSWERING
DIAGNOSIS_RUNNING
SAFETY_VALIDATING
```
这些值只是用户可见的处理阶段:
- 不是严格完备的状态机;
- 不表示终态;
- 不持有取消或预算;
- 不保证每个 Run 都经过全部阶段。
例如 Diagnosis Run 通常经过 `ROUTING -> DIAGNOSIS_RUNNING -> SAFETY_VALIDATING`,System Chat 则经过 `ROUTING -> SYSTEM_RESPONDING`。
## 6. Diagnosis Agent 执行生命周期
`DiagnosisAgentExecution` 只有两种合法形态,不额外定义一套枚举:
| 形态 | 字段 | 含义 |
|---|---|---|
| Completed | `draft != null, stopReason=null` | Agent 输出严格合法 DiagnosisDraft |
| Controlled Stop | `draft=null, stopReason != null` | Tool loop 因信息饱和、预算或协议错误受控停止 |
其他情况通过异常表达:
- 空 Draft;
- 非法 JSON;
- Schema 不合法;
- 模型调用失败;
- RunAborted。
Draft 合同失败不是 DiagnosisStopReason。非法 Draft 会被丢弃;只有异常携带的 ProgressSnapshot 已包含可验真 facts,Application 才允许 Release 生成过程型 Fallback。
## 7. 单次 Tool Invocation 生命周期
### 7.1 状态转换
```mermaid
stateDiagram-v2
[*] --> PREFLIGHT
PREFLIGHT --> REJECTED: invalid run/id/auth/readonly/request/budget/store
PREFLIGHT --> PROJECTING: canonical begin 成功
PROJECTING --> READY: backend + projection + store 成功
PROJECTING --> ERROR: execution/projection/size/budget/store error
READY --> [*]
ERROR --> [*]
REJECTED --> [*]
```
`PREFLIGHT` 和 `REJECTED` 是本文用于解释流程的阶段,不是 `InvocationStatus` 枚举值。canonical record 只有:
- `PROJECTING`:已创建,尚未形成可引用结果;
- `READY`:执行和投影完成,可以被当前 Run 引用;
- `ERROR`:调用失败,不可引用。
部分 preflight 失败发生在 canonical begin 之前,因此可能只有 `ToolBoundaryResult.ERROR` 和审计事件,没有 canonical ERROR record。
### 7.2 EvidenceStatus 是另一条轴
| InvocationStatus | EvidenceStatus | 语义 |
|---|---|---|
| `PROJECTING` | `null` | 未完成 |
| `READY` | `EVIDENCE_FOUND` | 技术完成,存在候选内容 |
| `READY` | `NO_EVIDENCE` | 技术完成,当前 scope 为空 |
| `ERROR` | `ERROR` | 技术失败 |
`READY + EVIDENCE_FOUND` 仍不表示证据足以支持结论;后续还要经过 InformationGain、EvidenceGuard 和 SemanticGuard。
## 8. 信息收集生命周期
### 8.1 Collection State
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED / 清零 no-gain
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值
COLLECTING --> SATURATED: 连续 progress protocol violation 达阈值
SATURATED --> SATURATED: 终态,不再允许新 evidence Tool
```
`SATURATED` 只表示证据收集不再允许继续。它不是整个 Run 的终态,Agent 仍有一次机会输出 Draft,之后进入 Release。
预算超限不会把 CollectionState 改成 SATURATED。它将 RunState 置为 `BUDGET_EXHAUSTED`,并把内部 DiagnosisStopReason 标记为 `BUDGET_LIMIT_REACHED`。
### 8.2 InformationGain
- `NO_EVIDENCE` 和完全重复的 `tool_name + normalized_scope` 由 Harness 产生 `NO_GAIN`;
- 其他成功非空结果由 Diagnosis Agent 评价 `GAINED / NO_GAIN`;
- `GAINED` 清零连续 NO_GAIN;
- 技术失败和 progress 协议错误不产生 InformationGain。
### 8.3 StopReason
| DiagnosisStopReason | 触发条件 | 对 CollectionState 的影响 |
|---|---|---|
| `INFORMATION_SATURATED` | 连续 NO_GAIN 达阈值 | 进入 SATURATED |
| `PROGRESS_PROTOCOL_VIOLATED` | 连续协议错误达阈值 | 进入 SATURATED |
| `BUDGET_LIMIT_REACHED` | RunBudget 超限 | 不要求进入 SATURATED;Run 已终止 |
STOP_REQUIRED 是否已经交付由独立 boolean 记录,它不是第四种 CollectionState。一次指令交付后仍请求 Tool,会抛出受控停止异常。
## 9. Guard 与 Release 生命周期
```mermaid
flowchart TD
I["DiagnosisAgentExecution"] --> D{"有合法 Draft?"}
D -->|"否,受控停止"| PS["验证 ProgressSnapshot"]
D -->|"是,无 conclusion"| NC["验证已有 Tool references"]
D -->|"是,有 conclusion"| EG["EvidenceGuard initial"]
EG --> EV{"evidence valid?"}
EV -->|"否"| ER["EvidenceRepair once"]
ER --> RE["EvidenceGuard recheck"]
EV -->|"是"| SG["SemanticGuard"]
RE -->|"valid"| SG
RE -->|"invalid"| EF["EVIDENCE_VALIDATION_FAILED"]
SG -->|"SUPPORTED"| SU["ReleaseOutcome.SUCCESS"]
SG -->|"UNSUPPORTED"| SF["SEMANTIC_UNSUPPORTED"]
SG -->|"unavailable"| UF["SEMANTIC_UNAVAILABLE"]
NC -->|"有 progress"| IF["INSUFFICIENT_EVIDENCE"]
NC -->|"无 progress,有 missing_info"| MF["MISSING_REQUIRED_CONTEXT"]
NC -->|"两者都无"| FAIL["FAILED"]
PS -->|"有 verified facts"| IF
PS -->|"无 verified facts"| FAIL
EF --> FB["ReleaseOutcome.FALLBACK"]
SF --> FB
UF --> FB
IF --> FB
MF --> FB
```
`DiagnosisReleaseResult` 只表达 `SUCCESS` 或 `FALLBACK`。真正不可恢复的 `FAILED / CANCELLED` 由 Chat Application 异常路径映射。
## 10. RunState、ReleaseOutcome 和 FallbackType 映射
| 场景 | RunState | ReleaseOutcome | FallbackType / 内容 |
|---|---|---|---|
| 有结论且 Guards 通过 | `SUCCESS` | `SUCCESS` | Diagnosis report |
| 缺少必要上下文,零 Tool 合法结束 | `SUCCESS` | `FALLBACK` | `MISSING_REQUIRED_CONTEXT` |
| 有限检查后证据不足 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| 信息饱和后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| 协议停止后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
| SemanticGuard 判定不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
| SemanticGuard 技术不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
| 预算耗尽但有安全进展 | `BUDGET_EXHAUSTED` | `FALLBACK` | 当前发布 `INSUFFICIENT_EVIDENCE` |
| 超时,无法形成安全内容 | `TIMED_OUT` | `FAILED` | failure |
| 预算耗尽且无安全进展 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
| 不可恢复内部失败 | `FAILED` | `FAILED` | failure |
| 客户端断开 | `CANCELLED` | `CANCELLED` | 不公开 CANCELLED done |
这张表解释了为什么不能只问“最后 status 是什么”。必须先确定是在问内存执行、发布结果还是 Fallback 原因。
## 11. SSE 连接生命周期
`ChatSseSession` 有自己的连接状态机:
```mermaid
stateDiagram-v2
[*] --> NEW
NEW --> OPEN: onStarted / metadata
NEW --> DISCONNECTED: 客户端提前断开
OPEN --> OPEN: status events
OPEN --> TERMINAL: content + done
OPEN --> TERMINAL: failure + done(FAILED)
OPEN --> DISCONNECTED: send failure / disconnect
TERMINAL --> [*]
DISCONNECTED --> [*]
```
公开事件顺序是:
```text
metadata -> status* -> content | failure -> done
```
边界规则:
- content 最多发送一次;
- result 的 sessionId/runId 必须匹配 metadata;
- TERMINAL 或 DISCONNECTED 后拒绝迟到内容;
- `ReleaseOutcome.CANCELLED` 不作为公开 done outcome;客户端已经断开时没有可靠发送目标;
- 当前 `SseOutcome` 枚举未参与运行时协议,`ChatSseEvent.Done` 使用 `ReleaseOutcome`。
## 12. 持久化生命周期
### 12.1 创建
RunContext 创建后,Application 写入:
```text
diagnosis_run.status = RUNNING
session_id / run_id / query
```
之后记录 intent。
### 12.2 完成映射
`JpaChatRunStore` 根据 ReleaseOutcome 映射数据库通用 status:
| ReleaseOutcome | diagnosis_run.status |
|---|---|
| `SUCCESS` | `SUCCESS` |
| `FALLBACK` | `SUCCESS` |
| `FAILED` | `FAILED` |
| `CANCELLED` | `CANCELLED` |
这里的 `status=SUCCESS` 表示请求已被正常处理并形成安全内容,不表示一定有诊断 conclusion。
同时保存:
- `release_outcome`;
- safe answer JSON;
- 从 safe content 提取的 conclusion;
- duration、Token、Agent step 数和实际 Tool call 数;
- 仅在 `DIAGNOSIS + SUCCESS` 时保存 `published_result`。
Fallback 不进入下一轮 PreviousTurn。
### 12.3 当前可观测缺口
内存 `RunTermination.state/reason` 当前没有独立字段直接持久化。`RUN_FINISHED` 主要记录 ReleaseOutcome 和预算对账,精确的 TIMED_OUT/BUDGET_EXHAUSTED 原因需要结合异常路径和其他 Trace 事件判断。
因此数据库 `status`、ReleaseOutcome 和 Trace 都是必要观察面,任何一个都不是完整替代品。
## 13. Trace Timeline 如何对应生命周期
典型 Diagnosis SUCCESS:
```text
RUN_STARTED
ROUTING_ATTEMPT
ROUTING_DECISION
AGENT_MODEL_STEP
TOOL_INVOCATION / TOOL_PROGRESS ...
EVIDENCE_GUARD_INITIAL
SEMANTIC_GUARD_ATTEMPT
SEMANTIC_GUARD_DECISION
RELEASE_DECISION SUCCESS
RUN_FINISHED SUCCESS
```
典型信息不足 Fallback:
```text
RUN_STARTED
ROUTING_ATTEMPT
ROUTING_DECISION
AGENT_MODEL_STEP
TOOL_INVOCATION / TOOL_PROGRESS ...
COLLECTION_STOP(可选)
EVIDENCE_GUARD_INITIAL(无结论引用检查)
RELEASE_DECISION FALLBACK
RUN_FINISHED FALLBACK
```
`TracePhase` 和 `TraceEventStatus` 用来组织 Timeline。它们描述事件发生在哪一阶段、该事件结果如何,不构成新的 Run 生命周期。
## 14. 并发和迟到结果规则
一次 Run 的终止安全依赖三层协作:
1. `RunLifecycle`:first-terminal-wins,终态不可覆盖;
2. `DiagnosisHarnessCore.checkActive`:每个关键边界阻止终止后的新工作;
3. `ChatSseSession`:TERMINAL/DISCONNECTED 后拒绝迟到 content。
这能保证逻辑上的“取消后不再发布”。它不等于强制终止底层线程或 Provider 计算;底层调用返回后仍要经过 active 和 SSE state 检查,迟到结果才会被丢弃。
## 15. 排障时应该先看哪个状态
| 问题 | 首先看 | 然后看 |
|---|---|---|
| 用户为什么收到 Fallback | `release_outcome + FallbackType` | RELEASE/EVIDENCE/SEMANTIC Trace |
| Agent 为什么停止调用 Tool | `DiagnosisStopReason` | TOOL_PROGRESS、TOOL_REQUEST_REJECTED、COLLECTION_STOP |
| Tool 为什么没有证据 | `InvocationStatus + EvidenceStatus` | canonical record 和 Tool audit error code |
| Run 是超时还是预算耗尽 | 内存 termination(运行中)或相关 Trace/异常 | budget usage、collection stop、失败码 |
| 数据库为什么 status=SUCCESS 但没有结论 | `release_outcome` | answer 的 content type / FallbackType |
| 为什么没有 SSE done | `ChatSseSession.State` | client disconnect/send failure 和 Run cancellation |
| 为什么下一轮没有上一轮上下文 | 是否 `DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result` | PublishedResultPolicy |
## 16. 生命周期不变量
1. 一个 Run 只能有一个 RunTermination。
2. 一个公开 SSE 最多有一次 content/failure 和一次 done。
3. Tool Invocation 只有 READY 才能被引用。
4. READY 必须同时拥有 `EVIDENCE_FOUND` 或 `NO_EVIDENCE`。
5. `NO_EVIDENCE` 不能被提升为全局否定。
6. SATURATED 后不再执行新的 evidence Tool。
7. StopReason 不直接决定公开内容,必须经过 Release。
8. 有 conclusion 才执行完整 EvidenceGuard/Repair/SemanticGuard 链。
9. FALLBACK 是正常发布结果,不等于 RunState.FAILED。
10. 数据库 status、ReleaseOutcome 和 RunState 不能互相替代。
@@ -0,0 +1,374 @@
# Harness 组件全景:职责、设计原因与边界
**更新日期**:2026-07-29
**适用代码**:`src/main/java/com/superbiz/agent/harness`
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**配套主文**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md)
> 本文是完整组件参考手册,不建议第一次接触 Harness 时顺序阅读。入门请先读 [Harness 阅读入口](README.md),需要逐组理解组件时使用[组件渐进式导读](components/README.md)。
## 1. 这份文档怎样定义“全部组件”
当前 `harness` 目录包含 10 个一级职责域、189 个 Java 源文件。它们并不都是独立运行的“服务”:
- **执行组件**拥有行为,例如 Core、Interceptor、Boundary、Guard、Release、Executor;
- **端口与适配器**隔离框架、Redis、JPA、JDBC 和业务 Tool;
- **状态与契约类型**固定跨组件语言,防止字符串协议漂移;
- **Limits、Prompt 和异常类型**把边界配置与失败语义显式化。
因此本篇先解释 10 个职责域为什么存在,再列出每个生产类型。判断某个类应该放在哪里时,只问三个问题:它拥有什么状态、它能作出什么决定、它绝不能决定什么。
## 2. 组件地图
| 职责域 | 文件数 | 解决的问题 | 核心组件 |
|---|---:|---|---|
| `application` | 34 | 谁创建 Run、路由请求、持久化和映射公开结果 | `ChatApplicationUseCase`、三个 Executor、`ChatRunStore` |
| `core` | 14 | deadline、取消、预算和唯一终态由谁拥有 | `DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunLifecycle` |
| `agent` | 17 | 如何把框架 ReAct 接入 Harness,而不复制 ReAct | `DiagnosisAgentUseCase`、Factory、Model/Tool Interceptor |
| `progress` | 14 | 如何识别无增益、重复和协议空转 | `DiagnosisProgressTracker`、Projector、scope normalizer |
| `tool` | 49 | Tool 如何安全执行、保存真相并只暴露必要内容 | `ToolBoundary`、Adapters、Projectors、Canonical Store、MySQL sandbox |
| `guard` | 15 | 如何分开验证引用真实性和结论支持度 | `EvidenceGuard`、`SemanticGuard`、`GuardModelCall` |
| `release` | 6 | 谁拥有最终 SUCCESS / FALLBACK 决策 | `DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory` |
| `retry` | 8 | 哪些失败允许重试、attempt 如何可见 | `HarnessRetryExecutor`、Policies、Failure taxonomy |
| `audit` | 17 | 如何重放决策而不复制敏感正文 | Trace、Tool audit、Model ledger、Agent hook |
| `contract` | 15 | 跨层公开语言如何保持类型化 | Draft、PublishedResult、Fallback、状态枚举 |
```mermaid
flowchart LR
APP["application<br/>Run 与公开用例"] --> CORE["core<br/>执行不变量"]
APP --> AGENT["agent<br/>ReAct 接入"]
AGENT --> PROGRESS["progress<br/>收敛控制"]
AGENT --> TOOL["tool<br/>证据边界"]
APP --> RELEASE["release<br/>唯一发布"]
RELEASE --> GUARD["guard<br/>真实性与支持度"]
CORE --> RETRY["retry<br/>显式 attempt"]
CORE -.-> AUDIT["audit<br/>可观测账本"]
AGENT -.-> AUDIT
TOOL -.-> AUDIT
RELEASE -.-> AUDIT
CONTRACT["contract<br/>类型化语言"] -.-> APP
CONTRACT -.-> AGENT
CONTRACT -.-> TOOL
CONTRACT -.-> GUARD
CONTRACT -.-> RELEASE
```
## 3. Application:Run 的应用所有者
### 为什么需要
Core 只知道一次 Run 是否活跃,并不知道 HTTP、SSE、意图路由、数据库持久化和上一轮上下文。若这些职责塞进 Core,Harness 会变成业务工作流引擎;若散落在 Controller,则每个入口都可能产生不同的终态和 Fallback。
### 核心组件
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `ChatApplicationUseCase` | 为一次请求建立唯一应用事务边界 | 解析 session、读取历史、创建 Run、路由、执行分支、落终态和安全输出 | 不做诊断推理,不自行构造诊断 Fallback |
| `IntentRouter` | 路由也会消耗模型、超时并返回非法 JSON | 在有界输入、timeout 和显式 retry 下输出唯一 `IntentType` | 不执行业务 Tool,不生成最终回答 |
| `SystemChatExecutor` | 系统问答不需要 ReAct,但仍必须受模型预算控制 | 单轮回答产品能力和闲聊 | 不声称查询了实时数据 |
| `KnowledgeQueryExecutor` | 知识问答需要一次 RAG 和一次受控生成,但不需要完整诊断链 | 调用知识 Tool、校验 Tool result、生成带来源答案 | 首版不进入 SemanticGuard,不执行多 Tool 诊断 |
| `DiagnosisChatExecutor` | Agent 执行和安全发布需要一个明确接合点 | 执行 Agent、处理合法停止/非法 Draft、调用 Release、映射公开内容 | 不重复 Guard 或 Release 决策 |
| `ChatRunStore` / `JpaChatRunStore` | 内存 Run 状态与长期数据库状态职责不同 | 保存 Run 开始、intent、结果、预算摘要;读取安全上一轮 | 不保存 canonical raw;只允许安全发布结果进入 PreviousTurn |
| `PublishedResultPolicy` | 直接复用上一轮完整结果会让上下文无限增长并传播失败内容 | 生成可持久化 PublishedResult 和有界 PreviousTurn | Fallback、失败、raw evidence 不进入下一轮 |
### 类型清单
| 类型 | 分类与功能 |
|---|---|
| `ChatApplicationRequest`、`ChatApplicationResult` | 应用入口和出口 DTO;固定 session、run、intent、outcome 和内容类型 |
| `ChatApplicationContent`、`ChatContentType` | 公开内容的 sealed/typed 边界,避免任意对象直接发给 SSE |
| `DiagnosisContent`、`KnowledgeContent`、`SystemChatContent`、`FallbackContent` | 四种公开内容载体;分别包装安全诊断、知识答案、系统回答和 Fallback |
| `ChatApplicationStatus`、`ChatApplicationObserver` | 向 SSE 报告有界阶段,不泄露模型内部步骤 |
| `ChatRunControl` | 只向入口暴露 exact session/run 和客户端断开取消能力 |
| `ChatApplicationException`、`ChatFailureCode` | 把内部异常映射为稳定、可公开的失败语义 |
| `IntentRouting`、`SystemChatOperation`、`KnowledgeQueryOperation`、`DiagnosisOperation` | 四个应用端口;使主用例不依赖具体模型或执行器 |
| `DiagnosisExecutionResult` | Diagnosis 分支的内部返回,携带 outcome、content、published result 和预算已处理标记 |
| `IntentRouterInput`、`IntentRouterLimits`、`IntentRouterPrompt`、`IntentRoutingException` | 路由输入、边界、Prompt 和失败类型 |
| `KnowledgeQueryLimits`、`SingleTurnExecutorLimits` | 知识与单轮模型路径的输入、输出、timeout 上限 |
| `ChatRunStore`、`RoutingHistory` | 持久化端口及最小路由历史 |
| `PreviousTurnLimits`、`PublishedResultPolicy` | 安全历史的字段/大小限制与投影策略 |
## 4. Core:每个 Run 的执行不变量
### 为什么需要
模型、Tool、Guard 和 Application 都要检查取消、deadline 和预算。如果每层各自维护计数或终态,会出现多个真相源;如果依赖 ThreadLocal,则异步线程无法可靠继承。
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `DiagnosisHarnessCore` | 所有执行边界需要同一套 active / budget / terminal 规则 | 创建 RunContext,执行模型/Tool/Token/bytes 门禁,处理取消和终态 | 不持久化,不调用 Agent/Tool,不维护全局 Run Map |
| `RunContext` | Run 身份和状态句柄必须一起显式传播 | 固定 sessionId、runId、deadline 及 per-run handles | record 结构不可变,不代表内部计数不变化 |
| `RunLifecycle` | 成功、失败、取消可能竞态到达 | 原子 `compareAndSet` 实现 first-terminal-wins | 不映射公开 ReleaseOutcome |
| `RunCancellation` | 取消原因和回调只能被第一个请求确定 | first-reason-wins,并通知 lifecycle/资源回调 | 不承诺强杀同步 Provider 请求 |
| `RunBudget` | 多维预算必须原子地先检查再计数 | 模型、Tool、单 Tool、输入/输出/总 Token 和 bytes 计量 | 不判断信息是否有价值 |
| `RunCapacityCounter` | bytes 可能由并发边界累计 | CAS 方式维护 Run 总容量 | 不负责字段级截断策略 |
### 类型清单
| 类型 | 分类与功能 |
|---|---|
| `RunBudgetLimits`、`RunBudgetUsage` | 预算配置和值快照;区分限制与已使用量 |
| `BudgetKind`、`BudgetExceededException` | 明确指出耗尽的是模型、Tool、Token 还是 bytes |
| `RunState`、`RunTermination` | 内存执行状态与不可变终止快照 |
| `RunCancellationReason` | 客户端断开、用户请求、deadline、预算和内部失败的取消分类 |
| `RunAbortedException` | 将已经确定的 RunTermination 穿过深层调用栈,不丢失终态 |
## 5. Agent:框架 ReAct 与 Harness 的接合层
### 为什么需要
业务需要框架原生 Tool Calling 和 ReAct loop,但框架默认并不知道项目的 RunContext、预算、审计、Tool 双视图和停止协议。接合层的目标是“拦截边界”,不是重新实现 Agent 循环。
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `DiagnosisAgentFactory` | 每个 Run 的 interceptor 和 metadata 不同 | 为当前 Run 创建 ReactAgent,注册 Tool callback、Prompt、Hook 和 interceptor | 不缓存跨 Run Agent 状态 |
| `DiagnosisAgentUseCase` | 框架输入输出是字符串,业务要求严格 Draft 和 bytes 边界 | 序列化输入、显式注入 Run metadata、调用 Agent、严格解析 Draft、映射受控停止 | 不执行证据和语义校验 |
| `HarnessModelInterceptor` | 每一轮 ReAct 模型调用都必须进入预算和 Token 账本 | 调用前 reserve,调用后记录 Provider usage 并再次检查 active | 不重试模型 |
| `HarnessToolInterceptor` | 模型 Tool Call 中混有 Harness 进展协议和业务参数 | 校验 Envelope、上一轮增益、重复/饱和、调用 Tool、投影 observation、交付 STOP_REQUIRED | 不执行 backend,不复制 ToolBoundary 预算 |
| `HarnessEvidenceTools` | Tool schema 必须由服务端原生注册,且业务 Tool 可选启用 | 注册 RAG/log/MySQL callback,严格解析通用 Envelope,桥接 Adapter | Prompt 不写死 Tool schema;未配置 MySQL 时不暴露死 Tool |
| `ToolResultViewProjector` | canonical agent_result 仍含 Harness 控制字段 | 生成 Control View 和白名单 Model Observation | 不读取 raw response,不判断根因 |
### 类型清单
| 类型 | 分类与功能 |
|---|---|
| `DiagnosisAgentInput`、`DiagnosisAgentExecution` | Agent 用例输入,以及 Draft/ProgressSnapshot/stop reason 的执行结果 |
| `DiagnosisAgentLimits` | query、previous turn、总输入和 Draft 的 UTF-8 bytes 上限 |
| `DiagnosisAgentPrompt`、`DiagnosisDraftOutputSchema` | 最小职责 Prompt 与严格结构化输出 Schema |
| `EvidenceToolInvoker` | Agent 层到具体 Adapter 的函数端口 |
| `ParsedAgentToolCall` | 解包后的 previous observation、typed business input 和 JSON 参数 |
| `ToolControlView` | Harness 消费的 evidence status、count、scope 等控制视图 |
| `DiagnosisAgentLimitException` | Agent 输入或输出越界 |
| `DiagnosisAgentOutputException` | 空、非法 JSON、Schema 不合格 Draft,并可携带安全 ProgressSnapshot |
| `DiagnosisCollectionStoppedException` | STOP_REQUIRED 后仍请求 Tool 时,把受控停止穿出框架 loop |
## 6. Progress:从资源上限到正常收敛
### 为什么需要
预算只能阻止无限消耗,不能识别“连续查询没有推进诊断”。Progress 子系统只保存 Run 内最小控制状态,不复制完整证据。
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `DiagnosisProgressTracker` | 连续无增益、待评价调用和协议错误需要线程安全单一所有者 | 记录 completed scope、pending evaluation、NO_GAIN、协议错误、饱和和一次停止指令 | 不保存 raw/agent_result,不判断非空内容的业务价值 |
| `ToolScopeNormalizer` | 字段顺序或无关格式不应绕过重复检测 | 将各 Tool typed input 规范化为稳定 scope | 首版不做自然语言语义去重 |
| `DiagnosisProgressProjector` | 受控停止或非法 Draft 后仍需安全说明已检查内容 | 按 Tracker identity 回读 canonical READY 记录,生成有界事实、来源和限制 | 无法验真的记录直接排除,不输出 raw |
| `DiagnosisProgressProjection` | Agent 用例不应依赖具体 Redis projector | 定义 Run 到安全快照的端口,并提供 empty 实现 | 不决定 Fallback 类型 |
### 类型清单
| 类型 | 分类与功能 |
|---|---|
| `InformationGain` | 仅 `GAINED / NO_GAIN`,避免引入含混质量等级 |
| `DiagnosisCollectionState` | `COLLECTING / SATURATED`,只描述信息收集状态 |
| `DiagnosisStopReason` | 区分信息饱和、预算限制和进展协议错误 |
| `PreviousObservation` | 模型在下一次 Tool Call 回传上一轮 `tool_call_id + information_gain` |
| `CompletedToolCall`、`ToolScopeIdentity` | 保存已完成调用的 identity 和规范化 scope,不保存 payload |
| `DiagnosisProgressSnapshotState` | Tracker 的内部控制快照,含计数、pending ID 和停止指令状态 |
| `DiagnosisProgressSnapshot` | Release 可消费的安全快照,含 verified sources、observed facts 和 limitations |
| `ProgressProtocolViolationType`、`ProgressProtocolViolationException` | 缺字段、乱序评价、意外评价和非法 Envelope 的稳定分类 |
## 7. Tool:执行、真相、投影与后端安全
Tool 是文件最多的职责域,但可以按四层理解。
### 7.1 Boundary:所有 Tool 共用的确定性入口
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `ToolBoundary` | 每个 Adapter 自己实现授权、预算、store 和 audit 会产生漂移 | 统一 preflight、active、read-only、Tool budget、bytes、canonical 状态迁移和 audit | 不理解 Tool 业务内容;不做信息增益判断 |
| `ToolCallRequestEnvelope` | 调用必须同时证明 Run、ID、Tool、参数、授权和只读意图 | Boundary 的内部调用信封 | 不等同于模型侧 progress Envelope |
| `ToolExecutor` | Boundary 不依赖具体 backend | raw 执行函数端口 | 不投影结果 |
| `ToolResultProjector` | raw 到 canonical agent_result 的逻辑因 Tool 而异 | 标准化、限制、计算客观 evidence status | 不判断结论支持度 |
| `ToolBoundaryResult` | 只允许 READY 或 ERROR 离开 Boundary | 向上返回 ID、状态、agent result、evidence status 或稳定错误码 | PROJECTING 不对外暴露 |
| `ProjectedToolResult` | projector 同时返回有界 agent result 与客观状态 | Boundary/store 的中间值 | 不是最终 Model Observation |
| `ToolBoundaryErrorCode` | 不能把内部异常正文交给 Agent | 固定非法 ID、Run mismatch、未授权、非只读、超限、执行/投影/store 等错误 | 不包含敏感原因 |
### 7.2 Canonical Store:TTL 内的完整 Tool 真相
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `CanonicalInvocationStore` | Guard 需要独立于 Agent 上下文读取原始调用真相 | 定义 begin/find/markReady/markError 状态端口 | 不负责长期审计 |
| `RedisCanonicalInvocationStore` | 完整 request/raw 有敏感性和容量,适合短期 TTL 存储 | 原子创建,保持剩余 TTL 的状态更新,读取不续期 | 只有该 Adapter 访问 Redis |
| `CanonicalToolInvocation` | request、raw、agent_result 和两个状态必须形成合法组合 | 封装 PROJECTING -> READY/ERROR 转换及 Run 可引用判断 | ERROR/PROJECTING 不可被 EvidenceGuard 引用 |
| `CanonicalInvocationLimits` | Redis record、raw 候选和 agent result 需要独立硬限制 | 统一 UTF-8 bytes 校验 | 不执行截断 |
| `ToolCallKeyFactory` | Key 要隔离 Run 且保留框架 ID | 校验安全 segment,生成 prefix/runId/toolCallId | 不生成或改写 tool_call_id |
| `DuplicateInvocationException`、`InvocationStateException`、`CanonicalStoreException`、`ResultTooLargeException` | Store 失败需要可分类而不是字符串猜测 | 区分重复、非法迁移、基础设施错误和容量错误 | 最终对 Agent 仍映射为安全错误码 |
### 7.3 Adapter 与 Projector:隔离业务后端
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `RagToolAdapter` | 检索实现会演进,但 Agent schema 和 Boundary 不应随之变化 | 解析 RAG request,经 Boundary 调 backend 和 `RagResultProjector` | 不把检索轨迹直接给模型 |
| `QueryLogsToolAdapter` | 现有日志 Tool 的返回和时间语义需要规范化 | 校验范围、桥接 backend、投影日志事件 | 0 命中是 `NO_EVIDENCE`,不是技术失败 |
| `MysqlToolAdapter` | LLM 生成 SQL 必须经过授权和只读沙箱 | 解析 request、先校验 SQL,再经 Boundary 执行和投影 | 未配置数据源时 Tool 不注册 |
| `RagResultProjector` | 上游 evidence、score、relevance 表达不稳定 | 输出有界证据,并保留粗粒度 `relevance_level` | 非空/REFERENCE 不等于支持根因 |
| `QueryLogsResultProjector` | raw 日志不可直接进入上下文 | 生成有界 events、pattern、scope 和截断标记 | 不泄露无限日志正文 |
| `MysqlResultProjector` | JDBC rows 和元数据需要稳定 Agent contract | 限制行列、单元格和 bytes,输出 rows/columns/scope | 不执行 SQL 安全判断 |
| `ToolProjectionLimits`、`MysqlToolLimits` | 各投影边界必须集中、可测试 | 配置证据数、文本、行列和结果上限 | 不改变 Run 总 bytes 预算 |
### 7.4 MySQL 只读沙箱
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `MysqlSqlValidator` | `readOnly=true` 声明不能证明 SQL 安全 | 解析并限制单条 SELECT、数据源、schema/table/column、limit 等 | 不执行查询 |
| `MysqlDataSourceDefinition` | 连接存在不代表 Agent 可访问所有表列 | 保存逻辑数据源和 allowlist | 不携带密码 |
| `MysqlQueryPlan` | 校验后的内容不应在 Executor 再解析原始请求 | 固定已批准数据源、SQL 和限制 | 只能由 Validator 产生 |
| `MysqlReadOnlyExecutor` / `JdbcMysqlReadOnlyExecutor` | JDBC 细节与 Harness Boundary 解耦 | 在只读连接、timeout 和 row limit 下执行 plan | 不接收未经验证的 request |
| `MysqlRawResult` | JDBC 原始但结构化的执行结果 | 携带 columns、rows、truncated、duration | 仍需 Projector 后才能进入 canonical agent_result |
| `MysqlSecurityException` | 安全拒绝必须与基础设施错误区分 | 表达非法 SQL、未授权对象等 | 不向 Agent泄露详细策略 |
### 7.5 Tool contract 完整清单
| 类型 | 设计原因与功能 |
|---|---|
| `AgentToolContracts` | Tool 名、描述和服务端注册契约的唯一常量源,防止 Prompt/代码漂移 |
| `RagToolCall`、`QueryLogsToolCall`、`MysqlToolCall` | 模型侧统一 Envelope:`previous_observation + input` |
| `RagToolRequest`、`QueryLogsRequest`、`MysqlToolRequest` | 业务 Tool 的 typed input;Interceptor 解包后仍保持原业务协议 |
| `RagToolResult`、`QueryLogsToolResult`、`MysqlToolResult` | canonical agent-facing 标准结果,不等同于 raw backend response |
| `RagEvidence`、`SourceDocument` | 有界知识证据及文档身份 |
| `RagRelevanceLevel` | `PRECISE / HIGHLY_RELEVANT / REFERENCE` 等客观检索相关度,不是诊断置信度 |
| `LogQueryScope`、`LogEvent`、`LogPattern` | 日志查询实际范围、事件和聚合模式 |
| `LogSourceKind`、`LogTopic` | 限制日志来源和主题的可选集合 |
| `ToolContractCollections` | 对 contract 集合做 defensive copy 和空值规范化 |
## 8. Guard:把两个验证命题分开
### 8.1 Evidence Guard
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `EvidenceGuard` | 模型不能被信任去验证自己引用的 ID 和 Run 归属 | 严格验证 Draft、analysis ID、canonical READY、当前 Run 所有权、evidence status 和引用闭包 | 不调用模型,不判断结论语义是否成立 |
| `EvidenceGuardResult` | 校验只能是 verified snapshot 或 violations | 阻止半有效结果继续发布 | 不包含原始 Draft 修复逻辑 |
| `VerifiedEvidenceSnapshot` | SemanticGuard 只能看到已验真的证据投影 | 汇总 verified analyses 和 sources | 不包含 raw Tool payload |
| `VerifiedAnalysisEvidence`、`VerifiedEvidence` | 保持 analysis 到证据的归属关系 | 提供来源、scope、excerpt 等安全证据 | 不提升为业务结论 |
| `EvidenceViolation`、`EvidenceViolationCode` | Repair 和 Fallback 需要稳定失败原因 | 标识缺 analysis、未知调用、Run mismatch、非 READY、引用不闭合等 | 不保存内部异常栈 |
### 8.2 Semantic Guard 与模型调用边界
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `SemanticGuard` | 引用真实仍可能无法支持结论 | 用隔离单轮调用输出 `SUPPORTED / UNSUPPORTED`,严格解析并有限 retry | 无 Tool、无记忆、不访问 Redis、不改写 Draft |
| `GuardModelCall` | Router、单轮回答、Repair、Semantic 都需一致的模型预算、timeout 和 bytes 控制 | 在线程池中执行单轮 ChatModel,记录 usage,超时取消 future | 不决定业务 retry policy |
| `SemanticDraftView` | Guard/Repair 只应比较用户可见语义 | 从 Draft 提取稳定语义视图,并判断修复前后是否一致 | 不包含引用实现细节 |
| `SemanticGuardInput`、`SemanticGuardDecision` | 固定 Guard 输入和带 verdict 的输出 | 只传 query、Draft view 和 verified snapshot | 不传 Prompt 历史或 raw Tool response |
| `SemanticGuardLimits`、`SemanticGuardPrompt` | 输入、输出、单次/总 timeout 和判定职责可测试 | 限定一次语义审查的成本与 Prompt | 不暴露给 Diagnosis Agent |
| `GuardModelCallException` | 单轮模型失败要按 timeout、transport、parse、schema 分类 | 为 Harness retry 提供类型化信号 | 不直接映射用户内容 |
## 9. Release:唯一公开决策点
### 为什么需要
如果 Agent、Guard、Application 都可以各自构造结果,同一个失败会出现不同用户语义,迟到内容也可能绕过安全校验。Release 必须集中回答一个问题:当前 Run 有哪些内容可以公开?
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `DiagnosisReleaseUseCase` | Draft、停止、Guard 和 Repair 的组合分支必须只有一个所有者 | 处理有结论、无结论、受控停止、非法 Draft;协调 Guard/Repair/Semantic;返回成功或 Fallback | 不持久化、不发送 SSE、不写新结论 |
| `EvidenceRepair` | 引用或结构小错不应总是丢掉语义正确的 Draft | 单轮修复引用,严格解析,并用 `SemanticDraftView` 保证用户语义不变 | 只 attempt 一次;不新增事实、不改结论 |
| `SafeFallbackFactory` | 失败文案若交给模型生成会再次引入幻觉 | 确定性构造 evidence failed、semantic unsupported/unavailable、insufficient evidence、missing context | 只使用已验真事实和有界问题码 |
| `DiagnosisReleaseResult` | 下游不能同时收到 Draft 和 Fallback | 类型化承载 outcome、Draft、verified evidence 或 SafeFallback | 不等同于 RunState |
| `EvidenceRepairLimits`、`EvidenceRepairPrompt` | Repair 的成本与职责必须比 Diagnosis Agent 更窄 | 限制输入/输出/timeout,固定只修引用的指令 | 不允许 Tool Calling |
## 10. Retry:显式、类型化、可审计的 attempt
### 为什么需要
重试会改变成本、延迟和副作用,必须是调用者的有意识决策。统一 Executor 负责循环,但具体组件持有自己的 Policy。
| 类型 | 设计原因与功能 |
|---|---|
| `HarnessRetryExecutor` | 在每个 attempt 前检查 Run active,统一记录成功/失败,并绝不吞掉取消和预算耗尽 |
| `HarnessRetryPolicies` | 集中定义 Router/SemanticGuard 最多两次,其余一次的严格策略 |
| `RetryPolicy` | `maxAttempts + retryable failures` 的不可变值,避免布尔 `retry=true` |
| `RetryFailure` | timeout、transport、parse、schema、invalid output、cancel、budget 等稳定分类 |
| `RetryAttempt` | 记录 attempt 序号、是否成功和失败类型,供 Trace 使用 |
| `RetryOperation`、`RetryFailureClassifier` | 将执行和异常分类作为端口注入,Executor 不依赖具体模型组件 |
| `RetryExecutionException` | 重试终止时保留最终 attempt 和失败分类 |
## 11. Audit:记录控制事实,而不是复制业务正文
### 为什么需要
诊断系统需要回答“为什么停止、调用了什么、Token 是否对账、发布为何降级”,但普通观察面不应长期保存 Prompt、SQL、日志 query、raw response 或 reasoning。
| 组件 | 为什么设计 | 作用 | 明确边界 |
|---|---|---|---|
| `DiagnosisTraceRecorder` / `JpaDiagnosisTraceRecorder` | 所有阶段需要同一 exact-run timeline | 按 Run 分配 sequence,追加安全事件;失败 best-effort | 不改变业务结果,不保存敏感正文 |
| `TraceAuditEvents` | 各组件手写 details 容易字段漂移或泄露 | 集中构造 run/routing/model/tool/progress/guard/release 事件 | 只接受有界、安全字段 |
| `DiagnosisTraceAuditEvent` | Recorder 与业务组件解耦 | 统一事件 identity、phase、type、status、details | 不是领域事件总线 |
| `ToolInvocationAuditSink` / `JpaToolInvocationAuditSink` | canonical raw 不能长期保存,但调用元数据要留存 | 保存 exact Run、Tool ID、状态、耗时、bytes 和有界 enrichments | 不保存完整 request/raw/agent result |
| `RagLookupAuditEnricher` | RAG 需要有限的质量诊断字段 | 从受限输入提取 step/query/relevance 等安全摘要 | 不打印完整 rewritten/raw query |
| `HarnessAgentAuditHook` | 框架每轮 Agent 模型步骤需关联数据库 step 和 Provider reasoning 可用性 | 写 AgentStep metadata,并把 reasoning/assistant text 放独立受限存储 | reasoning 不进普通 Timeline、Guard 或下一轮上下文 |
| `AgentStepAuditTracker` | Tool audit 需要关联当前 Agent step,但不能使用 ThreadLocal | 按 runId 显式绑定/查询/清理 stepId | 不保存 Step 实体 |
| `ModelCallAuditor` / `ModelCallLedger` | 所有模型入口都要按组件、轮次对账 Token | begin call、记录 Provider usage、写 MODEL_TOKEN_USAGE、生成 Run reconciliation | Usage 缺失时标 unavailable,不估算 |
| `RunConclusionExtractor` | Trace/DB 常需直接读取安全发布结论 | 从 public JSON 提取 conclusion | 不读取 Provider reasoning |
### Audit 类型清单
| 类型 | 分类与功能 |
|---|---|
| `ModelCallComponent` | Router、System、Knowledge、Diagnosis、Repair、Semantic 的统一组件枚举,并映射 Trace phase |
| `ToolInvocationAuditEvent` | durable Tool metadata 事件 |
| `TracePhase`、`TraceEventType`、`TraceEventStatus` | Timeline 的阶段、事件和结果词汇表 |
## 12. Contract:跨组件唯一语言
### 为什么需要
Harness 横跨模型 JSON、Java 对象、Redis、JPA 和 SSE。若状态以自由字符串在各层重复定义,`SUCCESS`、`READY`、`EVIDENCE_FOUND` 很容易被混为一谈。Contract 包将不同维度保持正交。
| 类型 | 为什么存在与表达什么 |
|---|---|
| `DiagnosisDraft` | Diagnosis Agent 唯一结构化输出;analysis 绑定 Tool Call 引用,允许 `conclusion=null` |
| `KnowledgeAnswerDraft` | Knowledge Query 单轮模型输出,答案项绑定文档来源 |
| `PublishedResult` | 只有成功、安全的诊断结果可持久化为下一轮候选 |
| `PreviousTurn` | PublishedResult 的有界历史投影,不是完整会话记录 |
| `SafeFallback` | 确定性公开降级结构,包含 verified sources、observed facts、limitations、next steps 和 validation issues |
| `SourceDocument` | 知识回答中的文档身份与来源 |
| `IntentType` | `SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS` 路由结果 |
| `InvocationStatus` | Tool invocation 生命周期:`PROJECTING / READY / ERROR` |
| `EvidenceStatus` | Tool 客观结果:`EVIDENCE_FOUND / NO_EVIDENCE / ERROR` |
| `SemanticVerdict` | 最终推论支持度:`SUPPORTED / UNSUPPORTED` |
| `ReleaseOutcome` | Harness 发布结果:`SUCCESS / FALLBACK / FAILED / CANCELLED` |
| `SseOutcome` | 当前未被运行时消费的遗留枚举;公开 `done` 实际使用 `ReleaseOutcome`,不要把它作为 SSE 协议真理源 |
| `FallbackType` | evidence validation、semantic、insufficient evidence、missing context 等降级原因 |
| `AnalysisKind` | 区分正向证据分析与限定范围的负向观察 |
| `ContractCollections` | 对公共 contract 集合做 defensive copy、去空和不可变处理 |
## 13. 配置装配:组件如何真正连起来
`HarnessChatConfiguration` 不在 `harness` 包内,但它是运行时组件图的 Composition Root:
- 构造两个有界线程池:Chat worker 与 Harness model executor;
- 从 `ChatHarnessProperties` 创建 Run、预算、停止阈值和各模型调用 limits;
- 装配 Redis canonical store、ToolBoundary、三类 Adapter/Projector;
- 仅在存在有效逻辑数据源时注册 `query_mysql`;
- 为每个 Run 创建带 Model/Tool interceptor 和 Audit Hook 的 Agent;
- 装配 Guard、Repair、Release、Router、三个执行分支和顶层 Application UseCase。
它存在的原因是让所有限制和替换点在一个地方可见。组件内部不应自行读取 Spring 配置或寻找全局 Bean,否则 focused test 很难证明其边界。
## 14. 用调用链快速定位组件
| 想回答的问题 | 首先阅读 | 接着阅读 |
|---|---|---|
| 一次请求如何创建并结束 Run | `ChatApplicationUseCase` | `DiagnosisHarnessCore`、`JpaChatRunStore` |
| ReAct 每轮如何计预算 | `DiagnosisAgentFactory` | `HarnessModelInterceptor`、`ModelCallAuditor` |
| Tool 为什么被拒绝 | `HarnessToolInterceptor` | `DiagnosisProgressTracker`、`ToolBoundary` |
| Tool 结果为何没有原样给模型 | `ToolBoundary` | 具体 ResultProjector、`ToolResultViewProjector` |
| 一条 evidence_ref 如何验真 | `EvidenceGuard` | `CanonicalToolInvocation`、具体 ToolResult contract |
| 为什么最终是 FALLBACK | `DiagnosisReleaseUseCase` | `SafeFallbackFactory`、Trace 的 RELEASE 事件 |
| 为什么诊断停止继续查 | `DiagnosisProgressTracker` | Tool progress / rejection / collection stop Trace |
| Token 为什么对不上 | `ModelCallAuditor` | `ModelCallLedger`、`RUN_FINISHED` reconciliation |
## 15. 组件边界自检
未来新增能力时,可以用下面的问题判断放置位置:
1. 它是在判断业务根因吗?应留在 Diagnosis Agent,而不是 Core/Tool/Guard。
2. 它能由代码机械证明吗?应放在 Boundary、Tracker 或 EvidenceGuard。
3. 它需要完整 Tool 真相吗?读取 canonical store,不要从 Agent observation 反推。
4. 它要决定公开内容吗?只能进入 Release,不要在 Application 或 Guard 私自构造。
5. 它是短期验真数据还是长期运营元数据?前者 canonical,后者 metadata audit。
6. 它会再次调用模型吗?必须进入 Core budget、ModelCallAuditor、timeout 和显式 retry policy。
7. 它引入了新的状态吗?先确认是否只是 error code、stop reason 或 fallback reason,避免再造全局生命周期。
@@ -0,0 +1,163 @@
# Harness 组件学习路线(进度追踪)
**用途**:记录面试准备过程中已了解的 Harness 组件,标记进度,规划下一步。每次学完一个职责域后更新本表。
**依据**:`mvp/engineering/harness/Harness组件全景-职责-设计原因与边界.md`(10 个职责域、189 个文件)
## 0. 学习交流方式与衔接说明(新会话请先读本节)
### 0.1 目标
为**面试准备**深入理解 Harness:不只是知道有哪些组件,要能讲清「为什么这样设计」——每个设计点都有动机(问题)→ 决策 → 代价 → 面试话术。
### 0.2 交流模式(用户与 AI 的协作方式)
1. **逐域学习**:按[学习主线](#3-一次请求的完整学习主线)顺序,一次一个职责域;进度见[第 1 节](#1-进度总览)。
2. **讲解顺序固定**:设计动机(为什么重试权归 Harness)→ 实现细节(真实代码)→ 面试话术。
3. **用户会用自己的话复述理解**(「我理解下...」)——AI 需逐条核对:基本正确就确认 + 精修表述;有偏差要明确指出并给出修正后的说法。
4. **用户会追问**(「为什么...」「如果...那...」)——AI 必须基于源码事实回答(`src/main/java/com/superbiz/agent/harness`),先读代码再答,不凭印象。
5. **概念分不清时用户会要求回到底层概念**(如「副作用幂等是什么」)——用类比 + 具体例子讲透再回到主线。
6. **每学完一个主题沉淀成 mermaid 文档**,放本目录 `mvp/engineering/harness/`(与已有笔记同风格:用途/图/表/面试话术/代码位置),并更新本路线图。
7. **终端对话中不输出 mermaid**(用户终端显示不了,用 ASCII 树/表格);落地文档中用 mermaid。
8. 回复用中文、不用 emoji、重要内容(代码/表/推理)不截断。
### 0.3 新会话衔接步骤
```text
1. 读本路线图:第 0 节(交流方式)+ 第 1 节(进度)+ 第 4 节(下一步)
2. 读「已产出笔记」里的文档,了解已学内容的深度(尤其是 core / retry)
3. 从第 4 节「下一步规划」继续,保持 0.2 的交流模式
```
### 0.4 当前会话的起始上下文(供追溯)
本次学习从 Harness 入口文档开始,已完整走过:入口导读 → 面试速查 → RunContext → 执行控制(budget/cancel/lifecycle/checkActive)→ RunBudget 深挖 → retry → progress(设计+代码双视角)→ tool 域(49 文件全注释 + 注册调用执行链路 + Tool 调用链旅程)→ RAG 检索体系(lookup_knowledge 后端:L0/多路召回+RRF/qualityScore/降级/契约/审计/离线评测,已闭环)。当前停在「tool 域只差 MySQL 沙箱线,下一步 tool 收尾」的位置。
### 0.5 面试准备策略(学习目标)
**学每个域的达标标准**(不只是「看懂了」):
```text
1. 能 2 分钟讲清该域:为什么存在 → 核心机制 → 边界/代价
2. 能接住 3 个追问:动机追问(为什么这样)→ 细节追问(怎么实现)→ 边界追问(什么不做)
3. 有一句背得出的面试话术(每篇笔记都有「面试话术」章节)
```
**每个域的面试讲法模板(固定叙事结构)**:
```text
① 动机:不这么做会出什么问题(问题驱动,不要先报组件名)
② 决策:选了什么方案、放弃了什么(对比)
③ 实现:关键机制 + 代码事实(一句话带过实现细节)
④ 边界:明确不做什么、代价是什么(诚实)
⑤ 话术:一段 30 秒可背诵的回答
```
**高频追问地图**(面试被问到时先答哪篇):
| 面试问题 | 答案指向 |
|---|---|
| 什么是 Harness?30 秒讲清 | [面试速查](Harness面试速查-一张图讲清设计.md) §1-2 |
| 为什么不用多 Agent? | [设计演进](Harness设计演进-从多Agent编排到确定性控制边界.md) |
| RunContext 为什么要显式传递? | [执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md) §2 |
| 取消是强杀吗? | [执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md) §6-8 |
| 预算和 Ledger 有什么区别? | [RunBudget 时序图](RunBudget预算流程-一次Run的资源门禁时序图.md) §5 |
| FALLBACK 算成功还是失败? | 状态流(未沉淀,学完后补) |
| 重试为什么归 Harness 管? | [Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md) §2 |
| 为什么 Agent/Tool 不重试? | [Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md) §9 |
| 如何防止 Agent 编造证据? | 证据安全链(tool/guard 学完后补) |
**面试总复习路径**(面试前一天):
```text
1. 30 秒电梯陈述 + 一张图(面试速查 §1-2)
2. 默画三张白板图:主链路、职责迁移、数据三层(面试速查 §2)
3. 过一遍六个易错点(面试速查 §8)
4. 2 分钟真实案例(支付超时)
5. 每篇笔记的「面试话术」章节快速背诵
```
## 1. 进度总览
| 职责域 | 作用摘要 | 状态 | 已深入了解 | 对应文档 |
|---|---|---|---|---|
| `core` | **执行控制**:身份 / deadline / 预算 / 取消 / 唯一终态,checkActive 三道闸 | ✅ 深入 | RunContext、budget、cancel、lifecycle、checkActive、termination | [执行控制笔记](Harness执行控制笔记-终态检查与取消广播.md)、[RunBudget 时序图](RunBudget预算流程-一次Run的资源门禁时序图.md) |
| `retry` | **显式可计量重试**:分类裁决(技术/业务)、次数/时间/成本三重封顶、attempt 可审计 | ✅ 深入 | 设计动机、分类裁决、剩余超时、幂等性、SDK 关闭 | [Retry 重试机制](Retry重试机制-显式可计量的attempt循环.md) |
| `contract` | **跨层类型化语言**:Draft / PublishedResult / SafeFallback / 状态枚举,防字符串漂移 | ✅ 深入 | 11 个状态枚举五层全景、四个正交轴(RunState⊥ReleaseOutcome、InvocationStatus⊥EvidenceStatus)、纵向映射链、SseOutcome 未接线发现 | [状态流笔记](Harness%20contract%20状态流学习笔记-11个状态枚举的正交全景.md) |
| `agent` | **框架 ReAct 接入**:拦截器把预算/审计/停止协议挂到框架循环上,不复制 loop | ✅ 深入 | 装配(Factory 粘合点)、双拦截器(Model:预算+Token 审计;Tool:五道门)、UseCase 循环外壳(字节/预算限制)、受控停止(异常栈捞回可控信号)、双视图投影(模型观察 vs 控制视图)、串行工具 | [agent 域学习笔记](Harness%20agent%20域学习笔记-从框架%20ReAct%20接入到受控停止.md) |
| `audit` | **可观测账本**:Trace 事件回放、Token 对账、metadata-only(不存敏感正文) | ✅ 深入 | trace 时序线(15 帧真实数据)、Ledger 分账、模型步审计 hook、RunConclusionExtractor、DiagnosisTraceService 三级回放 | [application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) |
| `application` | **Run 应用所有者**:创建 Run / 路由意图 / 执行分支 / 持久化 / SSE 输出 | ✅ 深入 | 六步编排、取消句柄(CoreRunControl)、统一失败出口、多轮记忆有界化、PublishedResultPolicy 落库 | [application+audit 笔记](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) |
| `guard` | **验证分离**:EvidenceGuard 机械验引用真实性 + SemanticGuard 隔离判结论支持度 | ✅ 深入 | 20 个违规码、三层校验(结构/验真/重读投影)、语义不变性、守卫模型受控调用、全栈衔接 | [证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) |
| `release` | **唯一发布点**:SUCCESS / FALLBACK 裁决,EvidenceRepair 只修引用,SafeFallback 确定性构造 | ✅ 深入 | 三分支决策树、fail closed、终态透传、EvidenceRepair 语义不变性、SafeFallbackFactory 五种降级 | [证据安全链笔记](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) |
| `tool` | **证据边界**:ToolBoundary 统一执行规则、canonical 保存真相、projector 有界投影、MySQL 只读沙箱 | ✅ 深入 | 49 文件全注释、Boundary/Contract/Projection/Store/Adapter/注册链、RAG 后端(L0/RRF/qualityScore/降级)、MySQL 沙箱(Validator/Executor/Projector 三层防线) | [tool 域注册调用执行链路](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md)、[Tool 调用链旅程](Harness%20Tool%20调用链-一次工具调用的完整旅程.md)、[RAG 检索体系](Harness%20RAG%20检索体系学习笔记-从%20query%20到可验证证据.md)、[MySQL 沙箱](Harness%20MySQL%20沙箱学习笔记-从%20SQL%20校验到脱敏投影.md) |
| `progress` | **收敛控制**:信息增益(GAINED/NO_GAIN)、重复检测、饱和停止(预算之外的第二套停止机制) | ✅ 深入 | 设计动机、Tracker 双计数/pending/软硬停止、拦截器五道门、canonical 生命周期、Projector 投影、Release 消费 | [代码学习笔记](Harness progress 代码学习笔记-从拦截器五道门到唯一发布点.md)(与[设计视角](Harness信息增益停止-让无证据诊断正常收敛.md)配套) |
图例:✅ 深入 = 已完整学透,能面试讲 2 分钟;⬜ 部分 = 接触过但没系统学;⬜ 空白 = 未开始
## 2. 已产出笔记
| 文档 | 内容 | 状态 |
|---|---|---|
| [Harness 执行控制笔记-终态检查与取消广播](Harness执行控制笔记-终态检查与取消广播.md) | RunContext / checkActive / 两个 CAS / 取消广播 / 打断机制 | ✅ 已沉淀 |
| [RunBudget 预算流程-一次 Run 的资源门禁时序图](RunBudget预算流程-一次Run的资源门禁时序图.md) | RunBudget 时序图 / 字段组件 / 异常终态 / 三要素 | ✅ 已沉淀 |
| [Retry 重试机制-显式可计量的 attempt 循环](Retry重试机制-显式可计量的attempt循环.md) | Retry 设计动机 / 分类裁决 / 剩余超时 / 幂等性 | ✅ 已沉淀 |
| [Harness 信息增益停止-让无证据诊断正常收敛](Harness信息增益停止-让无证据诊断正常收敛.md) | progress 设计视角:双停止机制 / 三方判断权 / 状态机 / 协议 / 真实问题 | ✅ 已沉淀(设计视角) |
| [Harness progress 代码学习笔记-从拦截器五道门到唯一发布点](Harness%20progress%20代码学习笔记-从拦截器五道门到唯一发布点.md) | progress 代码视角:类地图 / 五道门 / Tracker 状态机 / canonical 生命周期 / 门禁 / 投影发布 / 易错点 / 面试话术 | ✅ 已沉淀(代码视角) |
| [Harness tool 域代码学习笔记-工具的注册调用与执行链路](Harness%20tool%20域代码学习笔记-工具的注册调用与执行链路.md) | tool 域:装配/注册(bridge/callbacks)/调用/执行(ToolBoundary)/返回全链路 + 面试话术 | ✅ 已沉淀 |
| [Harness Tool 调用链-一次工具调用的完整旅程](Harness%20Tool%20调用链-一次工具调用的完整旅程.md) | 动态时序:模型决定 → 拦截器 → invoke → Adapter → ToolBoundary → 返回 → 模型观察 | ✅ 已沉淀 |
| [Harness RAG 检索体系学习笔记-从 query 到可验证证据](Harness%20RAG%20检索体系学习笔记-从%20query%20到可验证证据.md) | RAG 后端:L0/多路召回+RRF/qualityScore/去重判级/降级/契约/审计/离线评测/讨论沉淀 | ✅ 已沉淀 |
| [Harness MySQL 沙箱学习笔记-从 SQL 校验到脱敏投影](Harness%20MySQL%20沙箱学习笔记-从%20SQL%20校验到脱敏投影.md) | MySQL 工具:三层防线(语义/连接/输出)/白名单/参数化强制/取消联动/脱敏/与 RAG 对照 | ✅ 已沉淀 |
| [Harness 证据安全链学习笔记-从收敛控制到唯一发布点](Harness%20证据安全链学习笔记-从收敛控制到唯一发布点.md) | progress→guard→release 联动:双通道验证架构/六层状态流转与映射/关键字段来源与使用/设计要点/面试话术 | ✅ 已沉淀 |
| [Harness application+audit 学习笔记-从 Run 编排到可回放审计](Harness%20application+audit%20学习笔记-从%20Run%20编排到可回放审计.md) | application 六步编排/取消句柄/持久化策略;audit 域层次(trace 子体系/Ledger/审计表);**audit vs trace 区别**(真实数据对照)/三级回放 | ✅ 已沉淀 |
| [Harness contract 状态流学习笔记-11个状态枚举的正交全景](Harness%20contract%20状态流学习笔记-11个状态枚举的正交全景.md) | 五层状态/四正交轴/纵向映射链/真实数据案例/面试叙事模板与追问应对 | ✅ 已沉淀 |
| [Harness 面试复习笔记-五步复习与白板图沉淀](Harness%20面试复习笔记-五步复习与白板图沉淀.md) | **详细版**:30 秒陈述展开/三张白板图/九域五段式讲法(动机→决策→实现→边界→话术)/六易错点带原因/追问应对大全/支付超时案例/纠正认知清单/面试 Checklist | ✅ 已沉淀 |
| [Harness agent 域学习笔记-从框架 ReAct 接入到受控停止](Harness%20agent%20域学习笔记-从框架%20ReAct%20接入到受控停止.md) | agent 域:装配(Factory 粘合点)/双拦截器(Model 预算+Token 审计、Tool 五道门)/UseCase 循环外壳/受控停止/双视图投影/串行工具 | ✅ 已沉淀 |
| [Harness LLM Judge 设计笔记-从不可信判定到可信裁决](Harness%20LLM%20Judge%20设计笔记-从不可信判定到可信裁决.md) | LLM-as-a-judge 模式:SemanticGuard(判支持度)+ EvidenceRepair(修引用)+ GuardModelCall(受控底座);面试五段式回答稿 + 六追问应对 + 通用要素 | ✅ 已沉淀 |
| [Harness 整体架构学习笔记-从装配到入口到记忆到知识库写入](Harness%20整体架构学习笔记-从装配到入口到记忆到知识库写入.md) | 整体架构补充:配置装配中心(三层组织)/ HTTP 入口层(薄 Controller + SSE 状态机 + 断连取消)/ 会话与记忆体系(PreviousTurn 注入 + 术语校准 + skill 长期记忆)/ 知识库写入链路(分块 + hybrid 同源) | ✅ 已沉淀 |
## 3. 一次请求的完整学习主线
```mermaid
flowchart LR
A["core<br/>执行控制 ✅"] --> B["retry<br/>重试 ✅"]
B --> C["progress<br/>信息增益 ✅"]
C --> D["tool<br/>事实边界 ✅"]
D --> E["guard<br/>验证 ✅"]
E --> F["release<br/>发布 ✅"]
F --> G["application + audit<br/>收尾 ✅"]
G --> H["contract<br/>类型化语言 ✅"]
```
## 4. 下一步规划
```text
主线九域全部 ✅(含 agent 域收尾)+ 面试五步复习 ✅(共沉淀 14 篇笔记)
面试前一天建议:
1. 重读「面试速查」§1-2 + §8(30 秒陈述 / 一张图 / 六易错点)
2. 默画三张白板图(复习笔记 §3)
3. 背诵每域 30 秒话术(复习笔记 §5)
4. 过一遍纠正的认知清单(复习笔记 §6,最容易踩的坑)
5. 2 分钟支付超时案例(复习笔记 §7)
可选深化(不阻塞面试):
1. audit 域深化:RagLookupAuditEnricher 检索审计明细(已覆盖大半)
2. LLM Judge 设计(已沉淀:面试问答 + 追问应对)
3. 整体架构补充(已沉淀:装配/入口/记忆体系/知识库写入)
```
## 5. 建议每次学完一个域后更新
```text
1. 把本表"状态"从 ⬜ 改为 ✅/⬜
2. 在"已深入了解"列补充该域的关键类
3. 如产出笔记,加入"已产出笔记"表
```
## 6. 参考资料索引
| 文档 | 用途 |
|---|---|
| [Harness 面试速查-一张图讲清设计](Harness面试速查-一张图讲清设计.md) | 面试主叙事(30 秒回答、三大决策、易错点) |
| [Harness 组件全景-职责-设计原因与边界](Harness组件全景-职责-设计原因与边界.md) | 全部组件的参考手册(需要查类时用) |
| [components/README.md](components/README.md) | 组件渐进式导读入口(02-04 对应 progress/tool/guard+release) |
| [Harness 设计-非确定性 Agent 的确定性控制边界](Harness设计-非确定性Agent的确定性控制边界.md) | 设计主文档(决策总表、不变量) |
@@ -0,0 +1,503 @@
# Harness 设计:非确定性 Agent 的确定性控制边界
**更新日期**:2026-07-29
**适用范围**:当前 MVP Chat / Diagnosis Harness
**代码基线**:`com.superbiz.agent.harness`
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
**组件索引**:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md)
## 1. 结论先行
这个系统真正需要解决的,不是“怎样让模型多调用几个 Tool”,而是:
> 当业务推理由非确定性模型完成时,怎样保证每一次执行仍然有身份、有边界、有停止条件、有证据闭包,并且只发布系统能够负责的内容。
当前 Harness 给出的答案是职责分治:
- Diagnosis Agent 负责提出假设、选择 Tool、理解结果和撰写 Draft。
- Harness 负责所有必须确定的事情:Run 生命周期、预算、取消、Tool 授权、结果隔离、停止、验真、发布和审计。
- Tool 只报告客观执行结果,不宣称业务根因。
- Guard 不重新做诊断,只回答受限的验证问题。
- Release 是唯一对外发布决策点,只能发布原始安全 Draft 或确定性 SafeFallback。
这不是一个新的工作流引擎。Harness 不复制 ReAct 循环,不维护 Planner / Executor / Composer 图,也不替模型判断根因。它包围 ReAct 的不确定部分,在所有外部副作用和最终发布点建立确定性门禁。
## 2. 根问题:模型能推理,但系统必须能够负责
### 2.1 旧架构把“推理角色”当成了“安全边界”
旧方案使用 Planner、Executor、Verifier、Composer 等多个 Agent 串联。表面上每个角色各司其职,实际上每个角色都拥有一部分 Prompt、模型调用、Schema 转换、重试和 Fallback 逻辑。结果是:
1. 一次诊断的控制权分散在多个模型角色和业务 Service 中。
2. 同一种错误可能被不同层重试、改写或吞掉。
3. Verifier 既检查引用,又判断语义,还可能重写报告,成为第二个诊断者。
4. Tool 原始结果、Agent 上下文和长期审计没有明确的数据边界。
5. Run 身份依赖 ThreadLocal 传播,跨线程后无法证明一次调用属于哪个 Run。
```mermaid
flowchart LR
U["用户请求"] --> P["Planner Agent"]
P --> E["Executor Agent"]
E --> T["Tool / raw result"]
E --> V["Verifier Agent"]
V --> C["Composer Agent"]
C --> O["公开结果"]
P -.-> X1["独立 Prompt / retry / schema"]
E -.-> X2["独立 Prompt / retry / fallback"]
V -.-> X3["验真、语义判断、改写混合"]
C -.-> X4["再次生成用户事实"]
T -.-> X5["raw、trace、正文边界不清"]
H["ThreadLocal context"] -.-> P
H -.-> E
H -.-> V
classDef risk fill:#fff1f0,stroke:#cf1322,color:#5c0011;
class X1,X2,X3,X4,X5,H risk;
```
问题并不在于“Agent 数量多”本身,而在于控制职责没有单一所有者。只要预算、重试、证据和发布仍分散,多 Agent 换成 Graph 也不会自然变得可靠。
### 2.2 单 Agent 仍然不等于受控系统
把多 Agent 合并成一个 ReAct Agent,只消除了重复推理链,并没有自动解决以下问题:
- 模型可能无限改写相似查询;
- SDK 可能在 Harness 之外自动重试;
- Tool 可能返回过大、敏感或不可引用的原始数据;
- 模型可以引用其他 Run 或不存在的 Tool Call;
- 证据引用真实,不代表结论被证据支持;
- 客户端断开后,迟到的模型结果仍可能覆盖终态;
- “没有足够证据”可能被错误地发布为技术失败。
所以重构的核心不是“单 Agent”,而是“单 Agent + 确定性 Harness”。
## 3. 设计约束与不变量
以下不变量是组件拆分的依据,不是实现后的总结。
| 不变量 | 系统含义 | 由谁保证 |
|---|---|---|
| 一个 Run 只有一个终态 | 成功、失败、取消、超时、预算耗尽不能相互覆盖 | `RunLifecycle` 的 first-terminal-wins |
| 所有边界显式携带 Run 身份 | 不依赖线程绑定的隐式上下文 | `RunContext` 和 framework `tool_call_id` |
| 所有模型与 Tool 消耗可计量 | SDK 隐式 retry 不能绕过预算 | Core、Model/Tool interceptor、Harness retry |
| Tool raw 不直接进入模型 | 内部数据、体积和控制字段不能污染上下文 | ToolBoundary、canonical store、双视图投影 |
| 只有当前 Run 的 READY 调用可被引用 | 防止伪造、串 Run 和引用失败调用 | EvidenceGuard |
| 引用真实性与结论支持度分开 | 确定性规则和语义判断不互相冒充 | EvidenceGuard、SemanticGuard |
| Guard 不写报告 | 防止验证器成为第二个业务 Agent | Release Policy |
| 停止权属于 Harness | Prompt 建议不能替代系统保证 | ProgressTracker、预算和 interceptor |
| 没有根因是合法业务结果 | 证据不足不应伪装成内部故障 | `conclusion=null`、ProgressSnapshot、SafeFallback |
| 普通审计不保存敏感正文 | 可观测性不能以泄露 Prompt、raw、reasoning 为代价 | metadata-only audit、reasoning 独立存储 |
## 4. 当前总体架构
```mermaid
flowchart TB
subgraph Entry["应用入口与 Run 所有权"]
APP["ChatApplicationUseCase"]
ROUTER["IntentRouter"]
EXEC["DiagnosisChatExecutor"]
STORE["ChatRunStore"]
end
subgraph Harness["确定性 Harness 边界"]
CORE["Core<br/>context / lifecycle / budget / cancel"]
MI["Model Interceptor<br/>调用预算与 Token"]
TI["Tool Interceptor<br/>协议、重复、停止"]
TB["ToolBoundary<br/>授权、执行、canonical"]
PROGRESS["ProgressTracker<br/>信息增益与饱和"]
EG["EvidenceGuard<br/>引用真实性"]
SG["SemanticGuard<br/>结论支持度"]
RELEASE["Release Policy<br/>SUCCESS / FALLBACK"]
AUDIT["Audit / Trace<br/>metadata-only"]
end
subgraph Nondeterministic["非确定性区域"]
AGENT["Diagnosis ReAct Agent"]
MODEL["Chat Model"]
end
subgraph Evidence["证据执行与存储"]
ADAPTER["RAG / Logs / MySQL Adapter"]
BACKEND["Evidence Backend"]
CANON["Redis Canonical Invocation"]
META["MySQL Durable Metadata"]
end
APP --> CORE
APP --> ROUTER
ROUTER --> MODEL
APP --> EXEC
EXEC --> AGENT
AGENT --> MI --> MODEL
AGENT --> TI
TI --> PROGRESS
TI --> TB --> ADAPTER --> BACKEND
TB --> CANON
TB --> META
TB --> TI --> AGENT
EXEC --> EG --> SG --> RELEASE
PROGRESS --> RELEASE
RELEASE --> APP --> STORE
CORE -.-> MI
CORE -.-> TI
CORE -.-> TB
CORE -.-> EG
CORE -.-> SG
APP -.-> AUDIT
MI -.-> AUDIT
TI -.-> AUDIT
TB -.-> AUDIT
EG -.-> AUDIT
SG -.-> AUDIT
RELEASE -.-> AUDIT
```
图中的关键边界有三条:
1. **Agent 之前**:Application 创建 Run,Core 固定身份、预算和终止语义。
2. **Tool 前后**:Interceptor 管控制协议,ToolBoundary 管执行与真相,Projector 管模型可见内容。
3. **发布之前**:EvidenceGuard 验引用,SemanticGuard 验推论,Release 决定唯一公开内容。
## 5. 关键决策及设计推导
### 5.1 决策一:只保留一个拥有 Tool loop 的 Diagnosis Agent
**问题**:多角色 Agent 把同一份业务上下文在多个模型之间传递,每一跳都可能增加信息损失、重试和幻觉面。
**候选方案**:
| 方案 | 优点 | 主要问题 |
|---|---|---|
| 保留 Planner / Executor / Verifier / Composer | 角色概念直观 | 控制权分散;多套 Prompt 和重试;Verifier/Composer 会产生新事实 |
| 外层自建 ReAct / StateGraph | 流程显式 | 与框架 ReAct 重复;状态和异常路径翻倍 |
| 单 ReAct Agent + Harness | 推理上下文连续;控制边界集中 | 对 Harness 的契约和门禁设计要求更高 |
**决策**:Diagnosis Agent 是唯一业务推理者和 Draft 作者,使用框架 `ReactAgent` 自己完成 Thought / Action / Observation。项目不在外层复制循环。
**代价**:单 Agent 并不能通过“角色互审”获得表面冗余,因此必须把可机械验证的安全要求下沉到 Guard,把语义审查收缩成隔离的单轮判断。
### 5.2 决策二:RunContext 显式传播,结构不可变、状态句柄线程安全
**问题**:旧 ThreadLocal 可以在同步调用中工作,但异步 Tool、线程池和取消回调跨线程后,调用方无法证明读到的是当前 Run 的上下文。
**容易走错的方案**:把所有字段都做成不可变值对象。这样 deadline 和 identity 很干净,但预算、取消和终态不得不在外部另建全局 Map,反而形成第二真相源。
**决策**:`RunContext` 本身是 record,固定 `sessionId / runId / deadline` 和各状态句柄引用;预算、取消、生命周期、模型账本和进展 Tracker 在各自线程安全对象内变化。
```mermaid
flowchart LR
RC["RunContext<br/>结构不可变"] --> ID["sessionId / runId / deadline"]
RC --> C["RunCancellation<br/>first-reason-wins"]
RC --> B["RunBudget<br/>同步复合计数"]
RC --> L["RunLifecycle<br/>first-terminal-wins"]
RC --> M["ModelCallLedger<br/>组件/轮次账本"]
RC --> P["ProgressTracker<br/>Run 内进展"]
```
**为什么不是全局 Run Registry**:Harness 只控制当前调用,不承担 Run 查询和持久化;持久化真相仍由 `diagnosis_run` 所有,避免 Core 演变成工作流引擎。
### 5.3 决策三:关闭 SDK 隐式 retry,由 Harness 按失败类型拥有 retry
**问题**:Spring AI 默认 `maxAttempts=10`。如果 SDK 在模型边界内部自动重试,Harness 看到的一次调用可能对应多个 Provider attempt,预算、延迟、Trace 和取消都失真。
**决策**:底层 SDK retry 设为 1;仅在 Harness 中使用类型化策略:
- IntentRouter:超时、传输、非法输出最多 2 次 attempt;
- SemanticGuard:超时、传输、解析或 Schema 问题最多 2 次 attempt;
- Diagnosis Agent、业务 Tool、EvidenceRepair:1 次,不自动重试;
- 取消、预算耗尽、`NO_EVIDENCE`、业务拒绝:从不重试。
**设计理由**:重试不是通用容错开关。只有调用者知道一次失败是否幂等、是否还在 deadline 内、是否应该再次消耗预算。
**代价**:Provider 瞬时抖动更容易直接暴露,但真实 attempt 终于可计算、可审计,且不会把业务无证据误判为技术重试条件。
### 5.4 决策四:Tool 使用 canonical truth 与 Agent projection 双视图
**问题**:同一份 Tool 返回同时服务三个目标,而三个目标互相冲突:
- 证据验真需要完整、稳定、按 Run 归属的记录;
- Agent 只需要完成下一步推理的最小内容;
- 长期审计应保留身份和耗时,但不能长期保存敏感 raw。
**决策**:把 Tool 数据分成三层,而不是让一个 JSON 到处流转。
```mermaid
flowchart LR
REQ["framework tool_call_id<br/>typed request"] --> B["ToolBoundary"]
B --> RAW["backend raw response"]
RAW --> CAN["Redis canonical invocation<br/>request + raw + agent_result<br/>短 TTL"]
RAW --> PRJ["Tool-specific projector"]
PRJ --> CTRL["Harness control view"]
PRJ --> OBS["Agent observation<br/>白名单、有界"]
B --> META["Durable audit<br/>identity / status / latency / bytes"]
CTRL --> STOP["重复、NO_GAIN、停止控制"]
OBS --> AGENT["Diagnosis Agent"]
CAN --> GUARD["EvidenceGuard"]
```
**关键取舍**:
- 使用框架 `tool_call_id`,Harness 不生成第二套 ID;
- `PROJECTING / READY / ERROR` 表达调用生命周期;
- `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` 表达客观结果语义;
- raw 超限直接失败,不静默截断;Agent projection 可以有界截断,但必须标记 `truncated`;
- Redis 是 TTL 内完整 Tool 真相源,MySQL 只做长期 metadata audit;
- Redis 读取不续期,避免一次历史读取无限延长敏感 raw 生命周期。
**代价**:同一次 Tool 调用需要 projector、store 和 audit 三套表达。但它们不是重复数据模型,而是分别回答“真实发生了什么”“模型允许看到什么”“长期允许保留什么”。
### 5.5 决策五:EvidenceGuard、SemanticGuard 和 Release 三段分工
**问题**:“引用是真实的”和“结论被引用支持”是两个不同命题。只做前者会放过牵强推论;都交给模型则无法确定性防止伪造 ID、跨 Run 引用或修改报告。
**决策**:
1. `EvidenceGuard` 使用纯代码检查 Draft 结构、analysis ID、Tool Call 当前 Run 所有权、READY 状态、evidence status 和引用闭包。
2. 首次引用失败时,`EvidenceRepair` 只允许修复结构和引用;修复前后用户可见语义必须一致,然后重新执行 EvidenceGuard。
3. `SemanticGuard` 只接收 query、完整 Draft 和 verified snapshot,进行无 Tool、无记忆的单轮 `SUPPORTED / UNSUPPORTED` 判断。
4. `DiagnosisReleaseUseCase` 是唯一发布决策点。Guard 无权改写最终报告。
```mermaid
sequenceDiagram
participant A as Diagnosis Agent
participant R as Release UseCase
participant E as EvidenceGuard
participant C as Canonical Store
participant X as EvidenceRepair
participant S as SemanticGuard
participant P as Public Result
A->>R: DiagnosisDraft
R->>E: validate draft references
E->>C: read exact run/tool_call_id
C-->>E: READY agent_result
alt 引用或结构错误
E-->>R: violations
R->>X: 原 Draft + violations
X-->>R: 语义不变的修复 Draft
R->>E: revalidate once
end
alt 证据闭包有效
E-->>R: verified snapshot
R->>S: query + draft + snapshot
S-->>R: SUPPORTED / UNSUPPORTED
end
alt SUPPORTED
R-->>P: 原始安全 Draft
else 任一门禁失败
R-->>P: deterministic SafeFallback
end
```
**为什么 Repair 不能修正文义**:一旦 Repair 可以修改结论,它就成为新的报告作者;此时最终内容不再是 Diagnosis Agent 的 Draft,也无法证明修复只解决了引用问题。
**为什么 SemanticGuard 不是第二个 Agent**:它没有 Tool、历史记忆或 ReAct loop,只回答一个受约束的二值审查问题,不能探索新事实或生成新结论。
### 5.6 决策六:资源预算和信息增益是两套停止机制
**问题**:预算只能回答“还能不能花资源”,不能回答“继续查询还有没有价值”。知识库只有通用资料、日志持续为空时,Agent 可以在预算范围内不断换关键词,最终以 `BUDGET_EXHAUSTED` 结束。用户得到的是技术失败,而系统实际已经知道“当前范围证据不足”。
**决策**:
- Tool 负责客观事实:是否执行成功、是否为空、实际 scope;
- Harness 机械判定空结果和完全重复的 `tool_name + normalized_scope` 为 `NO_GAIN`;
- 其他成功非空结果由模型在下一次 Tool Call Envelope 中声明 `GAINED / NO_GAIN`;
- `DiagnosisProgressTracker` 维护连续 `NO_GAIN`,达到阈值后进入 `SATURATED`;
- 连续 Envelope 协议错误使用独立计数和 `PROGRESS_PROTOCOL_VIOLATED`,不伪装成无信息增益;
- STOP_REQUIRED 只交付一次,再次请求 Tool 直接受控终止;
- `INFORMATION_SATURATED` 与 `BUDGET_LIMIT_REACHED` 始终分开记录。
```mermaid
stateDiagram-v2
[*] --> COLLECTING
COLLECTING --> COLLECTING: GAINED / 清零 NO_GAIN
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值
COLLECTING --> SATURATED: 连续协议错误达阈值
COLLECTING --> STOPPED: 硬预算到达
SATURATED --> DRAFT_CHANCE: 单次 STOP_REQUIRED
DRAFT_CHANCE --> STOPPED: 再次请求 Tool
DRAFT_CHANCE --> RELEASE: 输出合法 Draft
STOPPED --> RELEASE: ProgressSnapshot
```
**为什么不用 `new_count`**:跨 RAG、日志和数据库建立统一内容指纹成本高,而且“新记录”不等于“对假设有价值”。
**为什么不用独立 Progress Judge**:它会增加模型成本和新的失败点,还会把简单的空结果、重复 scope 判断模型化。
**首版边界**:重复检测只比较规范化参数,不承诺识别自然语言语义等价查询。
### 5.7 决策七:`conclusion=null` 是合法完成,不是模型失败
**问题**:如果成功的唯一含义是“必须给出根因”,Agent 在缺少企业、时间范围或错误信息时只能继续盲查,或者编造结论。
**决策**:DiagnosisDraft 允许 `conclusion=null`:
- 缺少开始查询所需信息:`MISSING_REQUIRED_CONTEXT`;
- 已完成有限检查但证据不足:`INSUFFICIENT_EVIDENCE`;
- 有结论:才进入完整 EvidenceGuard、Repair、SemanticGuard 链。
最终公开生命周期仍只有 `SUCCESS / FALLBACK / FAILED / CANCELLED`。证据不足是 `FALLBACK` 的原因,不再引入一套与 Run 终态平行的诊断状态机。
如果模型输出非法 Draft,系统会丢弃非法正文;只有当前 Run 已存在可验真的 ProgressSnapshot,才允许降级成过程型 Fallback,否则保持 fail closed。
### 5.8 决策八:可观测性记录决策证据,不复制敏感上下文
**问题**:为了调试 Agent,最直接的做法是保存 Prompt、模型正文、Tool arguments 和 raw response。但这会让普通 Trace 变成敏感数据仓库,也会造成多份事实副本。
**决策**:
- `diagnosis_trace_event` 只保存追加式、按 exact Run 排序的事件和有界 metadata;
- `ToolInvocation` 长期保存 Tool 身份、状态、耗时和字节数,不保存完整请求/响应;
- `AgentStep` 保存步骤元数据和 Token,不保存 Prompt、Tool payload;
- Provider reasoning 与 assistant text 放入独立受限审计表和接口;
- Trace 写入失败不得改变业务结果;
- Model usage 按组件和轮次入账,Run 结束做 Token 对账;
- Tool 请求被 Harness 拒绝时记录 `TOOL_REQUEST_REJECTED`,不能伪装成一次真实 `TOOL_INVOCATION`。
**尚未完成的治理**:Reasoning 的访问控制、保留期限和加密仍由 ISS-015 跟踪。已有隔离不代表完整合规闭环。
## 6. 一次诊断的完整控制流程
```mermaid
sequenceDiagram
participant U as Client
participant App as Chat Application
participant Core as Harness Core
participant Agent as Diagnosis Agent
participant TI as Tool Interceptor
participant TB as ToolBoundary
participant Store as Canonical Store
participant Guard as Guards
participant Release as Release
U->>App: query + optional sessionId
App->>Core: startRun(sessionId)
Core-->>App: RunContext(runId, deadline, handles)
App->>Agent: query + bounded safe previousTurn
loop ReAct 由框架拥有
Agent->>TI: Tool Call Envelope
TI->>TI: 应用上一轮信息增益、检查重复/饱和/协议
alt 门禁允许
TI->>TB: framework id + business input + RunContext
TB->>TB: active / auth / readonly / budget / size
TB->>Store: PROJECTING -> READY or ERROR
TB-->>TI: bounded canonical agent_result
TI-->>Agent: whitelist observation
else 信息饱和或协议停止
TI-->>Agent: one-shot STOP_REQUIRED
end
end
Agent-->>App: DiagnosisDraft 或受控停止
App->>Release: Draft + ProgressSnapshot + stop reason
alt 有结论
Release->>Guard: evidence truth + semantic support
Guard-->>Release: verified / unsupported
else 无结论或受控停止
Release->>Release: 构造确定性 Fallback
end
Release-->>App: original safe Draft or SafeFallback
App->>Core: first terminal wins
App-->>U: content/failure + done
```
## 7. 真实问题如何反向修正设计
这些不是零散 Bug 清单。每个问题都暴露了一个原设计假设不成立,并促成了边界调整。
| 现场问题 | 被证伪的假设 | 设计修正 | 固化位置 |
|---|---|---|---|
| Spring AI 默认 10 次 retry | 一次 Harness 模型调用等于一次 Provider attempt | SDK retry=1,retry 所有权上移 | Core / Retry / 配置测试 |
| ThreadLocal 跨异步边界不稳定 | 同线程上下文足以表示 Run 所有权 | 显式 `RunContext` 贯穿调用链 | Core / framework metadata |
| Tool raw 直接进入 Agent | Tool 返回可同时服务推理、验真和审计 | canonical / control / observation 三层拆分 | ToolBoundary / Projector / Store |
| 0 条日志被表达为 `success=false` | 空结果等于技术失败 | 技术执行与 `NO_EVIDENCE` 分离 | Tool contract / Projector |
| `REFERENCE` 被当作诊断证据 | 非空候选等于支持结论 | 保留相关度,语义增益交给模型 | RAG projector / Progress |
| Redis canonical 全部 `STORE_ERROR` | 单测 ObjectMapper 与生产配置行为一致 | 使用生产 ObjectMapper 能力并增加 live E2E | Store wiring / E2E |
| raw/rewritten query 出现在日志 | 可观测性可以直接打印检索输入 | 普通日志和 Trace 仅保存安全 metadata | Audit boundary |
| 空查反复改写直到预算耗尽 | 硬预算可以承担正常收敛 | 引入信息增益和饱和停止 | ProgressTracker / Interceptor |
| 9 次协议拒绝仍消耗 13 轮模型 | 协议错误会被模型自然修正 | 独立协议错误阈值和一次 STOP_REQUIRED | Progress protocol |
| 非法 Draft 导致已完成检查全部丢失 | Draft 失败意味着整个 Run 没有安全价值 | 仅在已有验真进展时发布过程型 Fallback | ProgressSnapshot / Release |
## 8. 关键决策总表
| 决策 | 选择 | 放弃的方案 | 获得的能力 | 付出的代价 |
|---|---|---|---|---|
| 推理拓扑 | 单 Diagnosis ReAct Agent | 多 Agent Graph、外层 ReAct | 上下文连续、唯一 Draft 作者 | Harness 门禁必须完整 |
| Run 上下文 | 显式 record + 状态句柄 | ThreadLocal、全局 Registry | 异步可证明、状态所有权清楚 | 参数需要显式传递 |
| Tool ID | framework `tool_call_id` | Harness 二次生成 ID | 引用链唯一 | 依赖框架 ID 契约 |
| Tool 真相 | Redis canonical,短 TTL | JPA 保存完整 raw | 可验真且限制敏感数据寿命 | Redis 可用性成为验证依赖 |
| Agent 输入 | 白名单 projection | raw / 完整 canonical 直传 | 上下文有界、减少泄露 | 需为每类 Tool 维护 projector |
| 长期审计 | metadata-only | 永久保存请求和响应 | 降低泄露与重复真相 | 深度回放受 TTL 限制 |
| 证据安全 | 确定性 EvidenceGuard + 隔离 SemanticGuard | 单一 Verifier Agent | 分清真实性和支持度 | 两段门禁增加延迟 |
| 修复 | 只修引用且语义必须不变 | Guard 重写报告 | 保持唯一作者 | 部分报告只能 Fallback |
| 停止 | 预算 + 信息增益双机制 | 只靠 Prompt 或硬上限 | 正常无证据收敛 | 首版只能做参数级重复判断 |
| 无结论 | 合法 Draft + SafeFallback | 强制根因 | 避免盲查和编造 | 调用方需理解 FALLBACK 是业务结果 |
| 发布 | 唯一 Release Policy | 各层自行 fallback | 对外语义一致 | Release 成为关键集中组件 |
## 9. Harness 明确不做什么
边界是否清晰,既看它做什么,也看它拒绝做什么:
- 不判断业务根因;
- 不实现 Planner / Executor / Verifier / Composer 角色图;
- 不在框架外复制 ReAct while-loop;
- 不让 Tool 声称结果是否支持诊断结论;
- 不把检索分数直接提升为结论可信度;
- 不依赖 Prompt 作为唯一预算或停止机制;
- 不自动重试 Diagnosis Agent 和业务 Tool;
- 不允许 Guard 生成新事实或改写结论;
- 不把 Redis canonical 变成永久审计库;
- 不承诺同步 Provider 调用一定能被立即物理中断;
- 不在首版做自然语言语义去重或跨 Tool 内容指纹。
这些非目标是在防止 Harness 再次长成一套不可维护的业务编排系统。
## 10. 如何验证设计成立
验证重点不是某个类是否被调用,而是上述不变量能否在失败和竞态下保持:
| 验证层 | 需要证明的事实 | 代表性测试/证据 |
|---|---|---|
| Core 单测 | deadline、取消、预算、first-terminal-wins、异步显式 context | `RunContextTest`、`RunBudgetTest`、`DiagnosisHarnessCoreTest` |
| Tool 单测 | cross-run、重复 ID、只读、大小、状态迁移、投影 | `ToolBoundaryTest`、`CanonicalInvocationStoreTest`、各 Projector test |
| Agent loop | framework ID、Envelope、STOP_REQUIRED、非法 Draft | `DiagnosisAgentUseCaseTest`、`HarnessToolInterceptorTest` |
| Guard / Release | 引用闭包、Repair 语义不变、Unsupported Fallback | `EvidenceGuardTest`、`SemanticGuardTest`、`DiagnosisReleaseUseCaseTest` |
| Audit | metadata 边界、Token 对账、拒绝与执行分离 | audit 包 focused tests、exact-run Trace |
| Live E2E | 生产序列化、Redis、数据库、模型和 SSE 的组合契约 | `devflow/projects/2026-07-22-single-react-cleanup-e2e/`、ISS-016 evidence |
单元测试能证明局部状态机,不能证明生产 `ObjectMapper`、Redis serializer、Provider Tool Calling 和 SSE 串联正确。因此 canonical store 的 Java Time 问题、零日志语义和协议空转都必须依靠 live E2E 反证,而不能只看 mock tests。
## 11. 当前边界与后续治理
当前设计已经建立可运行的确定性边界,但仍有明确限制:
1. 重复检测是 `tool_name + normalized_scope` 的参数等价,不识别语义近似查询。
2. Redis canonical 受 TTL 约束;TTL 过后只能依赖 metadata audit,不能重建完整证据正文。
3. 同步模型请求的取消主要阻止后续边界和迟到发布,不承诺 Provider 已执行计算立即停止。
4. SemanticGuard 仍是模型判断,只是被限制在无 Tool、无记忆、二值输出的最小范围内。
5. Reasoning 已与普通 Trace 隔离,但访问控制、保留期限和加密仍需完成。
6. 信息增益阈值默认为 2,仍需通过固定评测集持续校准,不能通过线上直觉随意调大。
## 12. 可复用的设计原则
这套 Harness 最值得复用的不是某个 Java 类,而是以下判断顺序:
1. 先列出系统必须保证的确定性不变量,再决定组件。
2. 将推理权留给模型,将授权、预算、归属、验真和发布权留给代码。
3. 不让一个数据表示同时承担内部真相、模型上下文和长期审计。
4. 不用资源耗尽代替业务收敛,也不用 Prompt 代替硬门禁。
5. 不让验证器成为第二个作者;验证失败时降级,而不是偷偷改写。
6. 把“没有足够证据”设计成一等业务结果,系统才不需要用幻觉换取成功率。
## 13. 代码与文档入口
- 完整组件说明:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md)
- 当前质量门禁:[harness-quality-gates.md](../../architecture/harness-quality-gates.md)
- Diagnosis Agent 架构:[agent-orchestration.md](../../architecture/agent-orchestration.md)
- 信息增益停止:[diagnosis-information-gain-stop-architecture.md](../../architecture/diagnosis-information-gain-stop-architecture.md)
- 核心重构 Issue:[ISS-014-single-react-agent-harness-aci-ptk-refactor.md](../../issues/archived/ISS-014-single-react-agent-harness-aci-ptk-refactor.md)
- 信息增益 Issue:[ISS-016-diagnosis-information-gain-stop-contract.md](../../issues/active/ISS-016-diagnosis-information-gain-stop-contract.md)

Some files were not shown because too many files have changed in this diff Show More