Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83193bdf4a | ||
|
|
de56551fea | ||
|
|
da18fdf4e1 |
@@ -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,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,223 @@
|
||||
# Harness 整体架构学习笔记:从装配到入口到记忆到知识库写入
|
||||
|
||||
**更新日期**: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` |
|
||||
@@ -83,7 +83,7 @@
|
||||
| `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 | ⬜ 部分 | HarnessModelInterceptor(预算/Token 记账) | — |
|
||||
| `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) |
|
||||
@@ -110,6 +110,9 @@
|
||||
| [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. 一次请求的完整学习主线
|
||||
|
||||
@@ -127,7 +130,7 @@ flowchart LR
|
||||
## 4. 下一步规划
|
||||
|
||||
```text
|
||||
主线九域全部 ✅ + 面试五步复习 ✅(共沉淀 13 篇笔记)
|
||||
主线九域全部 ✅(含 agent 域收尾)+ 面试五步复习 ✅(共沉淀 14 篇笔记)
|
||||
|
||||
面试前一天建议:
|
||||
1. 重读「面试速查」§1-2 + §8(30 秒陈述 / 一张图 / 六易错点)
|
||||
@@ -137,8 +140,9 @@ flowchart LR
|
||||
5. 2 分钟支付超时案例(复习笔记 §7)
|
||||
|
||||
可选深化(不阻塞面试):
|
||||
1. agent 域收尾:HarnessModelInterceptor / HarnessToolInterceptor 装配细节
|
||||
2. audit 域深化:RagLookupAuditEnricher 检索审计明细
|
||||
1. audit 域深化:RagLookupAuditEnricher 检索审计明细(已覆盖大半)
|
||||
2. LLM Judge 设计(已沉淀:面试问答 + 追问应对)
|
||||
3. 整体架构补充(已沉淀:装配/入口/记忆体系/知识库写入)
|
||||
```
|
||||
|
||||
## 5. 建议每次学完一个域后更新
|
||||
|
||||
Reference in New Issue
Block a user