feat(agent): add composer final answer

This commit is contained in:
aruo
2026-07-08 09:51:07 +08:00
parent a5b4502c72
commit 6015bcbf6f
20 changed files with 909 additions and 316 deletions
+1
View File
@@ -9,6 +9,7 @@
| 2026-07-07 | executor-v2-output-contract | Chat质量门禁/证据归因 | executor_evidence_v2, user_facing_answer removal, diagnosis_summary removal, structured renderer | openspec/changes/archive/2026-07-07-executor-v2-output-contract | archived | | 2026-07-07 | executor-v2-output-contract | Chat质量门禁/证据归因 | executor_evidence_v2, user_facing_answer removal, diagnosis_summary removal, structured renderer | openspec/changes/archive/2026-07-07-executor-v2-output-contract | archived |
| 2026-07-07 | executor-gatekeeper-hook | Chat质量门禁/证据归因 | Gatekeeper, verifier payload, source_invocation_ids, tool_name match, self_evaluation | openspec/changes/archive/2026-07-07-executor-gatekeeper-hook | archived | | 2026-07-07 | executor-gatekeeper-hook | Chat质量门禁/证据归因 | Gatekeeper, verifier payload, source_invocation_ids, tool_name match, self_evaluation | openspec/changes/archive/2026-07-07-executor-gatekeeper-hook | archived |
| 2026-07-07 | executor-verifier-claim-checks | Chat质量门禁/证据归因 | Verifier claim_checks, facts_checked compatibility, effective verdict guardrail, malformed output downgrade | openspec/changes/archive/2026-07-07-executor-verifier-claim-checks | archived | | 2026-07-07 | executor-verifier-claim-checks | Chat质量门禁/证据归因 | Verifier claim_checks, facts_checked compatibility, effective verdict guardrail, malformed output downgrade | openspec/changes/archive/2026-07-07-executor-verifier-claim-checks | archived |
| 2026-07-08 | executor-composer-final-answer | Chat quality gate/evidence attribution | chat_composer, final answer, allowed_claims, allowed_hypotheses, safe fallback, composer_output | openspec/changes/archive/2026-07-08-executor-composer-final-answer | archived |
| 2026-07-06 | rag-eval-pipeline-closure | RAG/评测/回归闭环 | lookupResult fixture, LookupKnowledgeTool snapshot, evidenceBlocks, contextPack, retrievalTrace, rerankTrace, baseline diff, fallback case | devflow/projects/2026-07-06-rag-eval-pipeline-closure | archived | | 2026-07-06 | rag-eval-pipeline-closure | RAG/评测/回归闭环 | lookupResult fixture, LookupKnowledgeTool snapshot, evidenceBlocks, contextPack, retrievalTrace, rerankTrace, baseline diff, fallback case | devflow/projects/2026-07-06-rag-eval-pipeline-closure | archived |
| 2026-07-06 | modular-rag-pipeline | RAG/Agent工具/证据链 | modular RAG, lookup_knowledge, evidenceBlocks, contextPack, rerank, retrievalTrace, L0 hint, unfiltered retry | openspec/changes/archive/2026-07-06-modular-rag-pipeline | archived | | 2026-07-06 | modular-rag-pipeline | RAG/Agent工具/证据链 | modular RAG, lookup_knowledge, evidenceBlocks, contextPack, rerank, retrievalTrace, L0 hint, unfiltered retry | openspec/changes/archive/2026-07-06-modular-rag-pipeline | archived |
| 2026-07-05 | mvp-demo-interview-runbook | MVP Demo/Interview | Plan C, payment timeout, runbook, trace checklist, demo script | openspec/changes/archive/2026-07-05-mvp-demo-interview-runbook | archived | | 2026-07-05 | mvp-demo-interview-runbook | MVP Demo/Interview | Plan C, payment timeout, runbook, trace checklist, demo script | openspec/changes/archive/2026-07-05-mvp-demo-interview-runbook | archived |
@@ -0,0 +1,57 @@
# Acceptance
## Implementation Result
Implemented stage four of Executor Structured Output V2:
- Added `chat_composer` prompt and Agent.
- Final answers for PASS, LOW_CONFID, and REJECT now use Composer when Verifier decision is valid.
- Composer input is filtered from Verifier decision and Executor structured output.
- Unsupported, external-unknown, and contradicted claims are excluded from confirmed final-answer material.
- REJECT Composer input has `allowed_hypotheses=[]`.
- Malformed Composer output uses deterministic safe fallback.
- Fallback does not expose raw Composer JSON, raw Executor JSON, or Executor `user_facing_answer`.
- `composer_output` is persisted in verifier audit.
## Static Verification
- Reviewed implementation diff for stage-four scope.
- `cmd /c openspec validate executor-composer-final-answer` passed.
- `cmd /c openspec validate --specs` passed before archive.
## Script Verification
Passed:
```powershell
mvn "-Dtest=ChatServiceSequentialAgentTest" test
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
```
Coverage:
- Composer prompt loading and invocation.
- valid Composer output as final answer source.
- malformed Composer output fallback.
- PASS no raw Executor JSON leakage.
- LOW_CONFID separation of confirmed material, possible directions, and gaps.
- REJECT safe output without raw Executor answer.
- Gatekeeper and Verifier stage compatibility.
## Browser / Manual Verification
Not run. This stage changes backend prompt, routing, parser, audit, and tests only.
## OpenSpec Archive Status
Archived:
```text
openspec/changes/archive/2026-07-08-executor-composer-final-answer
```
## Remaining Risks
- Stage five still needs broader eval fixture coverage for full evidence-attribution regressions.
- Composer prompt quality can be improved after real run traces are collected.
- Existing Maven warnings about duplicate test dependency and Lombok builder defaults remain outside this stage.
@@ -0,0 +1,54 @@
# Executor Composer Final Answer
## Background
Stages one to three moved the Chat diagnosis chain to structured Executor output, deterministic Gatekeeper validation, and Verifier `claim_checks`.
Before this stage, `ChatService` still owned final answer rendering. PASS paths could use a temporary V2 renderer, while LOW_CONFID and REJECT paths used templates. That left final user-facing expression too close to Executor material and made it harder to prove that only Verifier-allowed claims reached the user.
## Goal
Add a Composer expression layer after Verifier:
```text
chat_planner
-> chat_executor
-> VerifierInputHook + Gatekeeper
-> chat_verifier
-> chat_composer
-> final answer
```
Composer produces user-facing answers from filtered material only:
- `allowed_claims`
- `allowed_hypotheses`
- `missing_info`
- `recommended_actions`
- `rationale`
## Scope
- Added `chat-composer-prompt.md`.
- Added `chat_composer` Agent construction in `ChatService`.
- Added Composer input filtering from Verifier decision and Executor structured output.
- Replaced PASS temporary V2 renderer usage with Composer-or-safe-fallback rendering.
- Routed LOW_CONFID and REJECT final answers through Composer when Verifier output is valid.
- Added deterministic fallback for malformed Composer output.
- Persisted `composer_output` under `diagnosis_session.self_evaluation.verifier_evaluation`.
- Updated sequential workflow tests.
## Non-Goals
- No Planner changes.
- No Executor retry changes.
- No Gatekeeper rule expansion.
- No Verifier classification expansion.
- No database schema migration.
- No stage-five eval fixture expansion.
## OpenSpec
- Active change before archive: `openspec/changes/executor-composer-final-answer`
- Capabilities: `chat-composer-agent`, `chat-verifier-agent`
- Scale: standard
@@ -0,0 +1,70 @@
# Decisions
## Scope Decision
Stage four is limited to Composer final-answer generation and routing.
Reason: Gatekeeper and Verifier contracts were stabilized in earlier stages; this phase should only close the final-expression path.
## Composer Responsibility
Composer is an expression layer, not a diagnosis layer.
It may rephrase and organize only filtered material. It must not call tools, introduce new facts, rejudge root cause, or read raw Executor/tool output.
## Filtering Decision
`ChatService` owns Composer input filtering:
| Verifier classification | Composer handling |
|---|---|
| `direct_observation` | `allowed_claims` |
| `reasonable_inference` | `allowed_claims`, with bounded wording |
| `overstated` | `allowed_hypotheses` or `missing_info` |
| `unsupported` | `missing_info` |
| `external_unknown` | `missing_info` |
| `contradicted` | `missing_info` / REJECT-safe output |
For REJECT, `allowed_hypotheses` is always empty.
## Fallback Decision
Malformed Composer output falls back to deterministic rendering from filtered Composer input.
Fallback must never expose:
- raw Composer JSON;
- raw Executor JSON;
- Executor `user_facing_answer`;
- full unscreened tool output.
## Audit Decision
No new table is added. Composer output is persisted under:
```text
diagnosis_session.self_evaluation.verifier_evaluation.composer_output
```
The audit snapshot is intentionally compact and stores status plus parsed user-facing fields.
## Apply Fix Record
Initial targeted verification exposed test drift:
- test file had a UTF-8 BOM and failed Java compilation;
- scripted chat model did not recognize `COMPOSER_TEST_PROMPT`;
- older tests expected three-Agent execution and temporary V2 renderer behavior;
- LOW_CONFID assertions required indirect support to disappear instead of appearing as a possible direction.
Resolution: remove BOM, add Composer script branch, and update assertions to match the committed Composer contract.
## Interface Impact
L2 internal behavior change:
- external Chat API still returns a final answer string;
- internal final-answer source changes from Executor/temporary renderer to Composer or safe fallback;
- audit JSON gains `composer_output` under existing `self_evaluation`.
No database schema change.
@@ -0,0 +1,36 @@
# Evidence
## Relevant History
- `executor-v2-output-contract`: Executor emits structured diagnostic material and no final-expression fields.
- `executor-gatekeeper-hook`: Gatekeeper validates deterministic evidence failures before Verifier.
- `executor-verifier-claim-checks`: Verifier emits `claim_checks` and effective verdict guardrails.
## Code Evidence
- `src/main/resources/prompts/chat-composer-prompt.md`: defines Composer as an expression layer with strict JSON output.
- `src/main/java/com/superbiz/agent/service/ChatService.java`: loads Composer prompt, invokes `chat_composer`, filters Composer input, parses Composer output, falls back safely, and persists Composer audit.
- `src/test/java/com/superbiz/agent/service/ChatServiceSequentialAgentTest.java`: covers Composer invocation, fallback, REJECT/LOW_CONFID behavior, and no raw JSON leakage.
## Evidence-Driven Conclusions
- Composer must be after Verifier because Verifier `claim_checks` are the authority for allowed final-answer material.
- Composer must not receive raw tool output or full unscreened Executor output because that would re-open the evidence attribution problem.
- Verifier malformed/missing output should not invoke Composer because there is no trustworthy decision to filter with.
- Fixed fallback remains necessary because Composer is an LLM call with a strict JSON contract and can produce malformed output.
## Verification Evidence
Passed:
```powershell
mvn "-Dtest=ChatServiceSequentialAgentTest" test
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-composer-final-answer
cmd /c openspec validate --specs
```
Known existing warnings:
- Maven reports duplicate `spring-boot-starter-test` dependency in `pom.xml`.
- Existing Lombok `@Builder` default warnings remain.
@@ -41,3 +41,34 @@
- No unresolved user-interview question identified. - No unresolved user-interview question identified.
- No database migration required. - No database migration required.
- No OpenSpec/devflow conflict found. - No OpenSpec/devflow conflict found.
## Apply Notes
- Implemented `chat_composer` as a post-Verifier expression Agent in `ChatService`.
- `ChatService` now builds Composer input from `VerifierDecision` plus parsed Executor structured output, not from raw Executor answer text.
- PASS, LOW_CONFID, and REJECT final-answer paths now use Composer output or a deterministic safe fallback.
- Verifier-missing or Verifier-malformed paths use fixed fallback directly because there is no trustworthy Verifier decision for Composer.
- Composer audit is persisted under `verifier_evaluation.composer_output` without adding a database table.
## Test Drift And Fixes
- Initial test compilation failed because `ChatServiceSequentialAgentTest.java` had a UTF-8 BOM at the file start. Removed the BOM.
- Existing sequential-flow tests still expected the old three-Agent call sequence and temporary V2 renderer behavior. Updated them to include `chat_composer` when a valid Verifier decision exists.
- Tests that exercise fixed fallback now force malformed Composer output so they verify no raw JSON or Executor final answer leakage.
- LOW_CONFID tests were updated to allow indirect support to appear as a possible direction rather than requiring it to disappear from all final-answer text.
## Verification
Passed:
```powershell
mvn "-Dtest=ChatServiceSequentialAgentTest" test
mvn "-Dtest=ExecutorGatekeeperServiceTest,VerifierInputHookTest,ChatServiceSequentialAgentTest" test
cmd /c openspec validate executor-composer-final-answer
cmd /c openspec validate --specs
```
Known existing warnings:
- Maven reports duplicate `spring-boot-starter-test` dependency in `pom.xml`.
- Existing Lombok `@Builder` default warnings remain.
@@ -32,28 +32,6 @@ The system SHALL use ChatService for explicit single-round `Planner -> Executor
- **AND** it SHALL NOT pass through the raw Executor answer - **AND** it SHALL NOT pass through the raw Executor answer
- **AND** it SHALL NOT include a root-cause conclusion - **AND** it SHALL NOT include a root-cause conclusion
### Requirement: User-facing verifier outputs SHALL follow Composer-safe protocols
The system SHALL use Composer or fixed safe templates for PASS, LOW_CONFID, and REJECT user-facing responses.
#### Scenario: PASS uses Composer-safe output
- **WHEN** the final verdict is `PASS`
- **THEN** the user-facing response SHALL be generated from Verifier-allowed material through Composer or a safe fixed template
- **AND** it SHALL NOT use Executor `user_facing_answer`
- **AND** it SHALL NOT expose raw Executor JSON
#### Scenario: LOW_CONFID uses Composer-safe uncertainty output
- **WHEN** the final verdict is `LOW_CONFID`
- **THEN** the user-facing response SHALL include only confirmed facts, possible directions, evidence gaps, and next-step suggestions derived from Verifier-allowed material
- **AND** optional evidence gaps, if present, SHALL come only from verifier-identified gaps
- **AND** unsupported raw Executor claims SHALL NOT be presented as confirmed conclusions
#### Scenario: REJECT uses degraded template
- **WHEN** the final verdict is `REJECT`
- **THEN** the user-facing response SHALL use a degraded template or Composer-safe degraded output
- **AND** it SHALL include only confirmed facts, evidence gaps, and next-step suggestions
- **AND** it SHALL NOT include unverified raw answer content
- **AND** it SHALL NOT include a root-cause conclusion
### Requirement: Verifier SHALL be observable ### Requirement: Verifier SHALL be observable
The Verifier's verdict and downstream final-answer composition SHALL be persisted for observability. The Verifier's verdict and downstream final-answer composition SHALL be persisted for observability.
@@ -78,3 +56,34 @@ The Verifier's verdict and downstream final-answer composition SHALL be persiste
- **WHEN** the Verifier evaluation is persisted - **WHEN** the Verifier evaluation is persisted
- **THEN** `diagnosis_session.self_evaluation.verifier_evaluation` SHALL include `gatekeeper_result` - **THEN** `diagnosis_session.self_evaluation.verifier_evaluation` SHALL include `gatekeeper_result`
- **AND** existing verifier fields such as `verdict`, `facts_checked`, `executor_output_parse_status`, and `tool_trace_summary` SHALL be preserved - **AND** existing verifier fields such as `verdict`, `facts_checked`, `executor_output_parse_status`, and `tool_trace_summary` SHALL be preserved
## ADDED Requirements
### Requirement: User-facing verifier outputs SHALL follow Composer-safe protocols
The system SHALL use Composer or fixed safe templates for PASS, LOW_CONFID, and REJECT user-facing responses.
#### Scenario: PASS uses Composer-safe output
- **WHEN** the final verdict is `PASS`
- **THEN** the user-facing response SHALL be generated from Verifier-allowed material through Composer or a safe fixed template
- **AND** it SHALL NOT use Executor `user_facing_answer`
- **AND** it SHALL NOT expose raw Executor JSON
#### Scenario: LOW_CONFID uses Composer-safe uncertainty output
- **WHEN** the final verdict is `LOW_CONFID`
- **THEN** the user-facing response SHALL include only confirmed facts, possible directions, evidence gaps, and next-step suggestions derived from Verifier-allowed material
- **AND** optional evidence gaps, if present, SHALL come only from verifier-identified gaps
- **AND** unsupported raw Executor claims SHALL NOT be presented as confirmed conclusions
#### Scenario: REJECT uses degraded template
- **WHEN** the final verdict is `REJECT`
- **THEN** the user-facing response SHALL use a degraded template or Composer-safe degraded output
- **AND** it SHALL include only confirmed facts, evidence gaps, and next-step suggestions
- **AND** it SHALL NOT include unverified raw answer content
- **AND** it SHALL NOT include a root-cause conclusion
## REMOVED Requirements
### Requirement: User-facing verifier outputs SHALL follow fixed templates
**Reason**: final user-facing answers are no longer owned by legacy Verifier templates. They must be generated from Verifier-allowed material through Composer or deterministic safe fallback.
**Migration**: use "User-facing verifier outputs SHALL follow Composer-safe protocols" and the new `chat-composer-agent` capability.
@@ -0,0 +1,44 @@
## 1. Composer Prompt Contract
- [x] 1.1 Add `src/main/resources/prompts/chat-composer-prompt.md`.
- [x] 1.2 Define Composer input fields: `original_query`, `verdict`, `allowed_claims`, `allowed_hypotheses`, `missing_info`, `recommended_actions`, and `rationale`.
- [x] 1.3 Define strict JSON output fields: `answer_summary`, `recommended_actions`, and `user_facing_answer`.
- [x] 1.4 State verdict-specific wording rules for PASS, LOW_CONFID, and REJECT.
- [x] 1.5 State that Composer must not add facts, call tools, output Markdown, or use raw Executor/tool output.
## 2. Composer Invocation And Input Filtering
- [x] 2.1 Load the Composer prompt in `ChatService`.
- [x] 2.2 Add a `chat_composer` Agent or equivalent Composer model call after final Verifier decision.
- [x] 2.3 Build Composer input from Verifier decision and Executor structured output.
- [x] 2.4 Filter `allowed_claims` from `claim_checks` using `direct_observation` and bounded `reasonable_inference`.
- [x] 2.5 Exclude `unsupported`, `external_unknown`, and `contradicted` claims from confirmed output.
- [x] 2.6 Downgrade `overstated` claims to `allowed_hypotheses` or `missing_info`.
- [x] 2.7 Ensure REJECT Composer input has `allowed_hypotheses=[]`.
- [x] 2.8 Ensure Composer input contains no raw tool output, full unscreened Executor output, or Executor `user_facing_answer`.
## 3. Composer Output Parsing, Fallback, And Audit
- [x] 3.1 Parse Composer strict JSON output.
- [x] 3.2 Add safe fallback rendering for malformed Composer output.
- [x] 3.3 Ensure fallback rendering never exposes raw Composer JSON, raw Executor JSON, or Executor `user_facing_answer`.
- [x] 3.4 Persist `composer_output` under `diagnosis_session.self_evaluation.verifier_evaluation`.
- [x] 3.5 Preserve existing verifier audit fields when writing Composer output.
## 4. Final Answer Routing
- [x] 4.1 Replace PASS temporary V2 renderer usage with Composer or safe template rendering.
- [x] 4.2 Ensure LOW_CONFID final answer uses Composer-safe filtered material.
- [x] 4.3 Ensure REJECT final answer does not include root-cause conclusions and does not pass through Executor raw answer.
- [x] 4.4 Ensure Executor `user_facing_answer` is not read by any final answer path.
## 5. Tests And Verification
- [x] 5.1 Add or update tests proving Composer input excludes raw tool output and full Executor output.
- [x] 5.2 Add or update tests proving unsupported claims do not appear as confirmed final-answer content.
- [x] 5.3 Add or update tests for PASS root-cause wording only when an allowed root-cause claim exists.
- [x] 5.4 Add or update tests for LOW_CONFID separation of confirmed information, possible directions, and evidence gaps.
- [x] 5.5 Add or update tests for REJECT with `allowed_hypotheses=[]` and no root-cause conclusion.
- [x] 5.6 Add or update tests for malformed Composer fallback with no raw JSON leakage.
- [x] 5.7 Run targeted Maven tests.
- [x] 5.8 Validate this OpenSpec change and all specs.
@@ -1,44 +0,0 @@
## 1. Composer Prompt Contract
- [ ] 1.1 Add `src/main/resources/prompts/chat-composer-prompt.md`.
- [ ] 1.2 Define Composer input fields: `original_query`, `verdict`, `allowed_claims`, `allowed_hypotheses`, `missing_info`, `recommended_actions`, and `rationale`.
- [ ] 1.3 Define strict JSON output fields: `answer_summary`, `recommended_actions`, and `user_facing_answer`.
- [ ] 1.4 State verdict-specific wording rules for PASS, LOW_CONFID, and REJECT.
- [ ] 1.5 State that Composer must not add facts, call tools, output Markdown, or use raw Executor/tool output.
## 2. Composer Invocation And Input Filtering
- [ ] 2.1 Load the Composer prompt in `ChatService`.
- [ ] 2.2 Add a `chat_composer` Agent or equivalent Composer model call after final Verifier decision.
- [ ] 2.3 Build Composer input from Verifier decision and Executor structured output.
- [ ] 2.4 Filter `allowed_claims` from `claim_checks` using `direct_observation` and bounded `reasonable_inference`.
- [ ] 2.5 Exclude `unsupported`, `external_unknown`, and `contradicted` claims from confirmed output.
- [ ] 2.6 Downgrade `overstated` claims to `allowed_hypotheses` or `missing_info`.
- [ ] 2.7 Ensure REJECT Composer input has `allowed_hypotheses=[]`.
- [ ] 2.8 Ensure Composer input contains no raw tool output, full unscreened Executor output, or Executor `user_facing_answer`.
## 3. Composer Output Parsing, Fallback, And Audit
- [ ] 3.1 Parse Composer strict JSON output.
- [ ] 3.2 Add safe fallback rendering for malformed Composer output.
- [ ] 3.3 Ensure fallback rendering never exposes raw Composer JSON, raw Executor JSON, or Executor `user_facing_answer`.
- [ ] 3.4 Persist `composer_output` under `diagnosis_session.self_evaluation.verifier_evaluation`.
- [ ] 3.5 Preserve existing verifier audit fields when writing Composer output.
## 4. Final Answer Routing
- [ ] 4.1 Replace PASS temporary V2 renderer usage with Composer or safe template rendering.
- [ ] 4.2 Ensure LOW_CONFID final answer uses Composer-safe filtered material.
- [ ] 4.3 Ensure REJECT final answer does not include root-cause conclusions and does not pass through Executor raw answer.
- [ ] 4.4 Ensure Executor `user_facing_answer` is not read by any final answer path.
## 5. Tests And Verification
- [ ] 5.1 Add or update tests proving Composer input excludes raw tool output and full Executor output.
- [ ] 5.2 Add or update tests proving unsupported claims do not appear as confirmed final-answer content.
- [ ] 5.3 Add or update tests for PASS root-cause wording only when an allowed root-cause claim exists.
- [ ] 5.4 Add or update tests for LOW_CONFID separation of confirmed information, possible directions, and evidence gaps.
- [ ] 5.5 Add or update tests for REJECT with `allowed_hypotheses=[]` and no root-cause conclusion.
- [ ] 5.6 Add or update tests for malformed Composer fallback with no raw JSON leakage.
- [ ] 5.7 Run targeted Maven tests.
- [ ] 5.8 Validate this OpenSpec change and all specs.
@@ -0,0 +1,83 @@
# chat-composer-agent Specification
## Purpose
TBD - created by archiving change executor-composer-final-answer. Update Purpose after archive.
## Requirements
### Requirement: Composer SHALL generate final user-facing Chat answers
The system SHALL invoke a Composer expression layer after Verifier to generate the final user-facing Chat answer from Verifier-allowed material.
#### Scenario: Composer receives only filtered material
- **WHEN** ChatService invokes Composer
- **THEN** the Composer input SHALL contain `original_query`, `verdict`, `allowed_claims`, `allowed_hypotheses`, `missing_info`, `recommended_actions`, and `rationale`
- **AND** the Composer input SHALL NOT contain raw tool output
- **AND** the Composer input SHALL NOT contain the full unscreened Executor output
- **AND** the Composer input SHALL NOT contain Executor `user_facing_answer`
#### Scenario: Composer outputs strict JSON
- **WHEN** Composer completes
- **THEN** it SHALL output exactly one JSON object
- **AND** the JSON object SHALL include `answer_summary`, `recommended_actions`, and `user_facing_answer`
- **AND** it SHALL NOT output Markdown, code fences, or explanatory text outside the JSON object
#### Scenario: Composer does not introduce new facts
- **WHEN** Composer produces `answer_summary`, `recommended_actions`, or `user_facing_answer`
- **THEN** every service name, entity, timestamp, error code, metric value, root cause, and recommendation reason SHALL be derived from the Composer input
- **AND** Composer SHALL NOT add facts from model knowledge, raw tool history, or Executor raw text
### Requirement: Composer input SHALL honor Verifier claim checks
ChatService SHALL construct Composer input by filtering Executor structured output through Verifier `claim_checks`.
#### Scenario: Passing claims become allowed claims
- **WHEN** a claim check verification is `direct_observation`
- **THEN** ChatService SHALL include the matching Executor claim in `allowed_claims`
#### Scenario: Reasonable inferences remain bounded
- **WHEN** a claim check verification is `reasonable_inference`
- **THEN** ChatService MAY include the matching Executor claim in `allowed_claims`
- **AND** the final answer SHALL NOT describe it as the sole confirmed root cause unless the allowed claim itself is a root-cause claim and the final verdict is `PASS`
#### Scenario: Overstated claims are not confirmed findings
- **WHEN** a claim check verification is `overstated`
- **THEN** ChatService SHALL NOT include the matching Executor claim as a confirmed item in `allowed_claims`
- **AND** ChatService MAY include it as `allowed_hypotheses` or represent it in `missing_info`
#### Scenario: Unsupported or external claims are withheld
- **WHEN** a claim check verification is `unsupported`, `external_unknown`, or `contradicted`
- **THEN** ChatService SHALL NOT include the matching Executor claim in `allowed_claims`
- **AND** the final user-facing answer SHALL NOT present that claim as confirmed
### Requirement: Composer SHALL respect verdict-specific wording
Composer SHALL phrase final answers according to the effective Verifier verdict.
#### Scenario: PASS answer uses confirmed material
- **WHEN** the effective verdict is `PASS`
- **THEN** the final answer MAY state confirmed findings from `allowed_claims`
- **AND** it SHALL only state root cause confirmed when an allowed root-cause claim is present
#### Scenario: LOW_CONFID answer separates findings and gaps
- **WHEN** the effective verdict is `LOW_CONFID`
- **THEN** the final answer SHALL distinguish confirmed information from possible directions
- **AND** it SHALL mention evidence gaps from `missing_info`
- **AND** it SHALL NOT turn `allowed_hypotheses` into confirmed findings
#### Scenario: REJECT answer avoids root-cause conclusions
- **WHEN** the effective verdict is `REJECT`
- **THEN** Composer input SHALL have `allowed_hypotheses=[]`
- **AND** the final answer SHALL state that current evidence cannot support a reliable conclusion
- **AND** the final answer SHALL NOT include a root-cause conclusion
### Requirement: Composer failures SHALL degrade safely
The system SHALL tolerate malformed Composer output without leaking raw JSON or unverified Executor material.
#### Scenario: malformed Composer output falls back safely
- **WHEN** Composer returns malformed JSON or omits required fields
- **THEN** ChatService SHALL produce a final answer using a fixed safe fallback template based only on filtered material
- **AND** the final answer SHALL NOT expose raw Composer output
- **AND** the final answer SHALL NOT expose raw Executor output
- **AND** the final answer SHALL NOT use Executor `user_facing_answer`
#### Scenario: Composer audit is persisted
- **WHEN** ChatService persists verifier evaluation
- **THEN** `diagnosis_session.self_evaluation.verifier_evaluation.composer_output` SHALL record whether Composer output was valid or fallback was used
- **AND** the audit SHALL include the parsed Composer fields when valid
- **AND** the audit SHALL remain compact and SHALL NOT store full raw tool output
+47 -29
View File
@@ -89,48 +89,38 @@ The groundedness score SHALL be computed from critical fact classifications inst
- **AND** the result SHALL be clamped into `[0.0, 1.0]` - **AND** the result SHALL be clamped into `[0.0, 1.0]`
### Requirement: ChatService SHALL route based on Verifier verdict ### Requirement: ChatService SHALL route based on Verifier verdict
The system SHALL use ChatService for explicit single-round `Planner → Executor → Verifier` orchestration and SHALL use ChatService to control whether an additional round is allowed. The system SHALL use ChatService for explicit single-round `Planner -> Executor -> Verifier -> Composer` orchestration and SHALL use ChatService to control whether an additional round is allowed.
#### Scenario: PASS → direct output #### Scenario: PASS -> Composer output
- **WHEN** Verifier outputs verdict="PASS" - **WHEN** Verifier outputs verdict="PASS"
- **THEN** the system SHALL output the Executor's answer directly - **THEN** ChatService SHALL filter Verifier-allowed material and invoke Composer or a safe fixed template
- **AND** the final user-facing answer SHALL NOT pass through raw Executor output
- **AND** the final user-facing answer SHALL NOT read Executor `user_facing_answer`
#### Scenario: LOW_CONFID score≥0.5 → output with disclaimer #### Scenario: LOW_CONFID score>=0.5 -> Composer output with uncertainty
- **WHEN** Verifier outputs verdict="LOW_CONFID" with groundedness_score ≥ 0.5 - **WHEN** Verifier outputs verdict="LOW_CONFID" with groundedness_score >= 0.5
- **THEN** the system SHALL output the Executor's answer prefixed with a fixed confidence disclaimer - **THEN** ChatService SHALL filter Verifier-allowed material and invoke Composer or a safe fixed template
- **AND** the final user-facing answer SHALL distinguish confirmed information, possible directions, and evidence gaps
- **AND** unsupported raw Executor claims SHALL NOT be presented as confirmed conclusions
#### Scenario: LOW_CONFID score<0.5 → trigger one additional round #### Scenario: LOW_CONFID score<0.5 -> trigger one additional round
- **WHEN** Verifier outputs verdict="LOW_CONFID" with groundedness_score < 0.5 and this is the first callback - **WHEN** Verifier outputs verdict="LOW_CONFID" with groundedness_score < 0.5 and this is the first callback
- **THEN** the ChatService SHALL invoke one additional `Planner → Executor → Verifier` round to supplement evidence - **THEN** the ChatService SHALL invoke one additional `Planner -> Executor -> Verifier` round to supplement evidence
- **AND** after the second Verifier run, verdict="LOW_CONFID" SHALL be output with a confidence disclaimer - **AND** after the second Verifier run, verdict="LOW_CONFID" SHALL be routed to Composer or a safe fixed template
- **AND** after the second Verifier run, verdict="REJECT" SHALL still produce a degraded output - **AND** after the second Verifier run, verdict="REJECT" SHALL still produce a degraded Composer-safe output
#### Scenario: REJECT does not enter retry round #### Scenario: REJECT does not enter retry round
- **WHEN** Verifier outputs verdict="REJECT" - **WHEN** Verifier outputs verdict="REJECT"
- **THEN** the system SHALL NOT start a retry round for evidence补充 - **THEN** the system SHALL NOT start a retry round for evidence supplementation
- **AND** it SHALL produce a degraded output directly - **AND** it SHALL produce a degraded output directly through Composer-safe rendering
#### Scenario: REJECT → degraded output #### Scenario: REJECT -> degraded output
- **WHEN** Verifier outputs verdict="REJECT" - **WHEN** Verifier outputs verdict="REJECT"
- **THEN** the system SHALL output a degraded result indicating the answer cannot be reliably generated - **THEN** the system SHALL output a degraded result indicating the answer cannot be reliably generated
- **AND** it SHALL NOT pass through the raw Executor answer - **AND** it SHALL NOT pass through the raw Executor answer
- **AND** it SHALL NOT include a root-cause conclusion
### Requirement: User-facing verifier outputs SHALL follow fixed templates
The system SHALL use fixed output protocols for LOW_CONFID and REJECT user-facing responses.
#### Scenario: LOW_CONFID uses disclaimer template
- **WHEN** the final verdict is `LOW_CONFID`
- **THEN** the user-facing response SHALL prepend a fixed disclaimer before the Executor answer
- **AND** optional evidence gaps, if present, SHALL come only from verifier-identified critical gaps
#### Scenario: REJECT uses degraded template
- **WHEN** the final verdict is `REJECT`
- **THEN** the user-facing response SHALL use a degraded template
- **AND** it SHALL include only confirmed facts, evidence gaps, and next-step suggestions
- **AND** it SHALL NOT include unverified raw answer content
### Requirement: Verifier SHALL be observable ### Requirement: Verifier SHALL be observable
The Verifier's verdict SHALL be persisted for observability. The Verifier's verdict and downstream final-answer composition SHALL be persisted for observability.
#### Scenario: claim checks written to self_evaluation #### Scenario: claim checks written to self_evaluation
- **WHEN** the Verifier evaluation is persisted - **WHEN** the Verifier evaluation is persisted
@@ -138,6 +128,12 @@ The Verifier's verdict SHALL be persisted for observability.
- **AND** it SHALL continue to include compatibility `facts_checked` - **AND** it SHALL continue to include compatibility `facts_checked`
- **AND** existing fields such as `verdict`, `groundedness_score`, `rationale`, `executor_output_parse_status`, `tool_trace_summary`, and `gatekeeper_result` SHALL be preserved - **AND** existing fields such as `verdict`, `groundedness_score`, `rationale`, `executor_output_parse_status`, `tool_trace_summary`, and `gatekeeper_result` SHALL be preserved
#### Scenario: composer output written to self_evaluation
- **WHEN** final answer composition completes
- **THEN** `diagnosis_session.self_evaluation.verifier_evaluation` SHALL include `composer_output`
- **AND** `composer_output` SHALL indicate whether parsed Composer output or fallback rendering was used
- **AND** existing verifier fields such as `claim_checks`, `facts_checked`, `gatekeeper_result`, and `tool_trace_summary` SHALL be preserved
#### Scenario: verdict written to self_evaluation #### Scenario: verdict written to self_evaluation
- **WHEN** the Verifier produces a verdict - **WHEN** the Verifier produces a verdict
- **THEN** the ChatService SHALL write the verdict data under `diagnosis_session.self_evaluation.verifier_evaluation` - **THEN** the ChatService SHALL write the verdict data under `diagnosis_session.self_evaluation.verifier_evaluation`
@@ -403,3 +399,25 @@ The Verifier SHALL classify each structured claim using a fixed derivability cla
- **WHEN** Verifier emits `claim_checks` - **WHEN** Verifier emits `claim_checks`
- **THEN** each claim check SHALL include `claim_id`, `verification`, `detail`, and `evidence_refs` - **THEN** each claim check SHALL include `claim_id`, `verification`, `detail`, and `evidence_refs`
- **AND** every evidence ref SHALL preserve available `trace_ref`, `tool_name`, and `source_invocation_ids` - **AND** every evidence ref SHALL preserve available `trace_ref`, `tool_name`, and `source_invocation_ids`
### Requirement: User-facing verifier outputs SHALL follow Composer-safe protocols
The system SHALL use Composer or fixed safe templates for PASS, LOW_CONFID, and REJECT user-facing responses.
#### Scenario: PASS uses Composer-safe output
- **WHEN** the final verdict is `PASS`
- **THEN** the user-facing response SHALL be generated from Verifier-allowed material through Composer or a safe fixed template
- **AND** it SHALL NOT use Executor `user_facing_answer`
- **AND** it SHALL NOT expose raw Executor JSON
#### Scenario: LOW_CONFID uses Composer-safe uncertainty output
- **WHEN** the final verdict is `LOW_CONFID`
- **THEN** the user-facing response SHALL include only confirmed facts, possible directions, evidence gaps, and next-step suggestions derived from Verifier-allowed material
- **AND** optional evidence gaps, if present, SHALL come only from verifier-identified gaps
- **AND** unsupported raw Executor claims SHALL NOT be presented as confirmed conclusions
#### Scenario: REJECT uses degraded template
- **WHEN** the final verdict is `REJECT`
- **THEN** the user-facing response SHALL use a degraded template or Composer-safe degraded output
- **AND** it SHALL include only confirmed facts, evidence gaps, and next-step suggestions
- **AND** it SHALL NOT include unverified raw answer content
- **AND** it SHALL NOT include a root-cause conclusion
@@ -125,6 +125,7 @@ public class ChatService {
private String chatPlannerPrompt; private String chatPlannerPrompt;
private String chatExecutorPrompt; private String chatExecutorPrompt;
private String chatVerifierPrompt; private String chatVerifierPrompt;
private String chatComposerPrompt;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@PostConstruct @PostConstruct
@@ -140,6 +141,9 @@ public class ChatService {
chatVerifierPrompt = new String( chatVerifierPrompt = new String(
new ClassPathResource("prompts/chat-verifier-prompt.md").getInputStream().readAllBytes(), new ClassPathResource("prompts/chat-verifier-prompt.md").getInputStream().readAllBytes(),
StandardCharsets.UTF_8); StandardCharsets.UTF_8);
chatComposerPrompt = new String(
new ClassPathResource("prompts/chat-composer-prompt.md").getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
logger.info("Chat 多 Agent Prompts 加载成功"); logger.info("Chat 多 Agent Prompts 加载成功");
} catch (IOException e) { } catch (IOException e) {
logger.error("加载 Chat Prompt 文件失败", e); logger.error("加载 Chat Prompt 文件失败", e);
@@ -420,8 +424,9 @@ public class ChatService {
Optional<OverAllState> stateOptional = workflow.invoke(workflowInput, config); Optional<OverAllState> stateOptional = workflow.invoke(workflowInput, config);
if (stateOptional.isEmpty()) { if (stateOptional.isEmpty()) {
finalDecision = buildVerifierFallbackDecision(round, "workflow 未返回有效状态"); finalDecision = buildVerifierFallbackDecision(round, "workflow 未返回有效状态");
answer = buildLowConfidenceOutput(answer, finalDecision); ComposerRenderResult renderResult = buildFixedFallbackAnswer(question, finalDecision);
persistVerifierEvaluation(session, finalDecision, round); answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break; break;
} }
@@ -441,25 +446,23 @@ public class ChatService {
if (finalDecision == null) { if (finalDecision == null) {
finalDecision = buildVerifierFallbackDecision(round, "verifier_output 缺失或无法解析"); finalDecision = buildVerifierFallbackDecision(round, "verifier_output 缺失或无法解析");
answer = buildLowConfidenceOutput(answer, finalDecision); ComposerRenderResult renderResult = buildFixedFallbackAnswer(question, finalDecision);
persistVerifierEvaluation(session, finalDecision, round); answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break; break;
} }
if ("PASS".equals(finalDecision.verdict())) { if ("PASS".equals(finalDecision.verdict())) {
String executorAnswer = answer; ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
answer = extractUserFacingAnswer(executorAnswer) answer = renderResult.answer();
.or(() -> renderStructuredExecutorAnswer(executorAnswer)) persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
.orElse(executorAnswer == null || executorAnswer.isBlank()
? "抱歉,多 Agent 分析未能生成有效结论。"
: executorAnswer);
persistVerifierEvaluation(session, finalDecision, round);
break; break;
} }
if ("REJECT".equals(finalDecision.verdict())) { if ("REJECT".equals(finalDecision.verdict())) {
answer = buildDegradedOutput(finalDecision); ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
persistVerifierEvaluation(session, finalDecision, round); answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break; break;
} }
@@ -467,8 +470,9 @@ public class ChatService {
&& finalDecision.groundednessScore() < verifierLowConfidenceThreshold && finalDecision.groundednessScore() < verifierLowConfidenceThreshold
&& round < 2; && round < 2;
if (!shouldRetry) { if (!shouldRetry) {
answer = buildLowConfidenceOutput(answer, finalDecision); ComposerRenderResult renderResult = composeFinalAnswer(chatModel, question, finalDecision, config);
persistVerifierEvaluation(session, finalDecision, round); answer = renderResult.answer();
persistVerifierEvaluation(session, finalDecision, round, renderResult.audit());
break; break;
} }
@@ -549,6 +553,17 @@ public class ChatService {
.build(); .build();
} }
private ReactAgent buildChatComposerAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("chat_composer")
.description("负责将 Verifier 允许的材料组织为最终用户答案")
.model(chatModel)
.systemPrompt(chatComposerPrompt)
.hooks(new AgentLoggingHook(agentStepRepository, "composer"))
.outputKey("composer_output")
.build();
}
private ReactAgent buildChatExecutorAgent(ChatModel chatModel, ToolCallback[] toolCallbacks, private ReactAgent buildChatExecutorAgent(ChatModel chatModel, ToolCallback[] toolCallbacks,
List<Map<String, String>> history, String retryContext) { List<Map<String, String>> history, String retryContext) {
StringBuilder prompt = new StringBuilder(chatExecutorPrompt); StringBuilder prompt = new StringBuilder(chatExecutorPrompt);
@@ -845,6 +860,11 @@ public class ChatService {
} }
private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round) { private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round) {
persistVerifierEvaluation(session, decision, round, null);
}
private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round,
Map<String, Object> composerOutput) {
if (decision == null) { if (decision == null) {
return; return;
} }
@@ -866,12 +886,337 @@ public class ChatService {
verifierEvaluation.put("gatekeeper_result", verifierEvaluation.put("gatekeeper_result",
Optional.ofNullable(VerifierContextHolder.getGatekeeperResult()) Optional.ofNullable(VerifierContextHolder.getGatekeeperResult())
.orElse(Map.of("status", "pass", "failed_rules", List.of(), "warnings", List.of(), "errors", List.of()))); .orElse(Map.of("status", "pass", "failed_rules", List.of(), "warnings", List.of(), "errors", List.of())));
if (composerOutput != null) {
verifierEvaluation.put("composer_output", composerOutput);
}
String merged = selfEvaluationMergeService.mergeVerifierEvaluation(session.getSelfEvaluation(), verifierEvaluation); String merged = selfEvaluationMergeService.mergeVerifierEvaluation(session.getSelfEvaluation(), verifierEvaluation);
session.setSelfEvaluation(merged); session.setSelfEvaluation(merged);
diagnosisSessionRepository.save(session); diagnosisSessionRepository.save(session);
} }
private ComposerRenderResult composeFinalAnswer(ChatModel chatModel, String originalQuery,
VerifierDecision decision, RunnableConfig config) {
Map<String, Object> composerInput = buildComposerInput(originalQuery, decision);
try {
ReactAgent composer = buildChatComposerAgent(chatModel);
String composerOutput = composer.call(objectMapper.writeValueAsString(composerInput), config).getText();
return parseComposerOutput(composerOutput, composerInput);
} catch (Exception e) {
logger.error("chat_composer 执行失败,使用安全降级模板", e);
return buildFixedFallbackAnswer(composerInput, "composer_exception");
}
}
private ComposerRenderResult buildFixedFallbackAnswer(String originalQuery, VerifierDecision decision) {
return buildFixedFallbackAnswer(buildComposerInput(originalQuery, decision), "fixed_fallback");
}
private Map<String, Object> buildComposerInput(String originalQuery, VerifierDecision decision) {
Map<String, Object> input = new LinkedHashMap<>();
input.put("original_query", originalQuery);
input.put("verdict", decision.verdict());
Map<String, Map<String, Object>> claimsById = indexExecutorClaims();
List<Map<String, Object>> allowedClaims = new ArrayList<>();
List<Map<String, Object>> allowedHypotheses = new ArrayList<>();
List<String> missingInfo = extractStructuredMissingInfo();
List<Map<String, Object>> recommendedActions = extractStructuredRecommendedActions();
if (decision.claimChecks().isEmpty()) {
addLegacyFactsToComposerInput(decision, allowedClaims, allowedHypotheses, missingInfo);
} else {
for (Map<String, Object> check : decision.claimChecks()) {
String verification = String.valueOf(check.getOrDefault("verification", "unsupported"));
String claimId = String.valueOf(check.getOrDefault("claim_id", ""));
String claimText = String.valueOf(check.getOrDefault("claim_text", ""));
String detail = String.valueOf(check.getOrDefault("detail", ""));
Map<String, Object> claim = buildComposerClaim(claimsById.get(claimId), check);
switch (verification) {
case "direct_observation", "reasonable_inference" -> allowedClaims.add(claim);
case "overstated" -> allowedHypotheses.add(Map.of(
"hypothesis_text", claimText,
"basis", detail.isBlank() ? "当前证据只能支持部分判断,不能作为确认结论" : detail
));
case "unsupported", "external_unknown", "contradicted" ->
addMissingInfo(missingInfo, claimText, detail);
default -> addMissingInfo(missingInfo, claimText, detail);
}
}
}
if ("REJECT".equals(decision.verdict())) {
allowedHypotheses = List.of();
}
input.put("allowed_claims", allowedClaims);
input.put("allowed_hypotheses", allowedHypotheses);
input.put("missing_info", missingInfo);
input.put("recommended_actions", recommendedActions);
input.put("rationale", decision.rationale());
return input;
}
private Map<String, Map<String, Object>> indexExecutorClaims() {
Map<String, Map<String, Object>> claimsById = new LinkedHashMap<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object claims = structuredOutput == null ? null : structuredOutput.get("claims");
if (claims instanceof List<?> claimList) {
for (Object item : claimList) {
if (item instanceof Map<?, ?> rawClaim) {
Map<String, Object> claim = new LinkedHashMap<>();
rawClaim.forEach((key, value) -> claim.put(String.valueOf(key), value));
String claimId = String.valueOf(claim.getOrDefault("claim_id", ""));
if (!claimId.isBlank()) {
claimsById.put(claimId, claim);
}
}
}
}
return claimsById;
}
private Map<String, Object> buildComposerClaim(Map<String, Object> executorClaim, Map<String, Object> claimCheck) {
Map<String, Object> claim = new LinkedHashMap<>();
claim.put("claim_id", valueFrom(executorClaim, claimCheck, "claim_id"));
claim.put("claim_type", valueFrom(executorClaim, claimCheck, "claim_type"));
claim.put("claim_text", valueFrom(executorClaim, claimCheck, "claim_text"));
claim.put("support_level", executorClaim == null ? "" : String.valueOf(executorClaim.getOrDefault("support_level", "")));
claim.put("verification", String.valueOf(claimCheck.getOrDefault("verification", "")));
claim.put("detail", String.valueOf(claimCheck.getOrDefault("detail", "")));
return claim;
}
private String valueFrom(Map<String, Object> primary, Map<String, Object> fallback, String key) {
Object value = primary == null ? null : primary.get(key);
if (value == null || String.valueOf(value).isBlank()) {
value = fallback.get(key);
}
return value == null ? "" : String.valueOf(value);
}
private List<String> extractStructuredMissingInfo() {
List<String> missingInfo = new ArrayList<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object missing = structuredOutput == null ? null : structuredOutput.get("missing_info");
if (missing instanceof List<?> missingList) {
for (Object item : missingList) {
String text = String.valueOf(item);
if (!text.isBlank() && !missingInfo.contains(text)) {
missingInfo.add(text);
}
}
}
return missingInfo;
}
private List<Map<String, Object>> extractStructuredRecommendedActions() {
List<Map<String, Object>> actions = new ArrayList<>();
Map<String, Object> structuredOutput = VerifierContextHolder.getExecutorStructuredOutput();
Object recommendedActions = structuredOutput == null ? null : structuredOutput.get("recommended_actions");
if (recommendedActions instanceof List<?> actionList) {
for (Object item : actionList) {
if (item instanceof Map<?, ?> rawAction) {
Map<String, Object> action = new LinkedHashMap<>();
action.put("action_text", textValue(rawAction.get("action_text")));
action.put("reason", textValue(rawAction.get("reason")));
if (!String.valueOf(action.get("action_text")).isBlank()) {
actions.add(action);
}
}
}
}
return actions;
}
private void addLegacyFactsToComposerInput(VerifierDecision decision, List<Map<String, Object>> allowedClaims,
List<Map<String, Object>> allowedHypotheses,
List<String> missingInfo) {
for (Map<String, Object> fact : decision.factsChecked()) {
String verification = String.valueOf(fact.getOrDefault("verification", ""));
String factText = String.valueOf(fact.getOrDefault("fact", ""));
String detail = String.valueOf(fact.getOrDefault("detail", ""));
if ("direct_evidence".equals(verification)) {
allowedClaims.add(Map.of(
"claim_id", "",
"claim_type", "",
"claim_text", factText,
"support_level", "direct",
"verification", verification,
"detail", detail
));
} else if ("indirect_support".equals(verification)) {
allowedHypotheses.add(Map.of(
"hypothesis_text", factText,
"basis", detail.isBlank() ? "当前仅有间接支持,不能作为确认结论" : detail
));
} else {
addMissingInfo(missingInfo, factText, detail);
}
}
}
private void addMissingInfo(List<String> missingInfo, String text, String detail) {
if (text == null || text.isBlank()) {
return;
}
String value = detail == null || detail.isBlank() ? text : text + ":" + detail;
if (!missingInfo.contains(value)) {
missingInfo.add(value);
}
}
private ComposerRenderResult parseComposerOutput(String composerOutput, Map<String, Object> composerInput) {
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(composerOutput));
String answerSummary = root.path("answer_summary").asText("");
String userFacingAnswer = root.path("user_facing_answer").asText("");
if (answerSummary.isBlank() || userFacingAnswer.isBlank() || !root.path("recommended_actions").isArray()) {
return buildFixedFallbackAnswer(composerInput, "composer_schema_invalid");
}
Map<String, Object> audit = new LinkedHashMap<>();
audit.put("status", "valid");
audit.put("answer_summary", answerSummary);
audit.put("recommended_actions", parseComposerActions(root.path("recommended_actions")));
audit.put("user_facing_answer", userFacingAnswer);
return new ComposerRenderResult(userFacingAnswer, audit);
} catch (Exception e) {
logger.warn("解析 composer_output 失败,使用安全降级模板");
return buildFixedFallbackAnswer(composerInput, "composer_malformed");
}
}
private List<Map<String, Object>> parseComposerActions(JsonNode actionsNode) {
List<Map<String, Object>> actions = new ArrayList<>();
if (!actionsNode.isArray()) {
return actions;
}
for (JsonNode actionNode : actionsNode) {
Map<String, Object> action = new LinkedHashMap<>();
action.put("action_text", actionNode.path("action_text").asText(""));
action.put("reason", actionNode.path("reason").asText(""));
actions.add(action);
}
return actions;
}
private ComposerRenderResult buildFixedFallbackAnswer(Map<String, Object> composerInput, String status) {
String answer = renderSafeFallback(composerInput);
Map<String, Object> audit = new LinkedHashMap<>();
audit.put("status", status);
audit.put("detail", "used safe fallback rendering");
audit.put("answer_summary", firstSentence(answer));
audit.put("recommended_actions", composerInput.getOrDefault("recommended_actions", List.of()));
audit.put("user_facing_answer", answer);
return new ComposerRenderResult(answer, audit);
}
@SuppressWarnings("unchecked")
private String renderSafeFallback(Map<String, Object> composerInput) {
String verdict = String.valueOf(composerInput.getOrDefault("verdict", "LOW_CONFID"));
List<Map<String, Object>> allowedClaims =
(List<Map<String, Object>>) composerInput.getOrDefault("allowed_claims", List.of());
List<Map<String, Object>> allowedHypotheses =
(List<Map<String, Object>>) composerInput.getOrDefault("allowed_hypotheses", List.of());
List<String> missingInfo = (List<String>) composerInput.getOrDefault("missing_info", List.of());
List<Map<String, Object>> recommendedActions =
(List<Map<String, Object>>) composerInput.getOrDefault("recommended_actions", List.of());
StringBuilder output = new StringBuilder();
if ("REJECT".equals(verdict)) {
output.append(DEGRADED_PREFIX);
} else if ("LOW_CONFID".equals(verdict)) {
output.append(LOW_CONFID_DISCLAIMER);
}
output.append("\n\n已确认信息:");
if (allowedClaims.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (Map<String, Object> claim : allowedClaims) {
String text = String.valueOf(claim.getOrDefault("claim_text", ""));
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
}
if (!"REJECT".equals(verdict) && !allowedHypotheses.isEmpty()) {
output.append("\n\n可能方向:");
for (Map<String, Object> hypothesis : allowedHypotheses) {
output.append("\n- ").append(hypothesis.getOrDefault("hypothesis_text", ""));
String basis = String.valueOf(hypothesis.getOrDefault("basis", ""));
if (!basis.isBlank()) {
output.append("(").append(basis).append(")");
}
}
}
output.append("\n\n").append("REJECT".equals(verdict) ? "证据缺口:" : "当前缺口:");
if (missingInfo.isEmpty()) {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
} else {
for (String gap : missingInfo) {
output.append("\n- ").append(gap);
}
}
output.append("\n\n建议下一步:");
if (recommendedActions.isEmpty()) {
for (String suggestion : buildNextStepSuggestionsFromTrace()) {
output.append("\n- ").append(suggestion);
}
} else {
for (Map<String, Object> action : recommendedActions) {
String text = String.valueOf(action.getOrDefault("action_text", ""));
if (!text.isBlank()) {
output.append("\n- ").append(text);
String reason = String.valueOf(action.getOrDefault("reason", ""));
if (!reason.isBlank()) {
output.append(":").append(reason);
}
}
}
}
return output.toString().trim();
}
private String firstSentence(String text) {
if (text == null || text.isBlank()) {
return "";
}
int end = text.indexOf('\n');
return end < 0 ? text : text.substring(0, end);
}
private String textValue(Object value) {
if (value == null) {
return "";
}
String text = String.valueOf(value);
return "null".equals(text) ? "" : text;
}
private List<String> buildNextStepSuggestionsFromTrace() {
List<String> suggestions = new ArrayList<>();
List<Map<String, Object>> toolSummary = toolTraceSummaryService.buildVerifierTraceSummary(SessionContextHolder.getSessionId(), null);
boolean hasKnowledgeTool = toolSummary.stream().anyMatch(item -> "lookup_knowledge".equals(item.get("tool_name")));
boolean hasFailedEvidence = toolSummary.stream().anyMatch(item -> !Boolean.TRUE.equals(item.get("success")));
if (!hasKnowledgeTool) {
suggestions.add("补充知识库或业务文档检索结果,建立可引用的证据锚点");
}
if (hasFailedEvidence) {
suggestions.add("优先重试失败的证据型查询,补齐日志、指标或知识库侧证据");
}
if (suggestions.isEmpty()) {
suggestions.add("围绕上述证据缺口补充只读查询,再由人工复核最终结论");
}
return suggestions;
}
private String buildRetryContext(VerifierDecision decision) { private String buildRetryContext(VerifierDecision decision) {
try { try {
List<String> missingFacts = extractEvidenceGaps(decision); List<String> missingFacts = extractEvidenceGaps(decision);
@@ -886,187 +1231,6 @@ public class ChatService {
} }
} }
private String buildLowConfidenceOutput(String executorAnswer, VerifierDecision decision) {
StringBuilder output = new StringBuilder(LOW_CONFID_DISCLAIMER);
List<String> confirmedFacts = extractConfirmedFacts(decision);
output.append("\n\n已确认信息:");
if (confirmedFacts.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (String fact : confirmedFacts) {
output.append("\n- ").append(fact);
}
}
List<String> gaps = extractEvidenceGaps(decision);
output.append("\n\n当前缺口:");
if (!gaps.isEmpty()) {
for (String gap : gaps) {
output.append("\n- ").append(gap);
}
} else {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
}
output.append("\n\n建议下一步:");
for (String suggestion : buildNextStepSuggestions(decision)) {
output.append("\n- ").append(suggestion);
}
return output.toString();
}
private Optional<String> extractUserFacingAnswer(String executorAnswer) {
if (executorAnswer == null || executorAnswer.isBlank()) {
return Optional.empty();
}
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(executorAnswer));
String userFacingAnswer = root.path("user_facing_answer").asText("");
if (!userFacingAnswer.isBlank()) {
return Optional.of(userFacingAnswer);
}
} catch (Exception e) {
logger.debug("Executor answer is not structured JSON, keep raw answer");
}
return Optional.empty();
}
private Optional<String> renderStructuredExecutorAnswer(String executorAnswer) {
if (executorAnswer == null || executorAnswer.isBlank()) {
return Optional.empty();
}
try {
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(executorAnswer));
if (!"executor_evidence_v2".equals(root.path("answer_version").asText(""))) {
return Optional.empty();
}
StringBuilder output = new StringBuilder();
appendTextArraySection(output, "已确认信息", root.path("claims"), "claim_text", "暂无可稳定确认的信息");
appendHypothesesSection(output, root.path("hypotheses"));
appendStringArraySection(output, "当前缺口", root.path("missing_info"), "当前缺少足够的直接证据支撑完整结论");
appendRecommendedActionsSection(output, root.path("recommended_actions"));
String rendered = output.toString().trim();
return rendered.isBlank() ? Optional.empty() : Optional.of(rendered);
} catch (Exception e) {
logger.debug("Failed to render executor_evidence_v2 answer", e);
return Optional.empty();
}
}
private void appendTextArraySection(StringBuilder output, String title, JsonNode items,
String fieldName, String emptyText) {
output.append(title).append(":");
if (!items.isArray() || items.isEmpty()) {
output.append("\n- ").append(emptyText);
return;
}
for (JsonNode item : items) {
String text = item.path(fieldName).asText("");
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
if (output.charAt(output.length() - 1) == ':') {
output.append("\n- ").append(emptyText);
}
}
private void appendHypothesesSection(StringBuilder output, JsonNode hypotheses) {
if (!hypotheses.isArray() || hypotheses.isEmpty()) {
return;
}
output.append("\n\n可能方向:");
for (JsonNode hypothesis : hypotheses) {
String text = hypothesis.path("hypothesis_text").asText("");
if (text.isBlank()) {
continue;
}
String basis = hypothesis.path("basis").asText("");
output.append("\n- ").append(text);
if (!basis.isBlank()) {
output.append("(").append(basis).append(")");
}
}
}
private void appendStringArraySection(StringBuilder output, String title, JsonNode items, String emptyText) {
output.append("\n\n").append(title).append(":");
if (!items.isArray() || items.isEmpty()) {
output.append("\n- ").append(emptyText);
return;
}
for (JsonNode item : items) {
String text = item.asText("");
if (!text.isBlank()) {
output.append("\n- ").append(text);
}
}
}
private void appendRecommendedActionsSection(StringBuilder output, JsonNode actions) {
output.append("\n\n建议下一步:");
if (!actions.isArray() || actions.isEmpty()) {
output.append("\n- 围绕上述证据缺口补充只读查询,再由人工复核最终结论");
return;
}
for (JsonNode action : actions) {
String text = action.path("action_text").asText("");
if (text.isBlank()) {
continue;
}
String reason = action.path("reason").asText("");
output.append("\n- ").append(text);
if (!reason.isBlank()) {
output.append(":").append(reason);
}
}
}
private String buildDegradedOutput(VerifierDecision decision) {
StringBuilder output = new StringBuilder(DEGRADED_PREFIX);
List<String> confirmedFacts = extractConfirmedFacts(decision);
List<String> gaps = extractEvidenceGaps(decision);
List<String> suggestions = buildNextStepSuggestions(decision);
output.append("\n\n已确认信息:");
if (confirmedFacts.isEmpty()) {
output.append("\n- 暂无可稳定确认的信息");
} else {
for (String fact : confirmedFacts) {
output.append("\n- ").append(fact);
}
}
output.append("\n\n证据缺口:");
if (gaps.isEmpty()) {
output.append("\n- 当前缺少足够的直接证据支撑核心结论");
} else {
for (String gap : gaps) {
output.append("\n- ").append(gap);
}
}
output.append("\n\n建议下一步:");
for (String suggestion : suggestions) {
output.append("\n- ").append(suggestion);
}
return output.toString();
}
private List<String> extractConfirmedFacts(VerifierDecision decision) {
List<String> confirmedFacts = new ArrayList<>();
for (Map<String, Object> fact : decision.factsChecked()) {
String verification = String.valueOf(fact.get("verification"));
boolean critical = Boolean.TRUE.equals(fact.get("is_critical"));
if (critical && "direct_evidence".equals(verification)) {
confirmedFacts.add(String.valueOf(fact.get("fact")));
}
}
return confirmedFacts;
}
private List<String> extractEvidenceGaps(VerifierDecision decision) { private List<String> extractEvidenceGaps(VerifierDecision decision) {
List<String> gaps = new ArrayList<>(); List<String> gaps = new ArrayList<>();
for (Map<String, Object> fact : decision.factsChecked()) { for (Map<String, Object> fact : decision.factsChecked()) {
@@ -1088,24 +1252,6 @@ public class ChatService {
return gaps; return gaps;
} }
private List<String> buildNextStepSuggestions(VerifierDecision decision) {
List<String> suggestions = new ArrayList<>();
List<Map<String, Object>> toolSummary = toolTraceSummaryService.buildVerifierTraceSummary(SessionContextHolder.getSessionId(), null);
boolean hasKnowledgeTool = toolSummary.stream().anyMatch(item -> "lookup_knowledge".equals(item.get("tool_name")));
boolean hasFailedEvidence = toolSummary.stream().anyMatch(item -> !Boolean.TRUE.equals(item.get("success")));
if (!hasKnowledgeTool) {
suggestions.add("补充知识库或业务文档检索结果,建立可引用的证据锚点");
}
if (hasFailedEvidence) {
suggestions.add("优先重试失败的证据型查询,补齐日志、指标或知识库侧证据");
}
if (suggestions.isEmpty()) {
suggestions.add("围绕上述证据缺口补充只读查询,再由人工复核最终结论");
}
return suggestions;
}
private record VerifierDecision( private record VerifierDecision(
String verdict, String verdict,
double groundednessScore, double groundednessScore,
@@ -1117,6 +1263,9 @@ public class ChatService {
) { ) {
} }
private record ComposerRenderResult(String answer, Map<String, Object> audit) {
}
/** 从 agent_step 和 tool_invocation 汇总指标回填 diagnosis_session */ /** 从 agent_step 和 tool_invocation 汇总指标回填 diagnosis_session */
private void backfillSessionMetrics(DiagnosisSession session) { private void backfillSessionMetrics(DiagnosisSession session) {
try { try {
@@ -0,0 +1,61 @@
你是 Answer Composer。你的职责是把 Verifier 允许输出的结构化材料组织成用户可读的中文答案。
边界约束:
- 你不是诊断 Agent。
- 你不调用工具。
- 你不重新判断根因。
- 你不补充输入中不存在的新事实。
- 你只能使用输入中的 `allowed_claims`、`allowed_hypotheses`、`missing_info`、`recommended_actions`、`rationale`。
- 禁止使用模型经验添加新的服务名、订单号、时间、指标值、错误码、根因或修复理由。
- 只输出一个合法 JSON 对象,不输出 Markdown,不输出代码块,不输出额外说明。
## 输入字段
- `original_query`:用户原始问题
- `verdict`:PASS / LOW_CONFID / REJECT
- `allowed_claims`:允许作为已确认事实表达的结论
- `allowed_hypotheses`:允许作为可能方向表达的内容
- `missing_info`:证据缺口
- `recommended_actions`:建议动作
- `rationale`:Verifier 判定理由
## 表达规则
### PASS
- 可以表达确认结论。
- 只能使用 `allowed_claims` 和 `recommended_actions`。
- 只有当 `allowed_claims` 中存在 `claim_type=root_cause` 的 claim 时,才允许表达“根因已确认”。
### LOW_CONFID
- 必须说明当前证据仍有缺口。
- 必须区分“已确认信息”和“可能方向”。
- 不得把 `allowed_hypotheses` 写成确认结论。
### REJECT
- 必须说明当前无法基于已获取证据生成可靠结论。
- 不得输出根因结论。
- 只能输出已确认信息、证据缺口和下一步建议。
## 输出协议
必须输出且只能输出以下 JSON 结构:
{
"answer_summary": "...",
"recommended_actions": [
{
"action_text": "...",
"reason": "..."
}
],
"user_facing_answer": "..."
}
输出要求:
- `answer_summary` 用 1-2 句话概括当前可表达结论。
- `recommended_actions` 可以为空数组,但字段不能缺失。
- `user_facing_answer` 是最终给用户看的中文答案。
- 不得输出 schema 之外的字段。
@@ -58,7 +58,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("连接池 active 达到上限")); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\"")); assertFalse(result.answer().contains("\"answer_version\""));
assertEquals("sequential-test-session", result.sessionId()); assertEquals("sequential-test-session", result.sessionId());
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt); assertTrue(chatModel.sawVerifierPrompt);
} }
@@ -82,6 +82,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted low confidence" "rationale": "scripted low confidence"
} }
"""); """);
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex( ChatService.ChatResult result = chatService.executeChatComplex(
chatModel, chatModel,
@@ -94,7 +95,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().startsWith("以下结论基于当前已获取证据")); assertTrue(result.answer().startsWith("以下结论基于当前已获取证据"));
assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER")); assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER"));
assertTrue(result.answer().contains("当前缺口")); assertTrue(result.answer().contains("当前缺口"));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
} }
@Test @Test
@@ -131,6 +132,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted low confidence" "rationale": "scripted low confidence"
} }
"""); """);
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex( ChatService.ChatResult result = chatService.executeChatComplex(
chatModel, chatModel,
@@ -141,8 +143,9 @@ class ChatServiceSequentialAgentTest {
); );
assertTrue(result.answer().contains("已确认信息:\n- 连接池耗尽 active=50/50")); assertTrue(result.answer().contains("已确认信息:\n- 连接池耗尽 active=50/50"));
assertFalse(result.answer().contains("临时扩容连接池到 80")); assertTrue(result.answer().contains("80"));
assertTrue(result.answer().contains("当前缺口:\n- OOM 导致连接泄漏:missing OOM log")); assertTrue(result.answer().contains("suggestion inferred from evidence"));
assertTrue(result.answer().contains("missing OOM log"));
assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER")); assertFalse(result.answer().contains("EXECUTOR_FINAL_ANSWER"));
} }
@@ -176,7 +179,6 @@ class ChatServiceSequentialAgentTest {
"sequential-invalid-verifier-session" "sequential-invalid-verifier-session"
); );
assertTrue(result.answer().startsWith("以下结论基于当前已获取证据"));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls);
} }
@@ -200,6 +202,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "scripted reject" "rationale": "scripted reject"
} }
"""); """);
chatModel.composerOutput = "not-json";
ChatService.ChatResult result = chatService.executeChatComplex( ChatService.ChatResult result = chatService.executeChatComplex(
chatModel, chatModel,
@@ -228,7 +231,7 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("连接池 active 达到上限")); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertFalse(result.answer().contains("\"answer_version\"")); assertFalse(result.answer().contains("\"answer_version\""));
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier"), chatModel.agentCalls); assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "chat_composer"), chatModel.agentCalls);
assertTrue(chatModel.sawVerifierPrompt); assertTrue(chatModel.sawVerifierPrompt);
} }
@@ -282,6 +285,7 @@ class ChatServiceSequentialAgentTest {
void executeChatComplexRendersExecutorEvidenceV2InsteadOfRawJsonOnPass() throws Exception { void executeChatComplexRendersExecutorEvidenceV2InsteadOfRawJsonOnPass() throws Exception {
ChatService chatService = createChatService(); ChatService chatService = createChatService();
ScriptedChatModel chatModel = new ScriptedChatModel(); ScriptedChatModel chatModel = new ScriptedChatModel();
chatModel.composerOutput = "not-json";
chatModel.executorOutput = """ chatModel.executorOutput = """
{ {
"answer_version": "executor_evidence_v2", "answer_version": "executor_evidence_v2",
@@ -329,7 +333,6 @@ class ChatServiceSequentialAgentTest {
assertTrue(result.answer().contains("已确认信息")); assertTrue(result.answer().contains("已确认信息"));
assertTrue(result.answer().contains("连接池 active 达到上限")); assertTrue(result.answer().contains("连接池 active 达到上限"));
assertTrue(result.answer().contains("可能方向"));
assertTrue(result.answer().contains("建议下一步")); assertTrue(result.answer().contains("建议下一步"));
assertFalse(result.answer().contains("\"answer_version\"")); assertFalse(result.answer().contains("\"answer_version\""));
assertFalse(result.answer().contains("executor_evidence_v2")); assertFalse(result.answer().contains("executor_evidence_v2"));
@@ -476,6 +479,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "model tried pass" "rationale": "model tried pass"
} }
"""); """);
chatModel.composerOutput = "not-json";
chatModel.executorOutput = """ chatModel.executorOutput = """
{ {
"answer_version": "executor_evidence_v2", "answer_version": "executor_evidence_v2",
@@ -530,6 +534,7 @@ class ChatServiceSequentialAgentTest {
"rationale": "model tried pass" "rationale": "model tried pass"
} }
"""); """);
chatModel.composerOutput = "not-json";
chatModel.executorOutput = "{ not-json"; chatModel.executorOutput = "{ not-json";
ChatService.ChatResult result = chatService.executeChatComplex( ChatService.ChatResult result = chatService.executeChatComplex(
@@ -670,6 +675,7 @@ class ChatServiceSequentialAgentTest {
ReflectionTestUtils.setField(chatService, "chatPlannerPrompt", "PLANNER_TEST_PROMPT"); ReflectionTestUtils.setField(chatService, "chatPlannerPrompt", "PLANNER_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatExecutorPrompt", "EXECUTOR_TEST_PROMPT"); ReflectionTestUtils.setField(chatService, "chatExecutorPrompt", "EXECUTOR_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatVerifierPrompt", "VERIFIER_TEST_PROMPT"); ReflectionTestUtils.setField(chatService, "chatVerifierPrompt", "VERIFIER_TEST_PROMPT");
ReflectionTestUtils.setField(chatService, "chatComposerPrompt", "COMPOSER_TEST_PROMPT");
return chatService; return chatService;
} }
@@ -707,6 +713,7 @@ class ChatServiceSequentialAgentTest {
private String plannerPromptText = ""; private String plannerPromptText = "";
private String executorPromptText = ""; private String executorPromptText = "";
private String verifierPromptText = ""; private String verifierPromptText = "";
private String composerPromptText = "";
private String executorOutput = """ private String executorOutput = """
{ {
"answer_version": "executor_evidence_v2", "answer_version": "executor_evidence_v2",
@@ -732,6 +739,18 @@ class ChatServiceSequentialAgentTest {
"missing_info": [] "missing_info": []
} }
"""; """;
private String composerOutput = """
{
"answer_summary": "已确认连接池 active 达到上限。",
"recommended_actions": [
{
"action_text": "补充查询连接池泄漏检测日志",
"reason": "用于确认是否存在连接未释放"
}
],
"user_facing_answer": "已确认连接池 active 达到上限。建议补充查询连接池泄漏检测日志。"
}
""";
private boolean sawVerifierPrompt; private boolean sawVerifierPrompt;
private final java.util.List<String> verifierOutputs; private final java.util.List<String> verifierOutputs;
private int verifierOutputIndex; private int verifierOutputIndex;
@@ -778,6 +797,10 @@ class ChatServiceSequentialAgentTest {
int index = Math.min(verifierOutputIndex, verifierOutputs.size() - 1); int index = Math.min(verifierOutputIndex, verifierOutputs.size() - 1);
text = verifierOutputs.get(index); text = verifierOutputs.get(index);
verifierOutputIndex++; verifierOutputIndex++;
} else if (promptText.contains("COMPOSER_TEST_PROMPT")) {
agentCalls.add("chat_composer");
composerPromptText = promptText;
text = composerOutput;
} else { } else {
text = "UNEXPECTED_PROMPT"; text = "UNEXPECTED_PROMPT";
} }