Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4b5015beb |
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: gitnexus-cli
|
||||
description: "Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\""
|
||||
---
|
||||
|
||||
# GitNexus CLI Commands
|
||||
|
||||
All commands work via `npx` — no global install required.
|
||||
|
||||
## Commands
|
||||
|
||||
### analyze — Build or refresh the index
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
|
||||
|
||||
| Flag | Effect |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| `--force` | Force full re-index even if up to date |
|
||||
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
||||
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
|
||||
|
||||
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout.
|
||||
|
||||
### status — Check index freshness
|
||||
|
||||
```bash
|
||||
npx gitnexus status
|
||||
```
|
||||
|
||||
Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
|
||||
|
||||
### clean — Delete the index
|
||||
|
||||
```bash
|
||||
npx gitnexus clean
|
||||
```
|
||||
|
||||
Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
|
||||
|
||||
| Flag | Effect |
|
||||
| --------- | ------------------------------------------------- |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
| `--all` | Clean all indexed repos, not just the current one |
|
||||
|
||||
### wiki — Generate documentation from the graph
|
||||
|
||||
```bash
|
||||
npx gitnexus wiki
|
||||
```
|
||||
|
||||
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
|
||||
|
||||
| Flag | Effect |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `--force` | Force full regeneration |
|
||||
| `--model <model>` | LLM model (default: minimax/minimax-m2.5) |
|
||||
| `--base-url <url>` | LLM API base URL |
|
||||
| `--api-key <key>` | LLM API key |
|
||||
| `--concurrency <n>` | Parallel LLM calls (default: 3) |
|
||||
| `--gist` | Publish wiki as a public GitHub Gist |
|
||||
|
||||
### list — Show all indexed repos
|
||||
|
||||
```bash
|
||||
npx gitnexus list
|
||||
```
|
||||
|
||||
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
|
||||
|
||||
## After Indexing
|
||||
|
||||
1. **Read `gitnexus://repo/{name}/context`** to verify the index loaded
|
||||
2. Use the other GitNexus skills (`exploring`, `debugging`, `impact-analysis`, `refactoring`) for your task
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Not inside a git repository"**: Run from a directory inside a git repo
|
||||
- **Index is stale after re-analyzing**: Restart Claude Code to reload the MCP server
|
||||
- **Embeddings slow**: Omit `--embeddings` (it's off by default) or set `OPENAI_API_KEY` for faster API-based embedding
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: gitnexus-debugging
|
||||
description: "Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: \"Why is X failing?\", \"Where does this error come from?\", \"Trace this bug\""
|
||||
---
|
||||
|
||||
# Debugging with GitNexus
|
||||
|
||||
## When to Use
|
||||
|
||||
- "Why is this function failing?"
|
||||
- "Trace where this error comes from"
|
||||
- "Who calls this method?"
|
||||
- "This endpoint returns 500"
|
||||
- Investigating bugs, errors, or unexpected behavior
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
|
||||
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
|
||||
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
|
||||
```
|
||||
|
||||
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
|
||||
|
||||
## Checklist
|
||||
|
||||
```
|
||||
- [ ] Understand the symptom (error message, unexpected behavior)
|
||||
- [ ] gitnexus_query for error text or related code
|
||||
- [ ] Identify the suspect function from returned processes
|
||||
- [ ] gitnexus_context to see callers and callees
|
||||
- [ ] Trace execution flow via process resource if applicable
|
||||
- [ ] gitnexus_cypher for custom call chain traces if needed
|
||||
- [ ] Read source files to confirm root cause
|
||||
```
|
||||
|
||||
## Debugging Patterns
|
||||
|
||||
| Symptom | GitNexus Approach |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| Error message | `gitnexus_query` for error text → `context` on throw sites |
|
||||
| Wrong return value | `context` on the function → trace callees for data flow |
|
||||
| Intermittent failure | `context` → look for external calls, async deps |
|
||||
| Performance issue | `context` → find symbols with many callers (hot paths) |
|
||||
| Recent regression | `detect_changes` to see what your changes affect |
|
||||
|
||||
## Tools
|
||||
|
||||
**gitnexus_query** — find code related to error:
|
||||
|
||||
```
|
||||
gitnexus_query({query: "payment validation error"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError, PaymentException
|
||||
```
|
||||
|
||||
**gitnexus_context** — full context for a suspect:
|
||||
|
||||
```
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
→ Processes: CheckoutFlow (step 3/7)
|
||||
```
|
||||
|
||||
**gitnexus_cypher** — custom call chain traces:
|
||||
|
||||
```cypher
|
||||
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
|
||||
RETURN [n IN nodes(path) | n.name] AS chain
|
||||
```
|
||||
|
||||
## Example: "Payment endpoint returns 500 intermittently"
|
||||
|
||||
```
|
||||
1. gitnexus_query({query: "payment error handling"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError
|
||||
|
||||
2. gitnexus_context({name: "validatePayment"})
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
→ Step 3: validatePayment → calls fetchRates (external)
|
||||
|
||||
4. Root cause: fetchRates calls external API without proper timeout
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: gitnexus-exploring
|
||||
description: "Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: \"How does X work?\", \"What calls this function?\", \"Show me the auth flow\""
|
||||
---
|
||||
|
||||
# Exploring Codebases with GitNexus
|
||||
|
||||
## When to Use
|
||||
|
||||
- "How does authentication work?"
|
||||
- "What's the project structure?"
|
||||
- "Show me the main components"
|
||||
- "Where is the database logic?"
|
||||
- Understanding code you haven't seen before
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. READ gitnexus://repos → Discover indexed repos
|
||||
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
|
||||
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
||||
```
|
||||
|
||||
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
|
||||
|
||||
## Checklist
|
||||
|
||||
```
|
||||
- [ ] READ gitnexus://repo/{name}/context
|
||||
- [ ] gitnexus_query for the concept you want to understand
|
||||
- [ ] Review returned processes (execution flows)
|
||||
- [ ] gitnexus_context on key symbols for callers/callees
|
||||
- [ ] READ process resource for full execution traces
|
||||
- [ ] Read source files for implementation details
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | What you get |
|
||||
| --------------------------------------- | ------------------------------------------------------- |
|
||||
| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) |
|
||||
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) |
|
||||
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) |
|
||||
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) |
|
||||
|
||||
## Tools
|
||||
|
||||
**gitnexus_query** — find execution flows related to a concept:
|
||||
|
||||
```
|
||||
gitnexus_query({query: "payment processing"})
|
||||
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Symbols grouped by flow with file locations
|
||||
```
|
||||
|
||||
**gitnexus_context** — 360-degree view of a symbol:
|
||||
|
||||
```
|
||||
gitnexus_context({name: "validateUser"})
|
||||
→ Incoming calls: loginHandler, apiMiddleware
|
||||
→ Outgoing calls: checkToken, getUserById
|
||||
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
|
||||
```
|
||||
|
||||
## Example: "How does payment processing work?"
|
||||
|
||||
```
|
||||
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
|
||||
2. gitnexus_query({query: "payment processing"})
|
||||
→ CheckoutFlow: processPayment → validateCard → chargeStripe
|
||||
→ RefundFlow: initiateRefund → calculateRefund → processRefund
|
||||
3. gitnexus_context({name: "processPayment"})
|
||||
→ Incoming: checkoutHandler, webhookHandler
|
||||
→ Outgoing: validateCard, chargeStripe, saveTransaction
|
||||
4. Read src/payments/processor.ts for implementation details
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: gitnexus-guide
|
||||
description: "Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: \"What GitNexus tools are available?\", \"How do I use GitNexus?\""
|
||||
---
|
||||
|
||||
# GitNexus Guide
|
||||
|
||||
Quick reference for all GitNexus MCP tools, resources, and the knowledge graph schema.
|
||||
|
||||
## Always Start Here
|
||||
|
||||
For any task involving code understanding, debugging, impact analysis, or refactoring:
|
||||
|
||||
1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
|
||||
2. **Match your task to a skill below** and **read that skill file**
|
||||
3. **Follow the skill's workflow and checklist**
|
||||
|
||||
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
|
||||
|
||||
## Skills
|
||||
|
||||
| Task | Skill to read |
|
||||
| -------------------------------------------- | ------------------- |
|
||||
| Understand architecture / "How does X work?" | `gitnexus-exploring` |
|
||||
| Blast radius / "What breaks if I change X?" | `gitnexus-impact-analysis` |
|
||||
| Trace bugs / "Why is X failing?" | `gitnexus-debugging` |
|
||||
| Rename / extract / split / refactor | `gitnexus-refactoring` |
|
||||
| Tools, resources, schema reference | `gitnexus-guide` (this file) |
|
||||
| Index, status, clean, wiki CLI commands | `gitnexus-cli` |
|
||||
|
||||
## Tools Reference
|
||||
|
||||
| Tool | What it gives you |
|
||||
| ---------------- | ------------------------------------------------------------------------ |
|
||||
| `query` | Process-grouped code intelligence — execution flows related to a concept |
|
||||
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
|
||||
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
|
||||
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
||||
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
||||
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
||||
| `list_repos` | Discover indexed repos |
|
||||
|
||||
## Resources Reference
|
||||
|
||||
Lightweight reads (~100-500 tokens) for navigation:
|
||||
|
||||
| Resource | Content |
|
||||
| ---------------------------------------------- | ----------------------------------------- |
|
||||
| `gitnexus://repo/{name}/context` | Stats, staleness check |
|
||||
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
|
||||
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
|
||||
| `gitnexus://repo/{name}/processes` | All execution flows |
|
||||
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
|
||||
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |
|
||||
|
||||
## Graph Schema
|
||||
|
||||
**Nodes:** File, Function, Class, Interface, Method, Community, Process
|
||||
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
|
||||
|
||||
```cypher
|
||||
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
||||
RETURN caller.name, caller.filePath
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: gitnexus-impact-analysis
|
||||
description: "Use when the user wants to know what will break if they change something, or needs safety analysis before editing code. Examples: \"Is it safe to change X?\", \"What depends on this?\", \"What will break?\""
|
||||
---
|
||||
|
||||
# Impact Analysis with GitNexus
|
||||
|
||||
## When to Use
|
||||
|
||||
- "Is it safe to change this function?"
|
||||
- "What will break if I modify X?"
|
||||
- "Show me the blast radius"
|
||||
- "Who uses this code?"
|
||||
- Before making non-trivial code changes
|
||||
- Before committing — to understand what your changes affect
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
3. gitnexus_detect_changes() → Map current git changes to affected flows
|
||||
4. Assess risk and report to user
|
||||
```
|
||||
|
||||
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
|
||||
|
||||
## Checklist
|
||||
|
||||
```
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
|
||||
- [ ] Review d=1 items first (these WILL BREAK)
|
||||
- [ ] Check high-confidence (>0.8) dependencies
|
||||
- [ ] READ processes to check affected execution flows
|
||||
- [ ] gitnexus_detect_changes() for pre-commit check
|
||||
- [ ] Assess risk level and report to user
|
||||
```
|
||||
|
||||
## Understanding Output
|
||||
|
||||
| Depth | Risk Level | Meaning |
|
||||
| ----- | ---------------- | ------------------------ |
|
||||
| d=1 | **WILL BREAK** | Direct callers/importers |
|
||||
| d=2 | LIKELY AFFECTED | Indirect dependencies |
|
||||
| d=3 | MAY NEED TESTING | Transitive effects |
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Affected | Risk |
|
||||
| ------------------------------ | -------- |
|
||||
| <5 symbols, few processes | LOW |
|
||||
| 5-15 symbols, 2-5 processes | MEDIUM |
|
||||
| >15 symbols or many processes | HIGH |
|
||||
| Critical path (auth, payments) | CRITICAL |
|
||||
|
||||
## Tools
|
||||
|
||||
**gitnexus_impact** — the primary tool for symbol blast radius:
|
||||
|
||||
```
|
||||
gitnexus_impact({
|
||||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
maxDepth: 3
|
||||
})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
- loginHandler (src/auth/login.ts:42) [CALLS, 100%]
|
||||
- apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]
|
||||
|
||||
→ d=2 (LIKELY AFFECTED):
|
||||
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**gitnexus_detect_changes** — git-diff based impact analysis:
|
||||
|
||||
```
|
||||
gitnexus_detect_changes({scope: "staged"})
|
||||
|
||||
→ Changed: 5 symbols in 3 files
|
||||
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
|
||||
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
|
||||
|
||||
2. READ gitnexus://repo/my-app/processes
|
||||
→ LoginFlow and TokenRefresh touch validateUser
|
||||
|
||||
3. Risk: 2 direct callers, 2 processes = MEDIUM
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: gitnexus-refactoring
|
||||
description: "Use when the user wants to rename, extract, split, move, or restructure code safely. Examples: \"Rename this function\", \"Extract this into a module\", \"Refactor this class\", \"Move this to a separate file\""
|
||||
---
|
||||
|
||||
# Refactoring with GitNexus
|
||||
|
||||
## When to Use
|
||||
|
||||
- "Rename this function safely"
|
||||
- "Extract this into a module"
|
||||
- "Split this service"
|
||||
- "Move this to a new file"
|
||||
- Any task involving renaming, extracting, splitting, or restructuring code
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
|
||||
2. gitnexus_query({query: "X"}) → Find execution flows involving X
|
||||
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
|
||||
4. Plan update order: interfaces → implementations → callers → tests
|
||||
```
|
||||
|
||||
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
|
||||
|
||||
## Checklists
|
||||
|
||||
### Rename Symbol
|
||||
|
||||
```
|
||||
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
|
||||
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
|
||||
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
|
||||
- [ ] gitnexus_detect_changes() — verify only expected files changed
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Extract Module
|
||||
|
||||
```
|
||||
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] Define new module interface
|
||||
- [ ] Extract code, update imports
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Split Function/Service
|
||||
|
||||
```
|
||||
- [ ] gitnexus_context({name: target}) — understand all callees
|
||||
- [ ] Group callees by responsibility
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] Create new functions/services
|
||||
- [ ] Update callers
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
**gitnexus_rename** — automated multi-file rename:
|
||||
|
||||
```
|
||||
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
→ 12 edits across 8 files
|
||||
→ 10 graph edits (high confidence), 2 ast_search edits (review)
|
||||
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
|
||||
```
|
||||
|
||||
**gitnexus_impact** — map all dependents first:
|
||||
|
||||
```
|
||||
gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware, testUtils
|
||||
→ Affected Processes: LoginFlow, TokenRefresh
|
||||
```
|
||||
|
||||
**gitnexus_detect_changes** — verify your changes after refactoring:
|
||||
|
||||
```
|
||||
gitnexus_detect_changes({scope: "all"})
|
||||
→ Changed: 8 files, 12 symbols
|
||||
→ Affected processes: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**gitnexus_cypher** — custom reference queries:
|
||||
|
||||
```cypher
|
||||
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
|
||||
RETURN caller.name, caller.filePath ORDER BY caller.filePath
|
||||
```
|
||||
|
||||
## Risk Rules
|
||||
|
||||
| Risk Factor | Mitigation |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| Many callers (>5) | Use gitnexus_rename for automated updates |
|
||||
| Cross-area refs | Use detect_changes after to verify scope |
|
||||
| String/dynamic refs | gitnexus_query to find them |
|
||||
| External/public API | Version and deprecate properly |
|
||||
|
||||
## Example: Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
→ 12 edits: 10 graph (safe), 2 ast_search (review)
|
||||
→ Files: validator.ts, login.ts, middleware.ts, config.json...
|
||||
|
||||
2. Review ast_search edits (config.json: dynamic reference!)
|
||||
|
||||
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
→ Applied 12 edits across 8 files
|
||||
|
||||
4. gitnexus_detect_changes({scope: "all"})
|
||||
→ Affected: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM — run tests for these flows
|
||||
```
|
||||
@@ -54,4 +54,3 @@ uploads/
|
||||
### docker
|
||||
/volumes
|
||||
/server.pid
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **SuperBizAgent-java** (1001 symbols, 2043 relationships, 78 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **SuperBizAgent-java** (1262 symbols, 2537 relationships, 89 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
@@ -40,4 +40,4 @@ This project is indexed by GitNexus as **SuperBizAgent-java** (1001 symbols, 204
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
<!-- gitnexus:end -->
|
||||
@@ -1,121 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Defaults
|
||||
|
||||
- Reply in **Chinese** unless I explicitly ask for English.
|
||||
- No emojis.
|
||||
- Do not truncate important outputs (logs, diffs, stack traces, commands, or critical reasoning that affects
|
||||
safety/correctness).
|
||||
|
||||
## Refactor policy (legacy code)
|
||||
|
||||
- When existing code is a "big ball of mud" (hard to maintain, clearly bad design,
|
||||
full of hacks), prefer a **clean, full refactor** over stacking more patches
|
||||
on top of it.
|
||||
- A refactor may completely replace internal structure
|
||||
(functions, modules, classes, data flow).
|
||||
- By default, try to preserve externally observable behaviour.
|
||||
If you intentionally change behaviour or protocols, you MUST:
|
||||
- Call out clearly that this is a **behaviour/protocol change**.
|
||||
- Explain why the change is necessary and which code paths/consumers are affected.
|
||||
- Update or add tests to cover the new behaviour.
|
||||
|
||||
## Before touching code (mandatory)
|
||||
|
||||
Find reuse opportunities + Trace the call/dependency chain and impact radius:
|
||||
|
||||
- Use semantic code search first via `codebase-retrieval` tool.
|
||||
- Confirm understanding with LSP: `goToDefinition`, `findReferences`.
|
||||
- Use Grep/Glob for verifying and understanding additional code snippets.
|
||||
|
||||
## Red lines
|
||||
|
||||
- No copy-paste duplication.
|
||||
- Do not break existing externally observable behaviour **unless**:
|
||||
- It is part of a deliberate refactor as described in the refactor policy, and
|
||||
- You clearly document the behavioural change and its impact.
|
||||
- Do not proceed with a known-wrong approach.
|
||||
- Critical paths must have explicit error handling.
|
||||
- Never implement "blindly": always confirm understanding via code reading + references.
|
||||
|
||||
## Task sizing
|
||||
|
||||
- **Simple**
|
||||
- Criteria — single file, clear requirement, < 20 lines changed,
|
||||
clearly local impact.
|
||||
- Handling — after doing the "Before touching code" steps
|
||||
(research + impact analysis + internal three-question checklist),
|
||||
you may execute directly with minimal explanation.
|
||||
- A very short context line is enough;
|
||||
a full breakdown of the checklist is not required.
|
||||
|
||||
- **Medium**
|
||||
- Criteria — 2–5 files, or requires some research, or impact is not obviously local.
|
||||
- Handling — write a short plan (bullet points) → then implement.
|
||||
- Briefly surface the checklist result in the reply
|
||||
(1–3 short lines describing real issue, key reuse, and main impact).
|
||||
|
||||
- **Complex**
|
||||
- Criteria — architecture changes, multiple modules, high uncertainty or risk.
|
||||
- Handling — follow this workflow:
|
||||
1. **RESEARCH**: inspect code and facts only (no proposals yet).
|
||||
2. **PLAN**: present options + tradeoffs + recommendation;
|
||||
use `AskUserQuestion` actively to align with the user;
|
||||
wait for user's confirmation.
|
||||
3. **EXECUTE**: implement exactly the approved plan.
|
||||
4. **REVIEW**: self-check (tests, edge cases, cleanup).
|
||||
|
||||
## Git
|
||||
|
||||
- Do not commit unless I explicitly ask.
|
||||
- Do not push unless I explicitly ask.
|
||||
- Before writing a commit message, glance at a few recent commits and match the repo's style:
|
||||
- `git log -n 5 --oneline`
|
||||
- If there is no obvious existing style, use this default format:
|
||||
- `<type>(<scope>): <description>`
|
||||
- Before any commit: run `git diff` and confirm the exact scope of changes.
|
||||
- Never force-push to `main` / `master` unless the user approves.
|
||||
- Do not add attribution lines in commit messages.
|
||||
|
||||
## Security
|
||||
|
||||
- Never hardcode secrets (keys/passwords/tokens).
|
||||
- Never commit `.env` files or any credentials.
|
||||
- Validate user input at trust boundaries (APIs, CLIs, external data sources).
|
||||
|
||||
## Quality & cleanup
|
||||
|
||||
- Prefer clarity and simplicity first (KISS); apply DRY to remove obvious
|
||||
copy-paste duplication when it does not hurt readability.
|
||||
- If you change a function signature, update **all** call sites.
|
||||
- After changes:
|
||||
- Remove temporary files.
|
||||
- Remove dead/commented-out code.
|
||||
- Remove unused imports.
|
||||
- Remove debug logging that is no longer needed.
|
||||
- Run the smallest meaningful verification (lint/test/build) for the parts you touched.
|
||||
|
||||
## Windows / PowerShell (if used)
|
||||
|
||||
- PowerShell does not support `&&`; use `;` to chain commands.
|
||||
- Quote paths that contain spaces or non-ASCII characters.
|
||||
|
||||
## Baisc Infos
|
||||
|
||||
Unless directly relevant to the user's current question, you should avoid proactively mentioning, illustrating, or
|
||||
trailing off into the following information in 99% of cases:
|
||||
|
||||
## Documentation
|
||||
|
||||
- 所有产生的文档(需求文档、计划文档、分析文档等)统一放到项目内的 `.docs` 文件夹中
|
||||
- 文档目录结构:
|
||||
- 不要将文档放到用户目录(如 `C:\Users\EDY\.claude\`)中
|
||||
|
||||
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **SuperBizAgent-java** (1001 symbols, 2043 relationships, 78 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **SuperBizAgent-java** (1262 symbols, 2537 relationships, 89 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
@@ -154,4 +40,4 @@ This project is indexed by GitNexus as **SuperBizAgent-java** (1001 symbols, 204
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
<!-- gitnexus:end -->
|
||||
@@ -0,0 +1,29 @@
|
||||
# 上下文词汇表
|
||||
|
||||
## 术语
|
||||
|
||||
### ChatModel
|
||||
- 定义:Spring AI 的聊天模型抽象接口,所有 LLM 提供商(DashScope、OpenAI、Ollama 等)都实现此接口
|
||||
- 使用场景:所有需要 LLM 推理/生成回答的代码应面向此接口编程
|
||||
|
||||
### EmbeddingModel
|
||||
- 定义:Spring AI 的文本向量化抽象接口,将文本转换为向量
|
||||
- 使用场景:RAG 流程中将文档文本转为向量存入 Milvus
|
||||
|
||||
### DashScopeChatModel
|
||||
- 定义:DashScope(阿里云)对 ChatModel 的具体实现
|
||||
- 使用场景:当前项目硬编码使用,需要改为通过 ChatModel 接口引用
|
||||
|
||||
### ReactAgent
|
||||
- 定义:Spring AI Alibaba Agent Framework 的反应式 Agent 实现
|
||||
- 使用场景:Planner-Executor-Replanner 多 Agent 协作
|
||||
|
||||
### Spring AI Alibaba Agent Framework
|
||||
- 定义:基于 Spring AI 的多 Agent 协作框架,提供 ReactAgent、PlannerAgent、ExecutorAgent 等
|
||||
- 使用场景:项目核心 Agent 逻辑,ReactAgent.builder().model() 接受 ChatModel 接口
|
||||
|
||||
## 业务规则
|
||||
|
||||
- ChatModel 是唯一 LLM 调用抽象:替换模型只需更换 Spring Boot starter 和配置
|
||||
- EmbeddingModel 是唯一向量化抽象:替换向量模型只需更换 starter 和配置
|
||||
- ReactAgent 已兼容 ChatModel 接口,不绑定 DashScope
|
||||
@@ -0,0 +1,7 @@
|
||||
# devflow 索引
|
||||
|
||||
## 项目
|
||||
|
||||
| 日期 | slug | 领域 | 关键词 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-05-29 | chatmodel-abstraction | 解耦 | ChatModel, EmbeddingModel, DashScope, Spring AI | 进行中 |
|
||||
@@ -0,0 +1,42 @@
|
||||
# ChatModel Abstraction Decisions
|
||||
|
||||
## Question Pool
|
||||
|
||||
| # | 维度 | 问题 | 模式 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| Q1 | 术语 | ChatModel 注入方式:Spring Boot 自动注入 vs 手动工厂创建 | evidence-driven | 已解决 |
|
||||
| Q2 | 边界 | RagService 流式对话:Spring AI ChatModel.stream() 替代 DashScope Generation | evidence-driven | 已解决 |
|
||||
| Q3 | 验收 | VECTOR_DIM 是否需要动态化 | user-interview | 已解决 |
|
||||
| Q4 | 边界 | VectorEmbeddingService 批量向量化:EmbeddingModel 支持批量调用 | evidence-driven | 已解决 |
|
||||
|
||||
## Evidence-driven
|
||||
|
||||
| 结论 | 证据来源 | 是否已汇报用户 |
|
||||
|---|---|---|
|
||||
| ReactAgent.builder().model() 接受 ChatModel 接口 | javap 反编译 | 已汇报 |
|
||||
| ChatModel 应通过 Spring Boot 自动注入 | DashScope starter 自动注册 ChatModel Bean | 已汇报 |
|
||||
| RagService 可用 ChatModel.stream() 替代 Generation | Spring AI 接口有 stream(Prompt) 返回 Flux | 已汇报 |
|
||||
| EmbeddingModel 支持批量调用 | EmbeddingModel.call(EmbeddingRequest) 接受多条文本 | 已汇报 |
|
||||
|
||||
## User-interview
|
||||
|
||||
| 问题原文 | 用户原话 | 确认状态 | OpenSpec 回写 |
|
||||
|---|---|---|---|
|
||||
| VECTOR_DIM 怎么处理? | "配置文件动态化" | 已确认 | 已回写 proposal |
|
||||
|
||||
## 关键取舍
|
||||
|
||||
- 决策:本次只解耦不替换实现
|
||||
- 原因:先验证抽象层正确再换模型
|
||||
- 影响:代码改动不改变运行行为
|
||||
- 风险接受:用户同意先只做解耦
|
||||
- 决策:VECTOR_DIM 从配置文件读取
|
||||
- 原因:换模型时改 yml 即可
|
||||
- 影响:MilvusConstants.VECTOR_DIM 改为从 MilvusProperties 读取
|
||||
|
||||
## 架构审计
|
||||
|
||||
- 风险1:RagService 流式适配 — DashScope Generation 和 Spring AI ChatModel.stream() 返回结构不同,需验证 thinking/content 分离逻辑
|
||||
- 风险2:DashScopeConfig 通用性 — 硬编码 dashscope 配置键,换模型后需改为通用键
|
||||
- 风险3:ChatModel Bean 冲突 — 多 starter 并存时需 @Primary 或条件注解
|
||||
- 低风险/无风险:VectorEmbeddingService、MilvusClientFactory 直接替换无问题
|
||||
@@ -1,81 +0,0 @@
|
||||
# 数据库设计文档索引
|
||||
|
||||
## 📂 文档结构
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # 总览(推荐从这里开始)
|
||||
├── database-design.md # 总览(同 README.md)
|
||||
│
|
||||
├── tables/ # 表设计详细文档
|
||||
│ ├── diagnosis_record.md # 诊断记录表(核心)
|
||||
│ ├── case_library.md # 案例库表
|
||||
│ └── api_document.md # 文档元数据表
|
||||
│
|
||||
└── architecture/ # 架构设计文档
|
||||
├── agent-architecture-mvp.md # ⭐ Agent 架构 MVP 精简版
|
||||
├── agent-architecture.md # Agent 架构完整版(含生产级扩展)
|
||||
├── session-management.md # 会话管理设计
|
||||
└── implementation-plan.md # 实施规划
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速导航
|
||||
|
||||
### 我是开发者
|
||||
1. [总览](README.md) - 了解整体设计
|
||||
2. [diagnosis_record](tables/diagnosis_record.md) - 核心业务表
|
||||
3. [实施规划](architecture/implementation-plan.md) - 开发计划
|
||||
|
||||
### 我是运维
|
||||
1. [总览](README.md) - 了解表结构
|
||||
2. [实施规划](architecture/implementation-plan.md) - 部署检查清单
|
||||
|
||||
### 我是产品
|
||||
1. [总览](README.md) - 了解系统定位
|
||||
2. [会话管理](architecture/session-management.md) - 了解用户交互流程
|
||||
|
||||
---
|
||||
|
||||
## 📋 表清单
|
||||
|
||||
| 表名 | 优先级 | 文档 | 说明 |
|
||||
|------|--------|------|------|
|
||||
| diagnosis_record | P0 | [查看](tables/diagnosis_record.md) | 诊断记录(核心) |
|
||||
| case_library | P0 | [查看](tables/case_library.md) | 案例库 |
|
||||
| api_document | P0 | [查看](tables/api_document.md) | 文档元数据 |
|
||||
|
||||
---
|
||||
|
||||
## 📖 阅读建议
|
||||
|
||||
### 第一次阅读
|
||||
```
|
||||
1. README.md(10分钟)
|
||||
- 了解设计原则
|
||||
- 了解表关系
|
||||
|
||||
2. diagnosis_record.md(15分钟)
|
||||
- 核心表设计
|
||||
- 字段泛化设计
|
||||
|
||||
3. implementation-plan.md(5分钟)
|
||||
- 分阶段实施计划
|
||||
```
|
||||
|
||||
### 深入理解
|
||||
```
|
||||
- case_library.md - 案例推荐机制
|
||||
- api_document.md - 文档管理设计
|
||||
- session-management.md - 会话管理机制
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 文档维护
|
||||
|
||||
- 原完整文档已备份:`database-design-backup-20240622.md`
|
||||
- 每个表的详细设计在 `tables/` 目录
|
||||
- 架构设计在 `architecture/` 目录
|
||||
- 修改表结构时,同步更新对应 Markdown
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
# 数据库设计文档
|
||||
|
||||
## 📚 文档导航
|
||||
|
||||
### 核心表设计
|
||||
- [diagnosis_record](tables/diagnosis_record.md) - 诊断记录表(核心)
|
||||
- [case_library](tables/case_library.md) - 案例库表
|
||||
- [api_document](tables/api_document.md) - 文档元数据表
|
||||
|
||||
### 架构设计
|
||||
- [Agent 架构设计](architecture/agent-architecture.md) - Agent 协作 + Skill + Harness
|
||||
- [会话管理](architecture/session-management.md) - Redis + MySQL 会话管理
|
||||
- [实施规划](architecture/implementation-plan.md) - 分阶段实施计划
|
||||
|
||||
---
|
||||
|
||||
## 一、设计原则
|
||||
|
||||
### 1.1 核心原则
|
||||
- ✅ **简单优先**:满足诊断流程需要,避免过度设计
|
||||
- ✅ **渐进增强**:先实现核心功能,再逐步扩展
|
||||
- ✅ **数据分离**:诊断结果持久化(MySQL),会话上下文临时化(Redis)
|
||||
- ✅ **适度冗余**:避免过度范式化,适当冗余提升查询性能
|
||||
|
||||
### 1.2 系统定位
|
||||
**自动化诊断系统**
|
||||
- 核心:一键诊断 → 返回完整报告
|
||||
- 辅助:支持追问,但不是主要场景
|
||||
- 特点:大部分用户单次诊断即结束,少数用户会追问细节
|
||||
|
||||
---
|
||||
|
||||
## 二、表结构总览
|
||||
|
||||
### 2.1 核心表关系
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ diagnosis_record │ 诊断记录(核心)
|
||||
│ - 每次诊断一条 │
|
||||
└──────────┬──────────┘
|
||||
│ 1:1
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ case_library │ 案例库(知识沉淀)
|
||||
│ - 诊断成功→案例 │
|
||||
└─────────────────────┘
|
||||
|
||||
┌─────────────────────┐
|
||||
│ api_document │ 文档元数据(管理层)
|
||||
│ - 状态追踪/去重 │
|
||||
└──────────┬──────────┘
|
||||
│ doc_id
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ Milvus │ 文档内容(检索层)
|
||||
│ - 向量检索 │
|
||||
└─────────────────────┘
|
||||
|
||||
┌─────────────────────┐
|
||||
│ Redis Session │ 会话管理(临时)
|
||||
│ - 30分钟过期 │
|
||||
│ - 支持追问 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 表统计
|
||||
|
||||
| 表名 | 类型 | 预估数据量 | 用途 |
|
||||
|------|------|-----------|------|
|
||||
| diagnosis_record | 核心 | 3.6万/年 | 诊断记录 |
|
||||
| case_library | 核心 | 500-1000 | 案例库 |
|
||||
| api_document | 核心 | 100-200 | 文档管理 |
|
||||
|
||||
---
|
||||
|
||||
## 三、技术栈
|
||||
|
||||
### 3.1 数据存储
|
||||
```
|
||||
MySQL 8.0+
|
||||
├─ 元数据管理
|
||||
├─ 事务支持
|
||||
└─ JSON 字段支持
|
||||
|
||||
Redis 6.0+
|
||||
├─ 会话存储
|
||||
├─ 缓存
|
||||
└─ TTL 自动过期
|
||||
|
||||
Milvus 2.6+
|
||||
├─ 向量存储
|
||||
├─ 语义检索
|
||||
└─ 混合检索
|
||||
```
|
||||
|
||||
### 3.2 开发框架
|
||||
```
|
||||
Spring Boot 3.2
|
||||
Spring AI Alibaba 1.1.0
|
||||
Milvus SDK Java 2.6.10
|
||||
DashScope SDK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、快速开始
|
||||
|
||||
### 4.1 创建数据库
|
||||
|
||||
```sql
|
||||
-- 1. 创建数据库
|
||||
CREATE DATABASE diagnosis_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 执行建表脚本(按顺序)
|
||||
SOURCE tables/diagnosis_record.sql;
|
||||
SOURCE tables/case_library.sql;
|
||||
SOURCE tables/api_document.sql;
|
||||
```
|
||||
|
||||
### 4.2 初始化 Milvus
|
||||
|
||||
```java
|
||||
// 创建 Collection
|
||||
MilvusClientFactory.createCollection();
|
||||
```
|
||||
|
||||
### 4.3 配置 Redis
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、版本历史
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| v1.0 | 2024-06-15 | 初版,定义核心表结构 |
|
||||
| v2.0 | 2024-06-15 | diagnosis_record 字段泛化,支持多种故障类型 |
|
||||
| v2.1 | 2024-06-22 | 文档拆分,增加 api_document 表 |
|
||||
|
||||
---
|
||||
|
||||
## 六、维护说明
|
||||
|
||||
- 每个表的详细设计在 `tables/` 目录下
|
||||
- 架构设计文档在 `architecture/` 目录下
|
||||
- 修改表结构时,同步更新对应的 Markdown 文档
|
||||
- 重大变更需记录在版本历史中
|
||||
@@ -0,0 +1,342 @@
|
||||
# Essence Report: SuperBizAgent-java — RAG 切片流程
|
||||
|
||||
> **Lens:** mechanical
|
||||
> **Design analyzed:** 四层递进式文档分块算法——标题→章节→段落→句子边界的逐级切割策略
|
||||
> **Files examined:** 3 (`DocumentChunkService.java`, `DocumentChunkConfig.java`, `DocumentChunk.java`)
|
||||
> **Pattern:** Hierarchical Splitter with Sentence-Boundary-Aware Overlap
|
||||
> **Status:** complete
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Deep Dive
|
||||
|
||||
### 核心文件
|
||||
|
||||
| # | 文件 | 行数 | 角色 |
|
||||
|---|------|------|------|
|
||||
| 1 | `service/DocumentChunkService.java` | 229 | 分块引擎本身 |
|
||||
| 2 | `config/DocumentChunkConfig.java` | 32 | 参数契约 `maxSize=800, overlap=100` |
|
||||
| 3 | `dto/DocumentChunk.java` | 59 | 分块数据载体 |
|
||||
|
||||
### 完整调用链
|
||||
|
||||
```
|
||||
VectorIndexService.indexSingleFile()
|
||||
└─ chunkService.chunkDocument(content, filePath) [L35]
|
||||
│
|
||||
├─ splitByHeadings(content) [L44]
|
||||
│ ├─ 正则: ^(#{1,6})\s+(.+)$ [L65]
|
||||
│ ├─ 迭代 matcher.find() 找到每个标题位置
|
||||
│ ├─ 标题之间的内容 → Section(title, content, startIndex)
|
||||
│ └─ → List<Section>
|
||||
│
|
||||
└─ for each Section:
|
||||
└─ chunkSection(section, globalChunkIndex) [L49]
|
||||
│
|
||||
├─ if content.length() ≤ maxSize (800):
|
||||
│ └─ 直接作为一个分块 [L110-119]
|
||||
│
|
||||
├─ else (需要进一步切割):
|
||||
│ ├─ splitByParagraphs(content) [L124]
|
||||
│ │ └─ content.split("\n\n+") [L178]
|
||||
│ │
|
||||
│ ├─ for each paragraph: [L130-167]
|
||||
│ │ ├─ 当前缓冲区 + 新段落 ≤ maxSize? → 继续追加
|
||||
│ │ └─ 当前缓冲区 + 新段落 > maxSize? → 触发切分:
|
||||
│ │ ├─ 保存当前分块
|
||||
│ │ ├─ getOverlapText(当前分块内容) [L147]
|
||||
│ │ │ ├─ 取末尾 overlap(100) 字符
|
||||
│ │ │ ├─ 在重叠文本中找最后一个句子终止符
|
||||
│ │ │ │ max(lastIndexOf('。'), lastIndexOf('?'), lastIndexOf('!'))
|
||||
│ │ │ ├─ if 句子边界 > overlapSize/2 (50字符):
|
||||
│ │ │ │ └─ 从句子边界后截取(保证新块以完整句开头)
|
||||
│ │ │ └─ else:
|
||||
│ │ │ └─ 直接用 overlap 末尾截取
|
||||
│ │ └─ 新缓冲区 = 重叠文本 + 当前段落
|
||||
│ │
|
||||
│ └─ 最后一个分块: 保存缓冲区剩余内容
|
||||
│
|
||||
└─ → List<DocumentChunk>
|
||||
```
|
||||
|
||||
### 算法的四层递进结构
|
||||
|
||||
```
|
||||
第1层:标题分割
|
||||
输入:"# CPU高负载\n内容...\n## 排查步骤\n内容..."
|
||||
输出:Section("CPU高负载", "内容..."), Section("排查步骤", "内容...")
|
||||
作用:保持文档结构,同一主题的内容不被拆散
|
||||
|
||||
第2层:容量判断
|
||||
if section.length() ≤ 800: 整个章节 = 一个分块
|
||||
else: 进入段落级切割
|
||||
作用:短章节保持完整,不破坏语义
|
||||
|
||||
第3层:段落边界切割
|
||||
输入:超长章节的全部段落
|
||||
算法:逐个追加段落到缓冲区,超过 maxSize 时触发一次切分
|
||||
作用:不在段落中间截断
|
||||
|
||||
第4层:重叠窗口 + 句子边界对齐
|
||||
输入:即将被切断的分块末尾
|
||||
算法:取末尾100字符 → 找最近的。?! → 从该位置之后截取作为下一块的"种子"
|
||||
作用:相邻分块在语义上是"连续"的,检索时召回更完整
|
||||
```
|
||||
|
||||
### 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DOC[/"原始文档"/] --> L1{"第1层: splitByHeadings()"}
|
||||
|
||||
L1 --> S1["Section 1<br/>title: CPU高负载<br/>content: ..."]
|
||||
L1 --> S2["Section 2<br/>title: 排查步骤<br/>content: ..."]
|
||||
L1 --> S3["Section N"]
|
||||
|
||||
S1 --> L2{"第2层: 容量判断"}
|
||||
S2 --> L2
|
||||
S3 --> L2
|
||||
|
||||
L2 -->|"≤800字符"| CHUNK["作为1个分块<br/>继承 title"]
|
||||
L2 -->|">800字符"| L3{"第3层: splitByParagraphs()<br/>在段落边界切分"}
|
||||
|
||||
L3 --> BUF["逐段追加到缓冲区"]
|
||||
BUF --> CHECK{"buf + para<br/>> maxSize?"}
|
||||
CHECK -->|否| APPEND["追加段落<br/>继续累积"]
|
||||
CHECK -->|是| L4{"第4层: getOverlapText()<br/>句子边界校准"}
|
||||
|
||||
APPEND --> CHECK
|
||||
|
||||
L4 --> FIND["在重叠区末尾100字符<br/>找最近的 。?!"]
|
||||
FIND --> EVAL{"句子边界位置<br/>> overlapSize/2?"}
|
||||
EVAL -->|是| ALIGN["从句号后截取<br/>保证新块以完整句开头"]
|
||||
EVAL -->|否| RAW["退回原始截取<br/>直接用末尾100字符"]
|
||||
|
||||
ALIGN --> SEED["种子 + 当前段落<br/>→ 新缓冲区"]
|
||||
RAW --> SEED
|
||||
SEED --> CHECK
|
||||
|
||||
CHUNK --> RESULT[/"List<DocumentChunk><br/>每个携带: content + title + startIndex + endIndex + chunkIndex"/]
|
||||
```
|
||||
|
||||
### 关键代码证据
|
||||
|
||||
#### 第1层——标题正则
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:65
|
||||
Pattern headingPattern = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
|
||||
```
|
||||
|
||||
支持 H1-H6,`MULTILINE` 模式让 `^` 匹配行首而非仅字符串首。
|
||||
|
||||
#### 第2层——容量判断(短路)
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:110-119
|
||||
if (content.length() <= chunkConfig.getMaxSize()) {
|
||||
DocumentChunk chunk = new DocumentChunk(content, startIndex, endIndex, chunkIndex);
|
||||
chunk.setTitle(title);
|
||||
chunks.add(chunk);
|
||||
return chunks; // 直接返回,不进入段落切割
|
||||
}
|
||||
```
|
||||
|
||||
#### 第3层——段落级触发切分
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132-148
|
||||
if (currentChunk.length() > 0 &&
|
||||
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 触发切分:保存当前块
|
||||
String overlap = getOverlapText(chunkContent); // 提取重叠文本
|
||||
currentChunk = new StringBuilder(overlap); // 新块以重叠文本开头
|
||||
currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length();
|
||||
}
|
||||
currentChunk.append(paragraph).append("\n\n"); // 继续追加
|
||||
```
|
||||
|
||||
#### 第4层——句子边界检测(核心巧思)
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:193-213
|
||||
private String getOverlapText(String text) {
|
||||
int overlapSize = Math.min(chunkConfig.getOverlap(), text.length());
|
||||
String overlap = text.substring(text.length() - overlapSize);
|
||||
|
||||
// 在重叠文本中找最近的句子终止符
|
||||
int lastSentenceEnd = Math.max(
|
||||
overlap.lastIndexOf('。'),
|
||||
Math.max(overlap.lastIndexOf('?'), overlap.lastIndexOf('!'))
|
||||
);
|
||||
|
||||
// 质量阈值:只有句子边界在重叠区后半段才采用
|
||||
if (lastSentenceEnd > overlapSize / 2) {
|
||||
return overlap.substring(lastSentenceEnd + 1).trim();
|
||||
}
|
||||
return overlap.trim(); // 退回普通重叠
|
||||
}
|
||||
```
|
||||
|
||||
`overlapSize / 2` 条件是一个**质量阈值**。如果最近的句子边界在重叠区的前半段(即离截断点太远),说明分块点本身就接近句子边界,不需要特殊处理。只有句子边界明显位于重叠区后半段时才调整——避免把半个句子作为新块的"种子"。
|
||||
|
||||
### 数据流契约
|
||||
|
||||
```
|
||||
chunkDocument(content, filePath)
|
||||
│
|
||||
│ IN: String content — 原始文档全文
|
||||
│ String filePath — 仅用于日志
|
||||
│
|
||||
│ INNER CLASS: Section
|
||||
│ String title — 所在标题(可为 null)
|
||||
│ String content — 标题下的所有文本
|
||||
│ int startIndex — 在原文档中的字符偏移
|
||||
│
|
||||
│ OUT: List<DocumentChunk>
|
||||
│ String content — 分块文本
|
||||
│ int startIndex — 在原文档中的起始位置
|
||||
│ int endIndex — 在原文档中的结束位置
|
||||
│ int chunkIndex — 分块序号 (0, 1, 2, ...)
|
||||
│ String title — 所属章节标题(继承自 Section)
|
||||
│
|
||||
└─ 消费者: VectorIndexService.indexSingleFile():142
|
||||
→ 遍历 chunks → embeddingService.generateEmbedding(chunk.content)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Extract Pattern
|
||||
|
||||
### 模式名:Hierarchical Splitter with Sentence-Boundary-Aware Overlap
|
||||
|
||||
**一句话:** 从粗到细逐级切割——先按文档结构(标题)分章,再按语义边界(段落)分块,最后在切分点用句子终止符校准重叠窗口。
|
||||
|
||||
### 问题
|
||||
|
||||
固定长度切割的典型失败场景:
|
||||
|
||||
```
|
||||
切在句子中间: "CPU使用率达到 95%,建议" | "立即重启相关服务"
|
||||
↑ 检索"CPU问题"时召回这块——后半句完全脱离上下文,LLM 误判
|
||||
|
||||
切在段落中间:"## 排查步骤\n1. 查看监控\n2. 检" | "查日志\n3. 重启服务"
|
||||
↑ 步骤 2 被切断,Agent 拿着残缺的排查步骤执行操作
|
||||
```
|
||||
|
||||
### 替代方案对比
|
||||
|
||||
| 方案 | 切分依据 | 优势 | 劣势 |
|
||||
|------|----------|------|------|
|
||||
| **固定字符切割**(最简陋) | maxSize,不关心内容 | 实现简单 | 句子截断、丢失语义 |
|
||||
| **递归字符切割**(LangChain RecursiveTextSplitter) | `\n\n` → `\n` → ` ` → `` | 通用性好 | 不理解 Markdown 结构 |
|
||||
| **语义切割**(用 LLM 判断切点) | LLM 标注切分位置 | 理论上最优 | 慢、贵、不可预测 |
|
||||
| **本项目:层级式+句子校准** | 标题→段落→句子终止符 | 快速 + 保留文档结构 | 仅支持 Markdown,非标题文档退化为段落切割 |
|
||||
|
||||
### 为什么标题分割放在第一步?
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:43-44
|
||||
// 1. 首先尝试按标题分割(Markdown格式)
|
||||
List<Section> sections = splitByHeadings(content);
|
||||
```
|
||||
|
||||
看本项目的知识库文档就懂了:
|
||||
|
||||
```markdown
|
||||
# CPU高负载问题排查 ← 一个独立主题
|
||||
## 问题现象
|
||||
...
|
||||
## 排查步骤 ← 这些步骤必须完整才能被 Agent 执行
|
||||
1. 使用 top 命令确认 CPU 使用率最高的进程
|
||||
2. 检查对应服务的日志
|
||||
3. ...
|
||||
## 解决方案
|
||||
...
|
||||
|
||||
# 内存高负载问题排查 ← 另一个独立主题
|
||||
...
|
||||
```
|
||||
|
||||
如果把「CPU 排查步骤」和「内存排查步骤」混在一个分块里,Agent 查询"CPU 高"时会召回包含内存排查步骤的分块——噪声干扰判断。
|
||||
|
||||
标题优先分割 = **用文档作者自己标注的结构来界定语义边界**,比任何算法都准确。
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Migrate
|
||||
|
||||
### 可迁移性
|
||||
|
||||
这个切分策略**直接可用**于任何需要为 Markdown 文档建 RAG 的项目。三个参数全部可配置:
|
||||
|
||||
```yaml
|
||||
# application.yml — 按文档类型调整
|
||||
document:
|
||||
chunk:
|
||||
max-size: 800 # 短文档(API文档)可设500,长文档(周报)可设1200
|
||||
overlap: 100 # 800的12.5%,保持比例即可
|
||||
```
|
||||
|
||||
### Steal-it 示例(17 行)
|
||||
|
||||
```java
|
||||
/**
|
||||
* 四层递进分块:标题 → 章节 → 段落 → 句子校准
|
||||
* 依赖:maxSize / overlap 两个参数
|
||||
*/
|
||||
public List<Chunk> chunk(String doc) {
|
||||
List<Chunk> result = new ArrayList<>();
|
||||
int globalIdx = 0;
|
||||
|
||||
// 第1层:按标题分章
|
||||
for (Section sec : splitByHeadings(doc)) {
|
||||
if (sec.content.length() <= maxSize) {
|
||||
// 第2层:短章节直接作为一个分块
|
||||
result.add(new Chunk(sec.content, sec.title, globalIdx++));
|
||||
} else {
|
||||
// 第3层:超长章节在段落边界切分
|
||||
String overlap = "";
|
||||
for (String para : sec.content.split("\n\n+")) {
|
||||
String candidate = overlap + para;
|
||||
if (candidate.length() > maxSize && !overlap.isEmpty()) {
|
||||
result.add(new Chunk(overlap, sec.title, globalIdx++));
|
||||
overlap = tailOverlap(overlap); // 第4层:句子校准
|
||||
}
|
||||
overlap = (overlap.isEmpty() ? "" : overlap + "\n\n") + para;
|
||||
}
|
||||
if (!overlap.isEmpty()) result.add(new Chunk(overlap, sec.title, globalIdx++));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 落地陷阱
|
||||
|
||||
| 陷阱 | 原因 | 规避 |
|
||||
|------|------|------|
|
||||
| **非 Markdown 文档退化为单块** | `splitByHeadings()` 找不到标题时整个文档作为一个 Section | L93-96:返回一个 Section,后续段落切割仍生效 |
|
||||
| **代码块内的 `#` 被误识别为标题** | 正则不区分代码块和正文 | 未规避——可加反引号检测 `` ``` `` |
|
||||
| **overlap=0 时句子校准无效** | `getOverlapText` 第一行 `Math.min(0, length)=0` 返回空串 | L194:直接返回空字符串,跳过校准 |
|
||||
| **单段落超过 maxSize 不做切割** | `splitByParagraphs` 后每个段落作为一个单位 | L132 条件要求 `currentChunk.length() > 0`,首段落即使超长也会被单独保存为一块 |
|
||||
|
||||
---
|
||||
|
||||
### Self-review
|
||||
|
||||
- [x] 设计真实——每层切割均有代码行号证据
|
||||
- [x] 深度足够——追溯到正则、条件分支、句子校准的数学逻辑
|
||||
- [x] 迁移示例 17 行——提取了四层递进的核心骨架
|
||||
- [x] 陷阱具体到代码行——非 Markdown 退化为单块(L93)、单段落超长不切割(L132)
|
||||
|
||||
```
|
||||
Essence Report: SuperBizAgent-java — RAG 切片流程
|
||||
Lens: mechanical
|
||||
Design analyzed: 四层递进式文档分块算法
|
||||
Files examined: 3
|
||||
Pattern: Hierarchical Splitter with Sentence-Boundary-Aware Overlap
|
||||
Migration: 17-line steal-it skeleton
|
||||
HTML generated: no
|
||||
Status: complete
|
||||
```
|
||||
@@ -0,0 +1,314 @@
|
||||
# Essence Report: SuperBizAgent-java — RAG 实现
|
||||
|
||||
> **Lens:** mechanical(机械论——结构、接口、数据流)
|
||||
> **Design analyzed:** RAG 管道——从文档上传到 Agent 辅助检索的完整写入/读取双路径
|
||||
> **Files examined:** 12
|
||||
> **Pattern:** Pipeline-as-Services + Agent-Mediated Retrieval
|
||||
> **Status:** complete
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 定位 — 设计目标确认
|
||||
|
||||
来自 `/explore` 报告的「设计二:完整的 RAG 管道(5 级流水线)」。用户指定深入 RAG 实现部分。
|
||||
|
||||
涉及 12 个核心文件,跨越 controller → service → client → constant 四层。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Deep Dive — 逐文件追踪
|
||||
|
||||
### 核心文件清单
|
||||
|
||||
| # | 文件 | 角色 | 暴露接口 |
|
||||
|---|------|------|----------|
|
||||
| 1 | `constant/MilvusConstants.java` | Schema 契约常量 | `VECTOR_DIM=1024`, `COLLECTION_NAME="biz"` |
|
||||
| 2 | `client/MilvusClientFactory.java` | 数据库初始化 | `createClient()` → 自动建表+建索引 |
|
||||
| 3 | `config/DocumentChunkConfig.java` | 分块参数 | `maxSize=800`, `overlap=100` |
|
||||
| 4 | `dto/DocumentChunk.java` | 分块实体 | `content`, `startIndex/endIndex`, `chunkIndex`, `title` |
|
||||
| 5 | `service/DocumentChunkService.java` | 智能分块器 | `chunkDocument(content, filePath)` → `List<DocumentChunk>` |
|
||||
| 6 | `service/VectorEmbeddingService.java` | 向量化网关 | `generateEmbedding(text)` → `List<Float>` (1024-dim) |
|
||||
| 7 | `service/VectorIndexService.java` | 写入管道编排 | `indexSingleFile(path)` → 读→删旧→分块→向量化→写 |
|
||||
| 8 | `service/VectorSearchService.java` | 语义检索 | `searchSimilarDocuments(query, topK)` → `List<SearchResult>` |
|
||||
| 9 | `service/RagService.java` | 全栈 RAG 问答 | `queryStream(question, history, callback)` → SSE流式 |
|
||||
| 10 | `agent/tool/InternalDocsTools.java` | Agent 工具桥 | `queryInternalDocs(query)` → JSON(仅检索,不生文) |
|
||||
| 11 | `controller/FileUploadController.java` | 写入入口 | `POST /api/upload` → 文件存储 + 自动索引 |
|
||||
| 12 | `controller/ChatController.java` | 读取入口 | `POST /api/chat(_stream)` → ReactAgent + 工具调用 |
|
||||
|
||||
### 完整的调用链(双路径)
|
||||
|
||||
#### 写入路径(索引管道)
|
||||
|
||||
```
|
||||
POST /api/upload
|
||||
└─ FileUploadController.upload() [L34]
|
||||
├─ Files.copy() → 保存文件到 uploadPath
|
||||
└─ VectorIndexService.indexSingleFile() [L124]
|
||||
├─ Files.readString() [L135]
|
||||
├─ deleteExistingData() [L173]
|
||||
│ └─ milvusClient.delete() [L198]
|
||||
│ expr: metadata["_source"] == "/path/to/file"
|
||||
├─ chunkService.chunkDocument() [L142]
|
||||
│ ├─ splitByHeadings() [L61]
|
||||
│ │ └─ 正则: ^(#{1,6})\s+(.+)$
|
||||
│ ├─ chunkSection() × N [L104]
|
||||
│ │ ├─ splitByParagraphs() [L174]
|
||||
│ │ └─ getOverlapText() [L193]
|
||||
│ └─ → List<DocumentChunk>
|
||||
└─ for each chunk: [L146]
|
||||
├─ embeddingService.generateEmbedding() [L76]
|
||||
│ └─ DashScope TextEmbedding API → List<Float>[1024]
|
||||
└─ insertToMilvus() [L255]
|
||||
└─ UUID(source+chunkIndex) + vector + content + metadata(JSON)
|
||||
```
|
||||
|
||||
#### 读取路径(Agent 中介检索)
|
||||
|
||||
```
|
||||
POST /api/chat_stream
|
||||
└─ ChatController.chatStream() [L143]
|
||||
└─ chatService.createReactAgent() [L183]
|
||||
└─ tools: [DateTimeTools, InternalDocsTools, QueryMetricsTools, QueryLogsTools]
|
||||
└─ agent.stream(question) [L189]
|
||||
└─ Agent 自主决策 → 调用 queryInternalDocs
|
||||
└─ InternalDocsTools.queryInternalDocs() [L53]
|
||||
└─ VectorSearchService.searchSimilarDocuments() [L42]
|
||||
├─ embeddingService.generateQueryVector() [L47]
|
||||
├─ milvusClient.search() [L51]
|
||||
│ └─ L2距离, IVF_FLAT, nprobe=10
|
||||
└─ → List<SearchResult>{id, content, score, metadata}
|
||||
└─ return JSON to Agent
|
||||
└─ Agent 融合检索结果 + LLM推理 → 最终回答
|
||||
```
|
||||
|
||||
### 架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 写入路径
|
||||
UPLOAD[POST /api/upload]
|
||||
FC[FileUploadController]
|
||||
VIS[VectorIndexService]
|
||||
DCS[DocumentChunkService]
|
||||
VES[VectorEmbeddingService]
|
||||
MV_W[(Milvus)]
|
||||
end
|
||||
|
||||
subgraph 读取路径
|
||||
CHAT[POST /api/chat_stream]
|
||||
CC[ChatController]
|
||||
AGENT[ReactAgent]
|
||||
IDT[InternalDocsTools<br/>@Tool注解]
|
||||
VSS[VectorSearchService]
|
||||
MV_R[(Milvus)]
|
||||
LLM[DashScope LLM]
|
||||
end
|
||||
|
||||
UPLOAD --> FC
|
||||
FC --> VIS
|
||||
VIS --> DCS --> VIS
|
||||
VIS --> VES --> VIS
|
||||
VIS --> MV_W
|
||||
|
||||
CHAT --> CC
|
||||
CC --> AGENT
|
||||
AGENT -->|自主决策调用| IDT
|
||||
IDT --> VSS
|
||||
VSS --> VES --> VSS
|
||||
VSS --> MV_R
|
||||
IDT -->|JSON结果| AGENT
|
||||
AGENT --> LLM
|
||||
LLM -->|SSE流式| CC
|
||||
```
|
||||
|
||||
### 关键设计决策(代码证据)
|
||||
|
||||
#### 1. 幂等上传——元数据驱动的去重策略
|
||||
|
||||
```java
|
||||
// VectorIndexService.java:138-139
|
||||
// 删除该文件的旧数据(如果存在)
|
||||
deleteExistingData(path.toString());
|
||||
```
|
||||
|
||||
`deleteExistingData()` (L173-215) 使用 `metadata["_source"] == filePath` 作为删除表达式。每次上传同一文件时,先清空旧向量再写入新数据,保证数据一致性。
|
||||
|
||||
#### 2. 路径标准化——跨平台一致性
|
||||
|
||||
```java
|
||||
// VectorIndexService.java:176-178
|
||||
Path path = Paths.get(filePath).normalize();
|
||||
String normalizedPath = path.toString().replace(File.separator, "/");
|
||||
```
|
||||
|
||||
Windows `\` 和 Unix `/` 统一为正斜杠,避免 Milvus 表达式解析错误。在 `deleteExistingData()` 和 `buildMetadata()` 中均有应用。
|
||||
|
||||
#### 3. 重叠窗口 + 句子边界感知
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132-148
|
||||
if (currentChunk.length() > 0 &&
|
||||
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 保存当前分片
|
||||
String overlap = getOverlapText(chunkContent); // 提取重叠文本
|
||||
currentChunk = new StringBuilder(overlap); // 新分片以重叠文本开头
|
||||
```
|
||||
|
||||
`getOverlapText()` (L193-213) 更进一步:在重叠文本中寻找句子边界(`。?!`),避免在句子中间截断。当句子边界超过 `overlapSize/2` 时才使用,否则退回原始重叠策略。
|
||||
|
||||
#### 4. 检索与生成分离
|
||||
|
||||
`InternalDocsTools.queryInternalDocs()` 只做检索,不做生成。它将搜索结果序列化为 JSON 返回给 Agent,由 Agent 的 LLM 自行判断如何使用这些信息。
|
||||
|
||||
```java
|
||||
// InternalDocsTools.java:68
|
||||
String resultJson = objectMapper.writeValueAsString(searchResults);
|
||||
return resultJson;
|
||||
```
|
||||
|
||||
对比 `RagService.queryStream()` 则完整执行「检索→构建上下文→LLM 生成」三步,是一个独立的全栈 RAG 备用路径。
|
||||
|
||||
#### 5. Milvus Schema 设计
|
||||
|
||||
```java
|
||||
// MilvusClientFactory.java:109-142
|
||||
// 四个字段:
|
||||
// id VarChar(256) 主键 — UUID(source + chunkIndex)
|
||||
// vector FloatVector(1024) — text-embedding-v4 输出
|
||||
// content VarChar(8192) — 分块后的文本内容
|
||||
// metadata JSON — {_source, _extension, _file_name, chunkIndex, totalChunks, title}
|
||||
// 索引: IVF_FLAT, L2距离, nlist=128
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Extract Pattern
|
||||
|
||||
### 设计模式:Pipeline-as-Services + Agent-Mediated Retrieval
|
||||
|
||||
**问题:** 如何将知识库文档转化为 AI Agent 可检索、可利用的语义记忆?
|
||||
|
||||
**传统方案的问题:**
|
||||
- 关键词检索:无法理解语义相似的查询
|
||||
- 硬编码 FAQ:无法应对未见过的问题
|
||||
- 直接向量检索 + 固定提示词:所有问题都触发检索,浪费资源
|
||||
|
||||
**本项目的方案:两阶段架构**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ STAGE 1: 写入管道 (离线/上传时触发) │
|
||||
│ │
|
||||
│ 文档 ──→ 智能分块 ──→ 向量化 ──→ Milvus存储 │
|
||||
│ (标题+段落 (text-embedding (IVF_FLAT │
|
||||
│ 边界感知) -v4, 1024-dim) L2索引) │
|
||||
│ │
|
||||
│ 接口契约: │
|
||||
│ IN: File → OUT: N × (vector + content + meta) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ STAGE 2: 读取管道 (Agent 决策时触发) │
|
||||
│ │
|
||||
│ 用户问题 ──→ Agent 思考 ──→ 决定查知识库 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 向量检索 (L2距离) ──→ Top-K 文档片段 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Agent 融合检索结果 + LLM推理 → 回答 │
|
||||
│ │
|
||||
│ 接口契约: │
|
||||
│ IN: query(自然语言) → OUT: JSON(检索结果) │
|
||||
│ Agent 自主决定: 是否调用 / 如何使用结果 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 接口契约(隐式——通过 Spring DI 实现)
|
||||
|
||||
| 契约 | 生产者 | 消费者 | 数据形状 |
|
||||
|------|--------|--------|----------|
|
||||
| `List<DocumentChunk>` | DocumentChunkService | VectorIndexService | `{content, startIndex, endIndex, chunkIndex, title}` |
|
||||
| `List<Float>[1024]` | VectorEmbeddingService | VectorIndexService, VectorSearchService | DashScope text-embedding-v4 输出 |
|
||||
| `List<SearchResult>` | VectorSearchService | InternalDocsTools, RagService | `{id, content, score, metadata}` |
|
||||
| `StreamCallback` | RagService | (外部调用者) | `{onSearchResults, onContentChunk, onComplete, onError}` |
|
||||
|
||||
### 替代方案对比
|
||||
|
||||
| 方案 | 本项目 | LangChain4j | 纯 DashScope API |
|
||||
|------|--------|--------------|-------------------|
|
||||
| 分块策略 | 标题感知 + 段落边界 + 句子重叠 | 多种内置 Splitter | 无,需自建 |
|
||||
| 向量库 | Milvus (IVF_FLAT) | 多后端支持 | 无 |
|
||||
| Agent 集成 | Spring AI @Tool 注解,Agent 自主决策 | AiServices + @Tool | 无 Agent 框架 |
|
||||
| 去重 | metadata["_source"] 匹配删除 | 需自定义 | 不适用 |
|
||||
|
||||
### 为什么选择这种设计?
|
||||
|
||||
1. **「检索」和「生成」分离**:`InternalDocsTools` 只返回检索结果,生成由 Agent 的 LLM 完成。Agent 可以选择**不使用**检索结果(如果检索质量不高),或者**交叉验证**多次检索的结果
|
||||
2. **工具化 RAG**:将 RAG 暴露为 Agent 工具而非独立 API,让 Agent 在合适的时机触发检索——而非对所有问题都做 RAG
|
||||
3. **5 个独立 Service**:每个阶段可单独替换。想换分块策略?只改 `DocumentChunkService`。想换向量库?只改 `VectorSearchService` + `VectorIndexService`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Migrate — 可迁移的设计
|
||||
|
||||
### 可迁移性评估
|
||||
|
||||
这个 RAG 设计**高度可迁移**到任何需要「知识库 + AI Agent」的 Java 项目。核心依赖是 Spring AI 生态 + 一个向量数据库。
|
||||
|
||||
### Steal-it 示例(12 行)
|
||||
|
||||
```java
|
||||
// 核心思想:Pipeline-as-Services + Agent Tool Bridge
|
||||
// 以下骨架可直接用于任何 Spring Boot 项目
|
||||
|
||||
// 1. 分块器:语义感知分割
|
||||
public List<Chunk> chunk(String doc) {
|
||||
return splitByHeadings(doc).stream()
|
||||
.flatMap(s -> splitToFit(s, MAX_SIZE, OVERLAP))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 2. Agent 工具桥:检索但不生文
|
||||
@Component
|
||||
class KnowledgeBaseTool {
|
||||
@Tool(description = "搜索内部知识库获取相关信息")
|
||||
public String search(@ToolParam(description="查询内容") String query) {
|
||||
List<Float> qv = embedder.embed(query); // 向量化
|
||||
var results = vectorDB.search(qv, TOP_K); // 语义检索
|
||||
return toJson(results); // 返回给Agent
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 落地陷阱
|
||||
|
||||
| 陷阱 | 说明 | 本项目如何规避 |
|
||||
|------|------|----------------|
|
||||
| **路径分隔符不一致** | Windows `\` vs Unix `/` 导致 Milvus 表达式解析失败 | `VectorIndexService.java:177` 强制 `replace(File.separator, "/")` |
|
||||
| **重复上传污染数据** | 同一文件多次上传产生重复向量 | `VectorIndexService.java:138-139` delete-before-insert |
|
||||
| **分块边界截断语义** | 固定长度切割可能切断句子 | `DocumentChunkService.java:203-206` 在重叠区找句子边界 |
|
||||
| **Agent 未触发工具** | Agent 不知道何时该查知识库 | `InternalDocsTools.java:49-52` @Tool description 用英文详细描述触发场景 |
|
||||
| **向量维度不匹配** | embedding 模型输出维度与 Milvus schema 不一致 | `MilvusConstants.java:18` 集中管理 `VECTOR_DIM=1024` |
|
||||
| **API Key 未初始化** | 静态 Constants 被其他线程覆盖 | `VectorEmbeddingService.java:86-89` 每次调用前检查并修复 |
|
||||
|
||||
### Self-review
|
||||
|
||||
- [x] 设计真实存在 — 每个声明均有文件+行号证据
|
||||
- [x] 分析深度足够 — 完整追踪了写入/读取两条全路径
|
||||
- [x] 迁移示例≤20行 — 仅提取 Pipeline + Tool Bridge 骨架
|
||||
- [x] 陷阱具体 — 每个都有代码规避证据
|
||||
- [x] 可解释为什么优于替代方案 — Agent 自主决策 vs 强制 RAG
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
Essence Report: SuperBizAgent-java
|
||||
Lens: mechanical
|
||||
Design analyzed: RAG 管道 — Pipeline-as-Services + Agent-Mediated Retrieval
|
||||
Files examined: 12
|
||||
Pattern: Pipeline-as-Services + Agent Tool Bridge
|
||||
Migration: 12-line steal-it skeleton
|
||||
HTML generated: no
|
||||
Status: complete
|
||||
```
|
||||
@@ -0,0 +1,352 @@
|
||||
# Explore Report: SuperBizAgent-java
|
||||
|
||||
> 生成时间: 2026-04-30
|
||||
> Project type: **code repository**
|
||||
> Phases completed: 4/4
|
||||
> Diagram included: yes
|
||||
> Core designs: 3
|
||||
> Status: complete
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Positioning & Structure
|
||||
|
||||
### 这是什么项目
|
||||
|
||||
SuperBizAgent-java 是一个基于 **Spring AI + Alibaba DashScope (Qwen)** 的智能运维 AI Agent 平台。它将大语言模型、向量检索增强生成(RAG)和多智能体协作(Planner-Executor-Replanner)整合为一体,面向企业 IT 运维场景提供:
|
||||
|
||||
- **智能文档问答**:上传运维知识库文档(Markdown/TXT),通过 RAG 管道实现向量化检索 + LLM 流式生成回答
|
||||
- **告警分析自动化**:多 Agent 协作分析 Prometheus 告警,结合日志查询(腾讯云 CLS)和内部知识库,生成结构化的告警分析报告
|
||||
- **MCP 协议集成**:通过 Spring AI MCP Client 连接外部工具服务,扩展 Agent 能力边界
|
||||
|
||||
### 为什么值得研究
|
||||
|
||||
| 维度 | 价值 |
|
||||
|------|------|
|
||||
| **AI 框架落地** | Spring AI Alibaba 生态的完整实践——ReactAgent、SupervisorAgent、Tool 注册、流式对话 |
|
||||
| **多 Agent 协作** | 非玩具级的 Planner-Executor-Replanner 监督循环,实际解决告警分析这种开放性问题 |
|
||||
| **RAG 工程化** | 完整的文档分块→向量化→Milvus 存储→语义检索→流式生成的端到端管道 |
|
||||
| **MCP 协议** | 业界较早将 MCP (Model Context Protocol) 用于生产场景的 Java 案例 |
|
||||
|
||||
### 适合谁
|
||||
|
||||
- Spring Boot / Java 开发者学习 AI Agent 框架的落地模式
|
||||
- AIOps / SRE 工程师了解智能运维 Agent 的架构设计
|
||||
- 对 Spring AI Alibaba 生态感兴趣的技术决策者
|
||||
|
||||
### 项目规模
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| Java 源文件 | ~25 个 |
|
||||
| 代码行数 | ~2500 行 |
|
||||
| API 端点 | 7 个 |
|
||||
| Agent 工具 | 4 个 |
|
||||
| 知识库文档 | 5 篇 |
|
||||
|
||||
### 技术栈
|
||||
|
||||
```
|
||||
应用层 Spring Boot 3.2 / Java 17
|
||||
AI 层 Spring AI Alibaba 1.1.0 / Qwen3-Max / text-embedding-v4
|
||||
存储层 Milvus 2.5 (向量库) / MinIO (对象存储)
|
||||
集成层 MCP Client (WebFlux SSE) / Prometheus
|
||||
部署 Docker Compose (Milvus + etcd + MinIO + Attu)
|
||||
```
|
||||
|
||||
### 与替代方案的对比
|
||||
|
||||
| 方案 | 优势 | 劣势 |
|
||||
|------|------|------|
|
||||
| 本项目 (Spring AI Alibaba) | 完整生态、国产模型、Java 原生 | 社区相对年轻 |
|
||||
| LangChain4j | 社区活跃、模型支持广 | 多 Agent 模式需自行构建 |
|
||||
| Python LangChain | 生态最丰富 | 非 Java 技术栈 |
|
||||
| 纯 DashScope API | 简单直接 | 缺乏 Agent 编排、工具调用框架 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Flow
|
||||
|
||||
### 架构总览
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 前端
|
||||
WEB[Web UI<br/>index.html + app.js]
|
||||
end
|
||||
|
||||
subgraph 控制层
|
||||
CC[ChatController<br/>/api/chat /api/chat_stream]
|
||||
AO[AIOpsController<br/>/api/ai_ops]
|
||||
UP[FileUploadController<br/>/api/upload]
|
||||
HC[MilvusCheckController<br/>/milvus/health]
|
||||
end
|
||||
|
||||
subgraph 服务层
|
||||
CS[ChatService<br/>ReactAgent 编排]
|
||||
AIS[AiOpsService<br/>多Agent 协作]
|
||||
RS[RagService<br/>RAG 流式问答]
|
||||
VIS[VectorIndexService<br/>文件索引管道]
|
||||
VSS[VectorSearchService<br/>向量相似搜索]
|
||||
VES[VectorEmbeddingService<br/>文本向量化]
|
||||
DCS[DocumentChunkService<br/>智能文档分块]
|
||||
end
|
||||
|
||||
subgraph Agent工具
|
||||
DT[DateTimeTools]
|
||||
IDT[InternalDocsTools]
|
||||
QMT[QueryMetricsTools]
|
||||
QLT[QueryLogsTools]
|
||||
end
|
||||
|
||||
subgraph 外部服务
|
||||
DS[DashScope API<br/>Qwen3-Max / Embedding]
|
||||
MV[Milvus<br/>向量数据库]
|
||||
PM[Prometheus<br/>监控告警]
|
||||
CLS[腾讯云CLS<br/>MCP SSE]
|
||||
end
|
||||
|
||||
WEB --> CC
|
||||
WEB --> AO
|
||||
WEB --> UP
|
||||
WEB --> HC
|
||||
|
||||
CC --> CS
|
||||
CC --> RS
|
||||
AO --> AIS
|
||||
UP --> VIS
|
||||
|
||||
CS --> DT & IDT & QMT & QLT
|
||||
AIS --> DT & IDT & QMT & QLT
|
||||
|
||||
CS --> DS
|
||||
RS --> DS
|
||||
RS --> VSS
|
||||
VIS --> DCS --> VES --> MV
|
||||
VSS --> MV
|
||||
QMT --> PM
|
||||
QLT --> CLS
|
||||
|
||||
VES --> DS
|
||||
```
|
||||
|
||||
### 主要运行时流程
|
||||
|
||||
#### 流程 A:RAG 智能问答(文档→检索→生成)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Ctrl as FileUploadController
|
||||
participant VIS as VectorIndexService
|
||||
participant DCS as DocumentChunkService
|
||||
participant VES as VectorEmbeddingService
|
||||
participant MV as Milvus
|
||||
participant RS as RagService
|
||||
participant DS as DashScope
|
||||
|
||||
Note over User,DS: === 索引阶段 ===
|
||||
User->>Ctrl: POST /api/upload (file.md)
|
||||
Ctrl->>VIS: indexSingleFile(file)
|
||||
VIS->>VIS: 删除旧向量(按source路径匹配)
|
||||
VIS->>DCS: chunkDocument(content)
|
||||
DCS-->>VIS: List<DocumentChunk>
|
||||
loop 每个分块
|
||||
VIS->>VES: generateEmbedding(chunk)
|
||||
VES->>DS: text-embedding-v4 API
|
||||
DS-->>VES: float[1024]
|
||||
VES-->>VIS: 向量
|
||||
end
|
||||
VIS->>MV: insert(向量 + 原文 + metadata)
|
||||
MV-->>VIS: OK
|
||||
|
||||
Note over User,DS: === 问答阶段 ===
|
||||
User->>Ctrl: POST /api/chat (question)
|
||||
Ctrl->>RS: generateAnswerStream(question)
|
||||
RS->>VES: 向量化问题
|
||||
VES->>DS: text-embedding-v4
|
||||
DS-->>RS: query_vector[1024]
|
||||
RS->>MV: search(query_vector, topK=3)
|
||||
MV-->>RS: 3条最相似文档片段
|
||||
RS->>DS: Generation API (提示词 + 上下文 + 问题)
|
||||
DS-->>User: SSE 流式生成回答
|
||||
```
|
||||
|
||||
#### 流程 B:AIOps 多 Agent 告警分析
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Ctrl as ChatController
|
||||
participant AIS as AiOpsService
|
||||
participant Sup as SupervisorAgent
|
||||
participant P as PlannerAgent
|
||||
participant E as ExecutorAgent
|
||||
participant Tools as Agent Tools
|
||||
participant DS as DashScope
|
||||
|
||||
User->>Ctrl: POST /api/ai_ops (告警信息)
|
||||
Ctrl->>AIS: executeAiOpsAnalysis(request)
|
||||
|
||||
Note over AIS, DS: 启动监督循环
|
||||
AIS->>Sup: 启动,传入 Planner + Executor
|
||||
|
||||
loop Planner-Executor-Replanner
|
||||
Sup->>P: 分析当前状态,决定下一步
|
||||
alt 需要制定/修订计划
|
||||
P-->>User: SSE: 📋 分析计划...
|
||||
else 需要执行步骤
|
||||
P-->>Sup: EXECUTE
|
||||
Sup->>E: 执行计划第一步
|
||||
E->>Tools: 调用工具收集证据
|
||||
Tools-->>E: 日志/告警/文档信息
|
||||
E-->>User: SSE: 🔍 执行结果...
|
||||
E-->>Sup: 反馈 + 证据
|
||||
Note over Sup: 将执行结果反馈给Planner
|
||||
else 分析完成
|
||||
P-->>Sup: FINISH
|
||||
end
|
||||
end
|
||||
|
||||
Sup-->>User: SSE: ✅ Markdown 告警分析报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Start Path
|
||||
|
||||
### 最小启动步骤
|
||||
|
||||
```bash
|
||||
# 1. 启动基础设施(Milvus + etcd + MinIO)
|
||||
cd D:\zhu\project\SuperBizAgent-java
|
||||
docker compose -f vector-database.yml up -d
|
||||
|
||||
# 2. 设置 API Key 环境变量
|
||||
export DASHSCOPE_API_KEY="your-dashscope-api-key"
|
||||
|
||||
# 3. 启动应用
|
||||
mvn spring-boot:run
|
||||
# 应用启动在 http://localhost:9900
|
||||
|
||||
# 4. 打开 Web 测试页面
|
||||
# http://localhost:9900/index.html
|
||||
```
|
||||
|
||||
### 学习起点
|
||||
|
||||
1. **第一入口**:`src/main/java/org/example/Main.java` — Spring Boot 启动类,了解组件扫描范围
|
||||
2. **核心对话**:`src/main/java/org/example/controller/ChatController.java` — 所有 API 端点定义,理解请求路由
|
||||
3. **Agent 编排**:`src/main/java/org/example/service/ChatService.java` — ReactAgent 如何注册工具、处理对话
|
||||
4. **多 Agent 协作**:`src/main/java/org/example/service/AiOpsService.java` — Planner-Executor-Replanner 模式完整实现
|
||||
5. **RAG 管道**:按 `VectorIndexService → DocumentChunkService → VectorEmbeddingService → RagService` 顺序阅读
|
||||
|
||||
### 建议的第一个修改
|
||||
|
||||
在 `QueryMetricsTools.java` 的 `queryPrometheusAlerts()` 方法中添加一个 Mock 数据,观察 Agent 如何将新的工具输出整合到对话中。修改后重新提问相关问题即可看到效果。
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Core Designs
|
||||
|
||||
### 设计一:Planner-Executor-Replanner 监督循环
|
||||
|
||||
**位置**:`src/main/java/org/example/service/AiOpsService.java`
|
||||
|
||||
**是什么**:一个三层多 Agent 协作模式,用监督者控制循环来解决开放性的告警分析问题。
|
||||
|
||||
```
|
||||
SupervisorAgent (监督者)
|
||||
├── PlannerAgent (规划者)
|
||||
│ └── 决策三个状态: PLAN → 制定/修订计划
|
||||
│ EXECUTE → 交给执行者
|
||||
│ FINISH → 输出最终报告
|
||||
└── ExecutorAgent (执行者)
|
||||
└── 执行计划中的第一步
|
||||
└── 调用工具获取真实数据
|
||||
└── 返回反馈给 Planner 重新规划
|
||||
```
|
||||
|
||||
**为什么重要**:
|
||||
- 不是简单的单次 Agent 调用,而是通过**循环反馈**逐步逼近准确分析
|
||||
- Planner 根据 Executor 返回的证据**动态调整计划**(即 Replan 机制)
|
||||
- 通过 SSE 将每一步的中间结果实时推送给前端,用户体验好
|
||||
- 工具调用是**实际的**:Prometheus 查询、日志搜索、知识库检索,不是 mock 玩具
|
||||
|
||||
**关键实现细节**:
|
||||
```java
|
||||
// SupervisorAgent 创建并传入子 Agent
|
||||
SupervisorAgent supervisor = SupervisorAgent.builder()
|
||||
.supervisorAgent(supervisor)
|
||||
.subAgents(plannerAgent, executorAgent)
|
||||
.build();
|
||||
```
|
||||
|
||||
### 设计二:完整的 RAG 管道(5 级流水线)
|
||||
|
||||
**位置**:`VectorIndexService` → `DocumentChunkService` → `VectorEmbeddingService` → `VectorSearchService` → `RagService`
|
||||
|
||||
**是什么**:从原始文档到流式问答输出的完整 RAG 管道,涉及 5 个松耦合的服务组件。
|
||||
|
||||
| 阶段 | 组件 | 关键技术点 |
|
||||
|------|------|------------|
|
||||
| 1. 智能分块 | DocumentChunkService | 按 Markdown 标题层级 + 段落边界分割,800 字符/块,100 字符重叠 |
|
||||
| 2. 向量化 | VectorEmbeddingService | DashScope text-embedding-v4,1024 维,支持批量 |
|
||||
| 3. 向量存储 | MilvusClientFactory | IVF_FLAT 索引,L2 距离,自动去重(按 source 路径) |
|
||||
| 4. 语义检索 | VectorSearchService | Top-K 配置化(default 3),返回原文 + 相似度分数 |
|
||||
| 5. 流式生成 | RagService | DashScope Generation API,SSE 流式输出,支持 system prompt |
|
||||
|
||||
**为什么重要**:
|
||||
- 每个阶段**独立可替换**——可以换分块策略、换向量库、换 LLM
|
||||
- **幂等上传**:同一文件重新上传时,先删除旧向量再写入,保证数据一致性
|
||||
- 分块策略考虑了 Markdown 的文档结构(标题层级),而不是简单的固定长度切割
|
||||
|
||||
### 设计三:工具即插即用的 Agent 工具系统
|
||||
|
||||
**位置**:`src/main/java/org/example/agent/tool/*.java`
|
||||
|
||||
**是什么**:基于 Spring AI `@Tool` 注解的工具系统,Agent 自动发现并可调用。
|
||||
|
||||
```java
|
||||
// 工具定义示例
|
||||
@Component
|
||||
public class DateTimeTools {
|
||||
@Tool(description = "获取当前日期和时间")
|
||||
public String getCurrentDateTime() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**核心设计决策**:
|
||||
|
||||
| 决策 | 做法 | 原因 |
|
||||
|------|------|------|
|
||||
| Mock 开关 | `QueryMetricsTools` 和 `QueryLogsTools` 都有 `mockEnabled` 配置 | 开发/演示时不需要真实 Prometheus/CLS 环境 |
|
||||
| MCP 优先 | 当 MCP Client 可用时,自动排除 `QueryLogsTools` | 避免工具重复,MCP 提供更丰富的日志能力 |
|
||||
| JSON Schema 生成 | 使用 `jsonschema-generator` 为工具参数生成 schema | 让 LLM 理解工具的参数类型和约束 |
|
||||
| 工具注册 | `ChatService` 和 `AiOpsService` 各自注册工具集 | Agent 只获得需要的能力,避免干扰 |
|
||||
|
||||
**ChatService 工具注册**:
|
||||
```java
|
||||
// 构建时注册所有可用工具
|
||||
ReactAgent agent = ReactAgent.builder()
|
||||
.tools(dateTimeTools, internalDocsTools,
|
||||
queryMetricsTools, queryLogsTools)
|
||||
.build();
|
||||
```
|
||||
|
||||
**为什么重要**:
|
||||
- Agent 工具系统是 AI Agent 的**能力边界**——定义了 Agent 能做什么
|
||||
- Mock/Real 模式切换体现了**开发友好性**
|
||||
- MCP 协议的集成展示了**可扩展性**——Agent 可以从外部获取新能力
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
SuperBizAgent-java 是一个小而完整的 AI Agent 实践项目。它的三个核心竞争力是:
|
||||
|
||||
1. **多 Agent 协作**(Planner-Executor-Replanner)——不是玩具,是真正解决问题的模式
|
||||
2. **工程化的 RAG 管道**——5 级流水线、幂等上传、智能分块
|
||||
3. **Spring AI 生态的完整实践**——从 @Tool 注解到 MCP 协议,展示了 Java 生态做 AI Agent 的成熟路径
|
||||
|
||||
对于想将 AI Agent 引入企业运维场景的 Java 团队,这是一个很好的学习起点和脚手架。
|
||||
@@ -1,529 +0,0 @@
|
||||
# Agent 架构设计(MVP 版)
|
||||
|
||||
## 一、MVP 全景
|
||||
|
||||
```
|
||||
用户输入
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 意图识别(Intent Recognition) 🆕 │
|
||||
│ "用户想干什么?" │
|
||||
│ │
|
||||
│ 诊断意图 → 路由到诊断 Skill │
|
||||
│ 文档意图 → 路由到文档问答 │
|
||||
│ 案例意图 → 路由到案例查询 │
|
||||
│ 闲聊 → 快速响应(不启动 Agent) │
|
||||
│ 模糊/无关 → 提示用户,直接中断 │
|
||||
└──────────────┬───────────────────────────┘
|
||||
│ 诊断意图
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Supervisor Agent(调度者) │
|
||||
│ "谁来干?什么时候停?" │
|
||||
└──────────────┬───────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Planner Agent(规划者) │
|
||||
│ 分析问题 → 制定策略 → 生成报告 │
|
||||
└──────────────┬───────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Executor Agent(执行者) │
|
||||
│ 调用工具收集证据 │
|
||||
└──────────────┬───────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Verifier Agent(验证者) │
|
||||
│ 事实核查 → 判定通过/修正/驳回 │
|
||||
└──────────────┬───────────────────────────┘
|
||||
↓
|
||||
诊断报告输出
|
||||
↓
|
||||
用户反馈(有用/无用)
|
||||
↓
|
||||
案例沉淀 + BadCase 优化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、意图识别(入口层)🆕
|
||||
|
||||
### 2.1 设计理念
|
||||
|
||||
```
|
||||
定位:独立模块,不嵌入任何单一 Agent
|
||||
|
||||
分层策略(不是二选一,而是组合):
|
||||
|
||||
L0: 正则规则 —— 0 成本,毫秒级 ✅ MVP
|
||||
├─ 处理 80%+ 的结构化查询
|
||||
├─ 正则匹配订单号/traceId/错误码格式
|
||||
└─ 关键词匹配("报错"/"异常"/"失败")
|
||||
|
||||
L1: 小模型 Agent —— 低成本,百毫秒级 ✅ MVP
|
||||
├─ L0 未命中时触发
|
||||
├─ 处理灵活的模糊表达("系统有点慢"、"怎么查不到了")
|
||||
├─ 不启动全链路 Agent,只做意图分类
|
||||
└─ 判断为诊断意图 → 路由到诊断 Skill
|
||||
|
||||
L2: 兜底策略 —— 极少使用
|
||||
├─ L0+L1 都无法判断 → 意图不明 → 中断
|
||||
└─ Phase 2 增强
|
||||
```
|
||||
|
||||
### 2.2 意图分类与路由
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ 意图 │ 说明 │ 路由 │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ 诊断意图 │ 包含结构化标识或错误描述 │ → 诊断Skill│
|
||||
│ 文档问答 │ "XX接口的参数有哪些" │ → 直接RAG │
|
||||
│ 案例查询 │ "之前有类似的问题吗" │ → 案例检索 │
|
||||
│ 闲聊 │ "你好"/"谢谢" │ → 快速响应 │
|
||||
│ 意图不明 │ 无法识别 │ → 中断+提示 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
|
||||
关键原则:
|
||||
- 只有诊断意图才启动 Agent 全链路
|
||||
- 非诊断意图走轻量路径或直接中断
|
||||
```
|
||||
|
||||
### 2.3 L0:正则规则(MVP,处理 80%)
|
||||
|
||||
```
|
||||
为什么先做 L0?
|
||||
→ 0 成本(不调 LLM),毫秒级响应
|
||||
→ 结构化查询占比最大(订单号、traceId、错误码、关键词)
|
||||
→ L0 命中直接路由,不需要走后续逻辑
|
||||
|
||||
规则配置(可扩展):
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ 规则 │ 意图 │ 方式 │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ 匹配 \d{12,} │ 诊断 │ 正则 │
|
||||
│ 匹配 trace[-_]?\w{8,} │ 诊断 │ 正则 │
|
||||
│ 包含"报错‖失败‖异常‖挂了‖超时" │ 诊断 │ 关键词│
|
||||
│ 包含"文档‖接口‖参数‖字段‖API" │ 文档 │ 关键词│
|
||||
│ 包含"案例‖之前‖类似‖历史" │ 案例 │ 关键词│
|
||||
│ 长度 <= 5 字符 │ 闲聊 │ 规则 │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
命中 → 直接路由,不调 L1
|
||||
未命中 → 进入 L1
|
||||
```
|
||||
|
||||
### 2.4 L1:小模型 Agent(MVP,处理剩余 20%)
|
||||
|
||||
```
|
||||
为什么用小模型 Agent 而非嵌入到 Supervisor?
|
||||
→ 意图识别是独立职责,不应耦合到任何业务 Agent
|
||||
→ 轻量 Agent:单一职责,只分类不执行
|
||||
→ 成本低(~50 token),延迟低(~200ms)
|
||||
|
||||
何时触发:L0 规则未命中
|
||||
|
||||
System Prompt:
|
||||
"你是意图分类器,判断用户想做什么。
|
||||
只返回一个词:[诊断 / 文档查询 / 案例查询 / 闲聊 / 意图不明]
|
||||
|
||||
诊断:用户描述了故障、报错、异常
|
||||
文档查询:用户询问接口文档、字段含义
|
||||
案例查询:用户询问历史案例、类似问题
|
||||
闲聊:简单的问候、感谢
|
||||
意图不明:无法判断用户意图"
|
||||
|
||||
输入:用户原始输入
|
||||
输出:意图类型 + 置信度
|
||||
```
|
||||
|
||||
### 2.5 L2:兜底策略
|
||||
|
||||
```
|
||||
L0+L1 都无法判断 → L2 兜底
|
||||
|
||||
中断规则:
|
||||
├─ 意图不明 → 提示用户 + 中断
|
||||
│ "无法判断您的意图,请提供订单号或错误码"
|
||||
├─ 闲聊 → 快速响应 + 中断
|
||||
│ "我是故障诊断助手,请描述您遇到的问题"
|
||||
└─ 不启动 Agent,直接返回
|
||||
|
||||
路由规则:
|
||||
├─ 诊断意图 → 启动 Supervisor + 4 Agent 全链路
|
||||
├─ 文档意图 → 不启动 Agent,直接 RAG 检索
|
||||
└─ 案例意图 → 不启动 Agent,直接查询 case_library
|
||||
```
|
||||
|
||||
### 2.5 架构位置
|
||||
|
||||
```
|
||||
用户输入
|
||||
↓
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 意图识别模块(独立) │
|
||||
│ │
|
||||
│ L1: 小模型 Agent ─→ 诊断意图? │
|
||||
│ │ 文档意图? │
|
||||
│ │ 案例意图? │
|
||||
│ │ 闲聊? │
|
||||
│ ↓ │
|
||||
│ L2: 兜底 ─────────→ 意图不明 → 中断 │
|
||||
│ 闲聊 → 快速响应 │
|
||||
└───────────────┬──────────────────────────┘
|
||||
│ 诊断意图
|
||||
↓
|
||||
Supervisor → Planner → Executor → Verifier
|
||||
```
|
||||
|
||||
### 2.6 MVP vs Phase 2
|
||||
|
||||
```
|
||||
MVP L0(正则)+ L1(小模型Agent) 覆盖 95%+ 场景
|
||||
Phase 2 L2(兜底增强) 细化中断提示,支持多轮澄清
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、4 个 Agent 设计
|
||||
|
||||
### 2.1 Supervisor Agent(调度者)
|
||||
|
||||
**职责**:总指挥,协调工作流
|
||||
|
||||
```
|
||||
调度规则:
|
||||
├─ 接收任务 → 发给 Planner 分析
|
||||
├─ Planner 完成 → 发给 Executor 执行
|
||||
├─ Executor 完成 → 发给 Verifier 校验
|
||||
└─ Verifier PASS → 输出报告 / REJECT → 返回 Planner 重新规划
|
||||
|
||||
不做:
|
||||
- 不直接调用工具
|
||||
- 不直接生成报告
|
||||
```
|
||||
|
||||
### 2.2 Planner Agent(规划者 + 分诊)
|
||||
|
||||
**职责**:分析问题、制定策略、生成报告草稿
|
||||
|
||||
```
|
||||
分析规则:
|
||||
├─ 有 errorCode + 接口 URL → EXTERNAL_API(外部接口故障)
|
||||
├─ 有堆栈信息 → INTERNAL_ERROR(系统内部错误)
|
||||
├─ 有数据库错误码(如 1213)→ DATABASE(数据库问题)
|
||||
└─ 其他 → 通用排查
|
||||
|
||||
规划流程:
|
||||
1. 确定 fault_category
|
||||
2. 制定排查步骤(每步:工具名 + 参数 + 预期)
|
||||
3. 生成决策:EXECUTE(继续执行)| FINISH(生成报告)
|
||||
|
||||
Replanner 职责:
|
||||
├─ Executor 每次返回结果后 → 评估证据是否充分
|
||||
├─ 需要补充?→ 调整步骤,继续执行
|
||||
├─ 证据齐全?→ FINISH,生成报告草稿
|
||||
└─ 连续 3 次失败?→ 降级
|
||||
|
||||
禁止:
|
||||
- 编造数据
|
||||
- 引用未经工具返回的内容
|
||||
```
|
||||
|
||||
### 2.3 Executor Agent(执行者)
|
||||
|
||||
**职责**:调用工具收集证据
|
||||
|
||||
```
|
||||
工具清单:
|
||||
├─ queryOrder:查询订单/业务数据(MySQL 只读)
|
||||
├─ searchDoc:检索接口文档(混合检索 Milvus + MySQL)
|
||||
├─ recommendCase:推荐相似案例(精确匹配 + 语义检索)
|
||||
└─ getCurrentTime:获取当前时间
|
||||
|
||||
执行规则:
|
||||
├─ 每次只执行 Planner 指定的一个步骤
|
||||
├─ 返回结构化的执行结果
|
||||
├─ 失败时返回错误详情(便于 Planner 调整)
|
||||
└─ 禁止编造结果
|
||||
|
||||
扩展预留:
|
||||
// 代码中 Executor 是接口,后续可扩展为 SubAgent
|
||||
public interface Executor {
|
||||
ExecutionResult execute(Step step);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Verifier Agent(验证者)
|
||||
|
||||
**职责**:验证诊断报告,防止编造
|
||||
|
||||
```
|
||||
验证流程:
|
||||
|
||||
1️⃣ 事实核查(最重要)
|
||||
├─ 报告中的错误码 → 在 tool_calls 中存在?
|
||||
├─ 根因结论 → 有日志/文档证据支撑?
|
||||
├─ 修复方案 → 引用了文档或案例?
|
||||
└─ 发现编造数据 → 直接 REJECT
|
||||
|
||||
2️⃣ 完整性检查
|
||||
├─ 根因分析章节不能为空
|
||||
├─ 证据链章节不能为空
|
||||
└─ 修复方案章节不能为空
|
||||
|
||||
判决结果:
|
||||
├─ PASS:报告成立,直接输出
|
||||
├─ REVISE:小问题可修正,返回 Planner 微调
|
||||
└─ REJECT:编造数据或严重错误,返回 Planner 重新分析
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、RAG 两层加载策略
|
||||
|
||||
### 4.1 设计理念
|
||||
|
||||
```
|
||||
问题:
|
||||
❌ 全前置:启动时把所有文档塞给 Agent → 信息过载,推理变慢
|
||||
❌ 纯被动:等到需要才查 → Planner 没有全局视野,可能跑偏
|
||||
❌ 固定步骤:每次都调 → 内部错误查接口文档浪费
|
||||
|
||||
正确做法:两层互补
|
||||
|
||||
L1 预加载(Planner 启动时)
|
||||
→ 通用领域知识:系统架构、通用错误码、业务流程
|
||||
→ 给 Planner 全局视野,避免方向性错误
|
||||
|
||||
L2 按需加载(Executor 执行中)
|
||||
→ 具体接口文档:字段定义、错误码含义、调用规范
|
||||
→ 给 Executor 精准证据,定位具体问题
|
||||
```
|
||||
|
||||
### 4.2 两层对比
|
||||
|
||||
| | L1 预加载 | L2 按需加载 |
|
||||
|------|---------|-----------|
|
||||
| 触发时机 | 意图识别后,Planner 启动前 | Executor 拿到具体信息后 |
|
||||
| 内容 | 通用知识(架构、流程、高频错误码) | 具体接口文档(字段、错误码含义) |
|
||||
| 目的 | 让 Planner 有全局视野 | 让 Executor 有精准证据 |
|
||||
| 成本 | 固定,每次诊断 1 次 | 按需,最多 2-3 次 |
|
||||
| 谁负责 | Supervisor 注入 | Executor 自主调用 |
|
||||
|
||||
### 4.3 实现方式
|
||||
|
||||
```
|
||||
L1 预加载:
|
||||
Supervisor 在启动 Planner 前:
|
||||
searchDoc(keyword="系统架构 通用错误码 业务流程")
|
||||
→ 注入到 Planner 的 System Prompt 中
|
||||
→ Planner 拥有"领域背景知识"
|
||||
|
||||
L2 按需加载:
|
||||
Executor 执行 Step 2 时:
|
||||
拿到 errorCode=40003, faultSource="广东"
|
||||
→ 自主调用 searchDoc(errorCode="40003", faultSource="广东")
|
||||
→ 获取该接口的具体字段定义和错误码说明
|
||||
→ 作为证据写入诊断报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、Skill 设计(1 个)
|
||||
|
||||
### /diagnose-by-orderid(按订单号诊断)
|
||||
|
||||
```
|
||||
输入:orderId
|
||||
|
||||
工作流(6 步):
|
||||
|
||||
Step 1: 查询订单信息
|
||||
工具:queryOrder
|
||||
失败:ABORT(订单不存在则终止)
|
||||
|
||||
Step 2: 检索接口文档
|
||||
工具:searchDoc
|
||||
参数:errorCode + faultSource
|
||||
失败:SKIP(标注"文档缺失")
|
||||
|
||||
Step 3: 查询日志
|
||||
工具:queryLogs(Mock)
|
||||
参数:traceId
|
||||
失败:SKIP(标注"日志缺失")
|
||||
|
||||
Step 4: 检索相似案例
|
||||
工具:recommendCase
|
||||
参数:errorCode + faultCategory
|
||||
失败:SKIP(标注"无相似案例")
|
||||
|
||||
Step 5: 生成诊断报告
|
||||
汇总所有证据,按模板生成报告
|
||||
|
||||
Step 6: Verifier 验证
|
||||
事实核查 → 判决
|
||||
|
||||
门禁规则:
|
||||
├─ Step 1 失败 → 终止,返回"订单不存在"
|
||||
├─ Step 2-4 失败 → 跳过,标注缺失信息
|
||||
├─ 任意步骤超时 30s → 终止
|
||||
└─ Verifier REJECT → 返回 Planner 重新规划
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Harness 控制层(精简版)
|
||||
|
||||
### 4.1 5 个 Quality Gates
|
||||
|
||||
```
|
||||
输入门禁(2 个):
|
||||
├─ Gate 1: 输入参数非空校验
|
||||
└─ Gate 2: 5 分钟内同一订单 → 返回缓存
|
||||
|
||||
执行门禁(1 个):
|
||||
└─ Gate 3: 工具调用超时(10 秒)
|
||||
|
||||
输出门禁(2 个):
|
||||
├─ Gate 4: 报告章节完整性(3 章节不全 → 不通过)
|
||||
└─ Gate 5: 置信度阈值(< 60 → 标记"低置信度")
|
||||
```
|
||||
|
||||
### 4.2 中断规则
|
||||
|
||||
```
|
||||
自动中断:
|
||||
├─ 工具连续失败 3 次 → 终止,降级输出
|
||||
└─ 全局超时 30 秒 → 终止
|
||||
|
||||
条件降级:
|
||||
├─ 文档检索为空 → 跳过继续
|
||||
├─ 案例推荐为空 → 跳过继续
|
||||
└─ 日志查询失败 → 跳过继续
|
||||
|
||||
降级输出:
|
||||
"无法自动诊断,请人工介入"
|
||||
+ 已收集的证据(订单信息 + 部分日志 + 已知错误码)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、技术实现
|
||||
|
||||
### 5.1 基于 Spring AI Alibaba
|
||||
|
||||
```java
|
||||
// Supervisor - 框架提供
|
||||
SupervisorAgent supervisor = SupervisorAgent.builder()
|
||||
.name("diagnosis_supervisor")
|
||||
.model(chatModel)
|
||||
.subAgents(List.of(planner, executor, verifier))
|
||||
.build();
|
||||
|
||||
// Planner
|
||||
ReactAgent planner = ReactAgent.builder()
|
||||
.name("planner_agent")
|
||||
.model(chatModel)
|
||||
.systemPrompt(plannerPrompt)
|
||||
.outputKey("planner_plan")
|
||||
.build();
|
||||
|
||||
// Executor(代码中预留 SubAgent 扩展接口)
|
||||
ReactAgent executor = ReactAgent.builder()
|
||||
.name("executor_agent")
|
||||
.model(chatModel)
|
||||
.systemPrompt(executorPrompt)
|
||||
.methodTools(diagnosisTools)
|
||||
.tools(new ToolCallback[]{queryOrder, searchDoc, recommendCase, getCurrentTime})
|
||||
.build();
|
||||
|
||||
// Verifier
|
||||
ReactAgent verifier = ReactAgent.builder()
|
||||
.name("verifier_agent")
|
||||
.model(chatModel)
|
||||
.systemPrompt(verifierPrompt)
|
||||
.outputKey("verifier_result")
|
||||
.build();
|
||||
```
|
||||
|
||||
### 5.2 工具注册
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class DiagnosisTools {
|
||||
|
||||
@Tool(description = "查询订单/业务数据(只读)")
|
||||
public OrderInfo queryOrder(@ToolParam(description = "订单号") String orderId) {
|
||||
// MySQL 只读 + SQL 注入防护
|
||||
}
|
||||
|
||||
@Tool(description = "检索接口文档")
|
||||
public List<DocChunk> searchDoc(
|
||||
@ToolParam(description = "错误码") String errorCode,
|
||||
@ToolParam(description = "省份/服务名") String faultSource
|
||||
) {
|
||||
// 混合检索:精确匹配 + 向量检索
|
||||
}
|
||||
|
||||
@Tool(description = "推荐相似历史案例")
|
||||
public List<CaseResult> recommendCase(
|
||||
@ToolParam(description = "错误码") String errorCode,
|
||||
@ToolParam(description = "故障类别") String faultCategory
|
||||
) {
|
||||
// 精确匹配 MySQL + 语义检索 Milvus
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、闭环机制
|
||||
|
||||
```
|
||||
诊断报告输出
|
||||
↓
|
||||
用户反馈(useful / not_useful)
|
||||
↓
|
||||
├─ useful → 自动生成 case_library
|
||||
└─ not_useful → 记录 BadCase
|
||||
↓
|
||||
每周 BadCase 分析
|
||||
↓
|
||||
Prompt / Skill 优化
|
||||
↓
|
||||
准确率验证(测试集重跑)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、MVP vs 扩展方向
|
||||
|
||||
| 维度 | MVP | 扩展方向 |
|
||||
|------|-----|---------|
|
||||
| Agent | 4 个 Agent | SubAgent 模式(专科医生) |
|
||||
| Skill | 1 个 | 渐进式披露(3 层知识) |
|
||||
| 工具 | @Tool 注解 | MCP 独立 Server |
|
||||
| 回退 | 2 级(失败→降级) | 4 级路由 |
|
||||
| Gates | 5 个 | 15 个全流程门禁 |
|
||||
| 隔离 | 单 JVM | K8s Pod 进程隔离 |
|
||||
| 进化 | 案例自动生成 | 模式识别 + Prompt 自优化 |
|
||||
|
||||
---
|
||||
|
||||
## 十、面试话术(精简版)
|
||||
|
||||
> "我用 Spring AI Alibaba 实现了一个故障诊断 Agent 系统。
|
||||
>
|
||||
> 入口层是**意图识别**:先判断用户想干什么——诊断故障、查文档、查案例还是闲聊。
|
||||
> 非诊断意图直接走轻量路径,只有诊断意图才启动 Agent 全链路,节省资源。
|
||||
>
|
||||
> 4 Agent 协作:Supervisor 调度、Planner 制定策略、
|
||||
> Executor 调用工具收集证据、Verifier 验证报告防止编造。
|
||||
>
|
||||
> 诊断流程封装成了 Skill,标准化 6 个步骤和异常处理。
|
||||
> Harness 层 5 个门禁保证质量——最关键的是输出门禁,
|
||||
> Verifier 会对比报告数据和工具返回数据,发现编造就驳回。
|
||||
>
|
||||
> 闭环机制:用户反馈 → BadCase 分析 → Prompt 优化。
|
||||
> 案例自动沉淀,系统越用越智能。"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,715 +0,0 @@
|
||||
# SuperBizAgent MVP 完整实施计划(AI 执行)
|
||||
|
||||
## 协作分工
|
||||
|
||||
```
|
||||
用户角色:规划者 + 验证者 + 架构师
|
||||
AI 角色: 执行者 + 编码者 + 记录者
|
||||
|
||||
用户负责:
|
||||
├─ 确认架构设计
|
||||
├─ 验收每个阶段产出
|
||||
├─ 调整优先级和方向
|
||||
└─ 最终验收和部署决策
|
||||
|
||||
AI 负责:
|
||||
├─ 编写全部代码
|
||||
├─ 编写全部测试
|
||||
├─ 执行测试验证
|
||||
├─ 记录实施过程
|
||||
├─ 遇到问题提出方案供用户决策
|
||||
└─ 自动化构建和本地验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总览:3 个 Phase,13 天
|
||||
|
||||
```
|
||||
Phase 1: 基础设施(5天)
|
||||
├─ Day 1-2: 数据库 + 实体 + 会话管理
|
||||
├─ Day 3: 代码结构重构
|
||||
└─ Day 4-5: 文档管理(CRUD + Milvus)
|
||||
|
||||
Phase 2: 核心功能(5天)
|
||||
├─ Day 6-7: 意图识别 + RAG 两层加载
|
||||
├─ Day 8-9: 4 Agent 协作 + Skill
|
||||
└─ Day 10: 工具层开发
|
||||
|
||||
Phase 3: 闭环优化(3天)
|
||||
├─ Day 11: Verifier + Harness
|
||||
├─ Day 12: 反馈机制 + 案例沉淀
|
||||
└─ Day 13: 端到端测试 + 验收
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:基础设施(5天)
|
||||
|
||||
### Day 1-2:数据库 + 实体 + 会话
|
||||
|
||||
#### 任务 1.1:MySQL 表结构(Flyway 迁移)
|
||||
|
||||
```sql
|
||||
产出文件:
|
||||
src/main/resources/db/migration/
|
||||
├── V001__create_diagnosis_record.sql
|
||||
├── V002__create_case_library.sql
|
||||
└── V003__create_api_document.sql
|
||||
|
||||
依据文档:
|
||||
- docs/tables/diagnosis_record.md
|
||||
- docs/tables/case_library.md
|
||||
- docs/tables/api_document.md
|
||||
|
||||
关键点:
|
||||
- 使用 Flyway 版本管理
|
||||
- 索引:trace_id, error_code, fault_category
|
||||
- JSON 字段:steps_executed, evidence_chain
|
||||
- 时间字段:created_at, updated_at 自动维护
|
||||
|
||||
验收标准:
|
||||
✓ 执行 mvn flyway:migrate 成功
|
||||
✓ 3 张表创建成功
|
||||
✓ 索引完整
|
||||
✓ 约束正确
|
||||
```
|
||||
|
||||
#### 任务 1.2:JPA 实体类
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
src/main/java/com/superbiz/agent/domain/entity/
|
||||
├── DiagnosisRecord.java
|
||||
├── CaseLibrary.java
|
||||
└── ApiDocument.java
|
||||
|
||||
技术栈:
|
||||
- Spring Data JPA
|
||||
- Lombok (@Data, @Builder)
|
||||
- Hibernate @JdbcTypeCode(SqlTypes.JSON)
|
||||
|
||||
验收标准:
|
||||
✓ 字段与 DDL 一致
|
||||
✓ 枚举映射正确
|
||||
✓ JSON 字段序列化正常
|
||||
✓ 编译通过
|
||||
```
|
||||
|
||||
#### 任务 1.3:Repository 层
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
src/main/java/com/superbiz/agent/repository/
|
||||
├── DiagnosisRecordRepository.java
|
||||
├── CaseLibraryRepository.java
|
||||
└── ApiDocumentRepository.java
|
||||
|
||||
常用查询:
|
||||
- findByOrderId
|
||||
- findByTraceId
|
||||
- findByErrorCodeAndFaultCategory
|
||||
- findTopByOrderByCreatedAtDesc
|
||||
|
||||
验收标准:
|
||||
✓ 继承 JpaRepository
|
||||
✓ 单元测试覆盖(@DataJpaTest + H2)
|
||||
✓ 分页查询正确
|
||||
```
|
||||
|
||||
#### 任务 1.4:Redis 会话管理
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
src/main/java/com/superbiz/agent/session/
|
||||
├── SessionManager.java # 接口
|
||||
├── RedisSessionManager.java # Redis 实现
|
||||
├── SessionContext.java # 会话上下文
|
||||
└── SessionConfiguration.java # 配置类
|
||||
|
||||
功能:
|
||||
- 替换内存 HashMap
|
||||
- TTL:30 分钟
|
||||
- JSON 序列化(Jackson)
|
||||
- 按 sessionId 存取删
|
||||
|
||||
验收标准:
|
||||
✓ 单元测试通过
|
||||
✓ Redis 连接成功
|
||||
✓ 序列化/反序列化正确
|
||||
✓ TTL 生效
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 3:代码结构重构
|
||||
|
||||
#### 任务 3.1:包名重构
|
||||
|
||||
```
|
||||
重构前:org.example
|
||||
重构后:com.superbiz.agent
|
||||
|
||||
操作:
|
||||
1. IDEA Refactor → Rename Package
|
||||
2. 全局搜索替换 import
|
||||
3. pom.xml 更新 mainClass
|
||||
|
||||
验收标准:
|
||||
✓ 编译通过
|
||||
✓ 无遗漏的 org.example
|
||||
✓ 启动成功
|
||||
```
|
||||
|
||||
#### 任务 3.2:分层结构优化
|
||||
|
||||
```
|
||||
目标结构:
|
||||
src/main/java/com/superbiz/agent/
|
||||
├── controller/ # REST 接口
|
||||
├── service/ # 业务逻辑
|
||||
├── repository/ # 数据访问
|
||||
├── domain/
|
||||
│ ├── entity/ # JPA 实体
|
||||
│ ├── dto/ # 数据传输对象
|
||||
│ ├── vo/ # 视图对象
|
||||
│ └── enums/ # 枚举
|
||||
├── agent/ # Agent 层
|
||||
│ ├── supervisor/
|
||||
│ ├── planner/
|
||||
│ ├── executor/
|
||||
│ └── verifier/
|
||||
├── tool/ # 工具层
|
||||
├── harness/ # Harness 控制
|
||||
│ ├── gate/
|
||||
│ └── interrupt/
|
||||
├── skill/ # Skill 定义
|
||||
├── session/ # 会话管理
|
||||
├── intent/ # 意图识别
|
||||
├── rag/ # RAG 加载
|
||||
└── config/ # 配置
|
||||
|
||||
验收标准:
|
||||
✓ 目录结构清晰
|
||||
✓ 职责单一
|
||||
✓ 编译通过
|
||||
```
|
||||
|
||||
#### 任务 3.3:DTO 抽离
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
src/main/java/com/superbiz/agent/domain/dto/
|
||||
├── DiagnosisRequest.java
|
||||
├── DiagnosisResponse.java
|
||||
├── DocumentUploadRequest.java
|
||||
├── CaseQueryRequest.java
|
||||
└── ...
|
||||
|
||||
要求:
|
||||
- Controller 不直接依赖 Entity
|
||||
- MapStruct 做对象转换
|
||||
- 校验注解 @Valid + @NotNull
|
||||
- 统一响应包装类 Result<T>
|
||||
|
||||
验收标准:
|
||||
✓ Controller 不 import Entity
|
||||
✓ 原有接口兼容
|
||||
✓ 编译通过
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 4-5:文档管理
|
||||
|
||||
#### 任务 4.1:文档上传
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
controller/DocumentController.java
|
||||
service/DocumentService.java
|
||||
service/TextExtractor.java
|
||||
service/VectorService.java
|
||||
|
||||
接口:POST /api/documents/upload
|
||||
功能:
|
||||
1. 接收文件(Word/PDF/Markdown)
|
||||
2. 提取纯文本
|
||||
3. 分块(chunk_size=500, overlap=50)
|
||||
4. 向量化(DashScopeEmbedding)
|
||||
5. 写 MySQL + Milvus
|
||||
|
||||
验收标准:
|
||||
✓ 上传成功返回 document_id
|
||||
✓ MySQL 记录正确
|
||||
✓ Milvus 向量正确
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 4.2:文档查询
|
||||
|
||||
```java
|
||||
接口:
|
||||
- GET /api/documents/{id}
|
||||
- GET /api/documents?province=XX&category=YY
|
||||
|
||||
验收标准:
|
||||
✓ 分页查询
|
||||
✓ 过滤生效
|
||||
✓ 性能可接受(< 100ms)
|
||||
```
|
||||
|
||||
#### 任务 4.3:文档删除同步
|
||||
|
||||
```java
|
||||
接口:DELETE /api/documents/{id}
|
||||
|
||||
功能:
|
||||
- 删除 MySQL 记录
|
||||
- 同步删除 Milvus 向量
|
||||
- 事务一致性
|
||||
|
||||
验收标准:
|
||||
✓ MySQL + Milvus 同步删除
|
||||
✓ 事务回滚正确
|
||||
```
|
||||
|
||||
#### 任务 4.4:混合检索实现
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
tool/DocumentSearchTool.java
|
||||
|
||||
策略:
|
||||
1. 精确匹配(MySQL)
|
||||
2. 语义检索(Milvus)
|
||||
3. RRF 融合排序
|
||||
|
||||
验收标准:
|
||||
✓ 精确匹配优先
|
||||
✓ 语义检索补漏
|
||||
✓ 返回 Top 3
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:核心功能(5天)
|
||||
|
||||
### Day 6-7:意图识别 + RAG
|
||||
|
||||
#### 任务 6.1:意图识别模块
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
intent/IntentClassifier.java
|
||||
intent/L0RulesMatcher.java
|
||||
intent/L1AgentClassifier.java
|
||||
intent/IntentResult.java
|
||||
|
||||
L0 规则匹配:
|
||||
- 正则:订单号、traceId、错误码
|
||||
- 关键词:报错、异常、失败
|
||||
- 返回:诊断/文档/案例/闲聊
|
||||
|
||||
L1 小模型 Agent:
|
||||
- 输入:用户原始输入
|
||||
- Prompt:分类意图
|
||||
- 输出:意图 + 置信度
|
||||
|
||||
验收标准:
|
||||
✓ L0 命中率 80%+
|
||||
✓ L1 准确率 90%+
|
||||
✓ 延迟 < 200ms
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 6.2:RAG 两层加载
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
rag/RagLoader.java
|
||||
rag/L1PreloadService.java
|
||||
rag/L2OnDemandService.java
|
||||
|
||||
L1 预加载:
|
||||
- 触发时机:意图识别后,Planner 启动前
|
||||
- 内容:通用领域知识(架构、流程、高频错误码)
|
||||
- 注入:Planner System Prompt
|
||||
|
||||
L2 按需加载:
|
||||
- 触发时机:Executor 拿到 errorCode 后
|
||||
- 内容:具体接口文档
|
||||
- 调用:searchDoc
|
||||
|
||||
验收标准:
|
||||
✓ L1 预加载成功
|
||||
✓ L2 按需调用成功
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 8-9:4 Agent 协作 + Skill
|
||||
|
||||
#### 任务 8.1:4 Agent 定义
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
agent/supervisor/SupervisorAgent.java
|
||||
agent/planner/PlannerAgent.java
|
||||
agent/executor/ExecutorAgent.java
|
||||
agent/verifier/VerifierAgent.java
|
||||
|
||||
配置文件:
|
||||
src/main/resources/prompts/
|
||||
├── supervisor-system.md
|
||||
├── planner-system.md
|
||||
├── executor-system.md
|
||||
└── verifier-system.md
|
||||
|
||||
技术栈:
|
||||
- Spring AI Alibaba
|
||||
- SupervisorAgent + ReactAgent
|
||||
- @Tool 注解
|
||||
|
||||
验收标准:
|
||||
✓ 4 Agent 注册成功
|
||||
✓ 协作流程跑通
|
||||
✓ Supervisor 调度正确
|
||||
```
|
||||
|
||||
#### 任务 8.2:Skill 实现
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
skill/SkillDefinition.java
|
||||
skill/DiagnoseByOrderIdSkill.java
|
||||
skill/SkillRegistry.java
|
||||
|
||||
工作流(6 步):
|
||||
1. queryOrder
|
||||
2. searchDoc (L2 按需)
|
||||
3. queryLogs (Mock)
|
||||
4. recommendCase
|
||||
5. 生成报告
|
||||
6. Verifier 验证
|
||||
|
||||
验收标准:
|
||||
✓ 6 步流程正确
|
||||
✓ 失败处理正确(ABORT/SKIP)
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 10:工具层开发
|
||||
|
||||
#### 任务 10.1:queryOrder 工具
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
tool/QueryOrderTool.java
|
||||
|
||||
功能:
|
||||
- 只读查询 MySQL
|
||||
- 返回订单信息 + 错误信息
|
||||
- SQL 注入防护
|
||||
|
||||
验收标准:
|
||||
✓ 查询正确
|
||||
✓ 超时控制(10s)
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 10.2:searchDoc 工具
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
tool/SearchDocTool.java
|
||||
|
||||
功能:
|
||||
- 调用混合检索
|
||||
- 返回 Top 3 文档片段
|
||||
|
||||
验收标准:
|
||||
✓ 调用成功
|
||||
✓ 结果格式正确
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 10.3:recommendCase 工具
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
tool/RecommendCaseTool.java
|
||||
|
||||
功能:
|
||||
- 精确匹配:error_code + fault_category
|
||||
- 语义检索:description 向量相似度
|
||||
- RRF 融合
|
||||
|
||||
验收标准:
|
||||
✓ 推荐准确
|
||||
✓ 返回 Top 3
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 10.4:getCurrentTime 工具
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
tool/GetCurrentTimeTool.java
|
||||
|
||||
功能:
|
||||
- 返回当前时间戳
|
||||
- 格式化输出
|
||||
|
||||
验收标准:
|
||||
✓ 返回正确
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:闭环优化(3天)
|
||||
|
||||
### Day 11:Verifier + Harness
|
||||
|
||||
#### 任务 11.1:Verifier Agent
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
agent/verifier/VerifierAgent.java
|
||||
|
||||
验证逻辑:
|
||||
1. 事实核查(报告数据 vs 工具返回数据)
|
||||
2. 完整性检查(3 章节不能为空)
|
||||
|
||||
判决:
|
||||
- PASS:通过
|
||||
- REVISE:需修正
|
||||
- REJECT:驳回
|
||||
|
||||
验收标准:
|
||||
✓ 事实核查正确
|
||||
✓ 编造检测生效
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 11.2:Harness 5 Gates
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
harness/gate/InputGates.java
|
||||
harness/gate/ExecutionGates.java
|
||||
harness/gate/OutputGates.java
|
||||
|
||||
门禁清单:
|
||||
- Gate 1: 输入参数非空
|
||||
- Gate 2: 5 分钟内重复 → 缓存
|
||||
- Gate 3: 工具超时(10s)
|
||||
- Gate 4: 报告完整性
|
||||
- Gate 5: 置信度阈值(60)
|
||||
|
||||
验收标准:
|
||||
✓ 5 Gates 生效
|
||||
✓ 中断机制正确
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 12:反馈机制 + 案例沉淀
|
||||
|
||||
#### 任务 12.1:反馈接口
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
controller/FeedbackController.java
|
||||
service/FeedbackService.java
|
||||
|
||||
接口:POST /api/diagnosis/{id}/feedback
|
||||
参数:useful / not_useful
|
||||
|
||||
功能:
|
||||
- 更新 diagnosis_record.feedback
|
||||
- useful → 自动生成 case_library
|
||||
|
||||
验收标准:
|
||||
✓ 反馈记录成功
|
||||
✓ 案例生成正确
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
#### 任务 12.2:案例自动生成
|
||||
|
||||
```java
|
||||
产出文件:
|
||||
service/CaseGenerationService.java
|
||||
|
||||
触发条件:
|
||||
- feedback = useful
|
||||
- confidence >= 80
|
||||
|
||||
生成逻辑:
|
||||
- 提取关键信息
|
||||
- 生成 case_library 记录
|
||||
- 向量化 solution_steps
|
||||
|
||||
验收标准:
|
||||
✓ 案例生成正确
|
||||
✓ 向量化成功
|
||||
✓ 单元测试覆盖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Day 13:端到端测试 + 验收
|
||||
|
||||
#### 任务 13.1:Mock 5 个场景
|
||||
|
||||
```
|
||||
场景 1:外部接口故障(广东社保 40003)
|
||||
场景 2:内部空指针异常
|
||||
场景 3:数据库连接超时
|
||||
场景 4:意图不明(闲聊)
|
||||
场景 5:缓存命中(重复诊断)
|
||||
|
||||
验收标准:
|
||||
✓ 5 个场景全部跑通
|
||||
✓ 诊断报告正确
|
||||
✓ 反馈闭环完整
|
||||
```
|
||||
|
||||
#### 任务 13.2:性能测试
|
||||
|
||||
```
|
||||
指标:
|
||||
- 诊断延迟 < 10s(P95)
|
||||
- 意图识别 < 200ms
|
||||
- 文档检索 < 500ms
|
||||
- 并发 10 QPS 稳定
|
||||
|
||||
验收标准:
|
||||
✓ 性能达标
|
||||
✓ 无内存泄漏
|
||||
✓ 无明显瓶颈
|
||||
```
|
||||
|
||||
#### 任务 13.3:文档更新
|
||||
|
||||
```
|
||||
产出文件:
|
||||
docs/
|
||||
├── API.md # 接口文档
|
||||
├── DEPLOYMENT.md # 部署指南
|
||||
└── TEST_REPORT.md # 测试报告
|
||||
|
||||
验收标准:
|
||||
✓ 文档完整
|
||||
✓ 部署可复现
|
||||
✓ 测试报告详实
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试要求
|
||||
|
||||
### 单元测试
|
||||
|
||||
```
|
||||
框架:JUnit 5 + Mockito
|
||||
覆盖率:
|
||||
- Repository: 100%
|
||||
- Service: 80%+
|
||||
- Tool: 80%+
|
||||
- Agent: 70%+
|
||||
- Controller: 70%+
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
```
|
||||
框架:@SpringBootTest
|
||||
覆盖:
|
||||
- Redis 集成
|
||||
- MySQL 集成
|
||||
- Milvus 集成
|
||||
- Agent 协作
|
||||
```
|
||||
|
||||
### E2E 测试
|
||||
|
||||
```
|
||||
工具:RestAssured
|
||||
场景:5 个 Mock 场景
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施记录格式
|
||||
|
||||
每完成一个任务,AI 在此文档追加:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## [完成] 任务 X.X:任务名称
|
||||
|
||||
**执行时间**:2026-XX-XX HH:mm
|
||||
|
||||
**产出文件**:
|
||||
- path/to/file1.java (126 行)
|
||||
- path/to/file2.java (89 行)
|
||||
|
||||
**关键决策**:
|
||||
- 决策点:选择方案 A,因为...
|
||||
- 权衡点:备选方案 B 的劣势是...
|
||||
|
||||
**遇到的问题**:
|
||||
- 问题:XXX
|
||||
- 解决方案:YYY
|
||||
- 影响范围:ZZZ
|
||||
|
||||
**测试结果**:
|
||||
✓ 单元测试:8/8 通过
|
||||
✓ 集成测试:3/3 通过
|
||||
✓ 代码覆盖率:85%
|
||||
|
||||
**验收状态**:⏳ 等待用户确认 / ✅ 已通过
|
||||
|
||||
**用户反馈**:(用户确认后填写)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前进度
|
||||
|
||||
```
|
||||
Phase 1: 基础设施(5天) [ ] 0%
|
||||
├─ Day 1-2: 数据库 + 实体 [ ] 未开始
|
||||
├─ Day 3: 代码结构重构 [ ] 未开始
|
||||
└─ Day 4-5: 文档管理 [ ] 未开始
|
||||
|
||||
Phase 2: 核心功能(5天) [ ] 0%
|
||||
├─ Day 6-7: 意图识别 + RAG [ ] 未开始
|
||||
├─ Day 8-9: Agent + Skill [ ] 未开始
|
||||
└─ Day 10: 工具层 [ ] 未开始
|
||||
|
||||
Phase 3: 闭环优化(3天) [ ] 0%
|
||||
├─ Day 11: Verifier + Harness [ ] 未开始
|
||||
├─ Day 12: 反馈 + 案例 [ ] 未开始
|
||||
└─ Day 13: E2E 测试 [ ] 未开始
|
||||
|
||||
总体进度:0/13 天
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
等待用户确认:
|
||||
1. ✅ 这个完整计划是否符合预期?
|
||||
2. 有没有需要调整的优先级?
|
||||
3. 有没有需要增删的任务?
|
||||
4. 确认后开始执行 Phase 1 Day 1-2。
|
||||
@@ -1,211 +0,0 @@
|
||||
# 实施规划
|
||||
|
||||
## Phase 1:核心功能(第1周)
|
||||
|
||||
### 实现内容
|
||||
```
|
||||
✅ diagnosis_record 表
|
||||
✅ case_library 表
|
||||
✅ api_document 表
|
||||
✅ Redis 会话管理
|
||||
✅ 单次诊断流程
|
||||
```
|
||||
|
||||
### 不实现
|
||||
```
|
||||
❌ conversation_history 表(先不加)
|
||||
❌ 会话同步(先不做)
|
||||
❌ 追问功能(先不支持)
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
```
|
||||
- 用户输入订单号 → 返回诊断报告
|
||||
- 诊断记录持久化到 MySQL
|
||||
- 可以查询历史诊断
|
||||
- 可以统计诊断成功率
|
||||
- 文档可以导入、查询、删除
|
||||
- 案例可以推荐
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:追问功能(第2周)
|
||||
|
||||
### 实现内容
|
||||
```
|
||||
✅ 支持多轮对话(基于 Redis 上下文)
|
||||
✅ conversation_history 表(可选)
|
||||
✅ 会话上下文管理
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
```
|
||||
- 用户可以追问细节
|
||||
- Agent 能基于上下文回答
|
||||
- 追问不创建新的诊断记录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:优化分析(第3周)
|
||||
|
||||
### 实现内容
|
||||
```
|
||||
✅ 会话同步(Redis → MySQL)
|
||||
✅ BadCase 分析
|
||||
✅ 追问频率统计
|
||||
✅ 案例质量评分
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
```
|
||||
- 重要会话自动同步到 MySQL
|
||||
- 可以分析用户追问模式
|
||||
- 可以优化 Prompt 和功能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术债务清单
|
||||
|
||||
### 待优化项(Phase 4+)
|
||||
|
||||
```
|
||||
1. api_document 增强
|
||||
- 软删除(archived_at)
|
||||
- 启用开关(enabled)
|
||||
- 批次管理(batch_id)
|
||||
- 状态细化(PARSING/SPLITTING/INDEXING...)
|
||||
|
||||
2. case_library 增强
|
||||
- 复杂评分(useful_count + score)
|
||||
- 标签分类(tags)
|
||||
- 版本管理
|
||||
- 案例合并
|
||||
|
||||
3. 性能优化
|
||||
- Redis 缓存有效文档列表
|
||||
- 分页查询优化
|
||||
- 索引优化
|
||||
|
||||
4. 监控告警
|
||||
- 诊断成功率监控
|
||||
- 诊断耗时监控
|
||||
- 文档索引状态监控
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据迁移计划
|
||||
|
||||
### 如果已有旧数据
|
||||
|
||||
```
|
||||
1. diagnosis_record 迁移
|
||||
- 旧字段 → 新字段映射
|
||||
- order_id → business_id
|
||||
- province → fault_source
|
||||
- api_url → fault_target
|
||||
|
||||
2. 执行迁移脚本
|
||||
UPDATE diagnosis_record SET
|
||||
business_id = order_id,
|
||||
fault_category = 'EXTERNAL_API',
|
||||
fault_source = province,
|
||||
fault_target = api_url
|
||||
WHERE fault_category IS NULL;
|
||||
|
||||
3. 验证数据一致性
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署检查清单
|
||||
|
||||
### Phase 1 部署前
|
||||
|
||||
```
|
||||
□ MySQL 数据库已创建
|
||||
□ 三张核心表已创建(diagnosis_record/case_library/api_document)
|
||||
□ Redis 已配置并可连接
|
||||
□ Milvus Collection 已创建
|
||||
□ 向量化服务(DashScope)配置正确
|
||||
□ 文件上传目录已创建并有写权限
|
||||
□ 应用配置文件检查完成
|
||||
```
|
||||
|
||||
### 配置文件示例
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/diagnosis_system
|
||||
username: root
|
||||
password: xxx
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
|
||||
milvus:
|
||||
host: localhost
|
||||
port: 19530
|
||||
collection-name: api_doc_collection
|
||||
|
||||
dashscope:
|
||||
api-key: sk-xxx
|
||||
|
||||
file:
|
||||
upload:
|
||||
path: /data/uploads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚方案
|
||||
|
||||
### 数据库回滚
|
||||
|
||||
```sql
|
||||
-- 保留旧表备份
|
||||
CREATE TABLE diagnosis_record_backup_20240622 AS SELECT * FROM diagnosis_record;
|
||||
|
||||
-- 回滚时恢复
|
||||
DROP TABLE diagnosis_record;
|
||||
RENAME TABLE diagnosis_record_backup_20240622 TO diagnosis_record;
|
||||
```
|
||||
|
||||
### Milvus 回滚
|
||||
|
||||
```
|
||||
- Milvus 数据无法回滚
|
||||
- 建议:重要操作前先备份 Collection
|
||||
- 或者:保留原始文件,可重新索引
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 监控指标
|
||||
|
||||
### 核心指标
|
||||
|
||||
```
|
||||
1. 诊断成功率
|
||||
- 目标:> 85%
|
||||
- 告警:< 80%
|
||||
|
||||
2. 诊断耗时
|
||||
- 目标:P95 < 10s
|
||||
- 告警:P95 > 15s
|
||||
|
||||
3. 文档索引成功率
|
||||
- 目标:> 95%
|
||||
- 告警:< 90%
|
||||
|
||||
4. 案例推荐准确率
|
||||
- 目标:> 70%
|
||||
- 评估:用户反馈
|
||||
```
|
||||
@@ -1,210 +0,0 @@
|
||||
# 会话管理设计
|
||||
|
||||
## 会话存储策略
|
||||
|
||||
### Redis(主)
|
||||
|
||||
**数据结构**:
|
||||
```
|
||||
key: session:{session_id}
|
||||
value: {
|
||||
"sessionId": "sess-abc",
|
||||
"userId": "user-123",
|
||||
"currentDiagnosisId": "diag-001",
|
||||
"messages": [
|
||||
{"role": "user", "content": "诊断订单 A"},
|
||||
{"role": "assistant", "content": "完整报告..."}
|
||||
],
|
||||
"context": {
|
||||
"province": "广东",
|
||||
"apiName": "社保查询",
|
||||
"errorCode": "40003"
|
||||
},
|
||||
"createdAt": "2024-06-15T14:30:00Z",
|
||||
"lastActiveAt": "2024-06-15T14:35:00Z"
|
||||
}
|
||||
ttl: 1800秒(30分钟)
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- ✅ 快速读写
|
||||
- ✅ 自动过期
|
||||
- ✅ 支持追问(保存上下文)
|
||||
|
||||
---
|
||||
|
||||
### MySQL(辅助,可选)
|
||||
|
||||
**同步策略**:
|
||||
1. 重要会话同步
|
||||
- 有用户反馈的会话
|
||||
- 诊断失败的会话(BadCase)
|
||||
- 多轮对话 > 3 轮的会话
|
||||
|
||||
2. 同步时机
|
||||
- 会话结束时(30分钟过期)
|
||||
- 用户反馈时(实时)
|
||||
- 定时任务(每小时,可选)
|
||||
|
||||
3. 同步目标
|
||||
- conversation_history 表
|
||||
- 用于长期分析和审计
|
||||
|
||||
---
|
||||
|
||||
## 数据流设计
|
||||
|
||||
### 场景1:单次诊断(主流 80%)
|
||||
|
||||
```
|
||||
1. 用户发起诊断
|
||||
POST /api/diagnosis/start
|
||||
{
|
||||
"orderId": "202406150001"
|
||||
}
|
||||
|
||||
2. 创建会话(Redis)
|
||||
key: session:sess-abc
|
||||
ttl: 1800秒
|
||||
|
||||
3. 创建诊断记录(MySQL)
|
||||
INSERT INTO diagnosis_record
|
||||
- diagnosis_id: diag-001
|
||||
- session_id: sess-abc
|
||||
- status: RUNNING
|
||||
|
||||
4. Agent 执行诊断
|
||||
- 调用工具(queryOrder, queryLogs, searchDoc...)
|
||||
- 生成报告
|
||||
|
||||
5. 更新诊断记录(MySQL)
|
||||
UPDATE diagnosis_record
|
||||
- status: SUCCESS
|
||||
- root_cause: "idCard字段缺失"
|
||||
- report_markdown: "完整报告..."
|
||||
|
||||
6. 返回报告
|
||||
→ 大部分用户到此结束
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:追问(少数 20%)
|
||||
|
||||
```
|
||||
1. 用户追问
|
||||
POST /api/chat
|
||||
{
|
||||
"sessionId": "sess-abc",
|
||||
"message": "为什么会缺失字段?"
|
||||
}
|
||||
|
||||
2. 从 Redis 获取上下文
|
||||
GET session:sess-abc
|
||||
- 有之前的诊断结果
|
||||
- 有对话历史
|
||||
|
||||
3. Agent 基于上下文回答
|
||||
- 不创建新的 diagnosis_record
|
||||
- 只是普通对话
|
||||
|
||||
4. 更新 Redis 会话
|
||||
- 追加对话历史
|
||||
- 刷新 TTL(重新计时30分钟)
|
||||
|
||||
5. 可选:保存到 conversation_history(MySQL)
|
||||
- 如果需要长期分析
|
||||
- 异步存储
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:同一会话多次诊断
|
||||
|
||||
```
|
||||
1. 用户第一次诊断
|
||||
"诊断订单 A"
|
||||
→ diagnosis_record(diag-001, session_id=sess-abc)
|
||||
|
||||
2. 用户第二次诊断
|
||||
"再诊断订单 B"
|
||||
→ diagnosis_record(diag-002, session_id=sess-abc)
|
||||
|
||||
3. 会话关联
|
||||
- 同一个 session_id
|
||||
- 两条 diagnosis_record
|
||||
- Redis 中保存完整对话历史
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话生命周期
|
||||
|
||||
```
|
||||
创建
|
||||
↓
|
||||
活跃(每次交互刷新TTL)
|
||||
↓
|
||||
30分钟无活动
|
||||
↓
|
||||
自动过期
|
||||
↓
|
||||
可选:同步到 MySQL(重要会话)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现示例
|
||||
|
||||
### Java 代码
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class SessionService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private static final String SESSION_PREFIX = "session:";
|
||||
private static final Duration SESSION_TTL = Duration.ofMinutes(30);
|
||||
|
||||
// 创建会话
|
||||
public String createSession(String userId) {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
|
||||
SessionData session = SessionData.builder()
|
||||
.sessionId(sessionId)
|
||||
.userId(userId)
|
||||
.messages(new ArrayList<>())
|
||||
.context(new HashMap<>())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.lastActiveAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
String key = SESSION_PREFIX + sessionId;
|
||||
redisTemplate.opsForValue().set(key, toJson(session), SESSION_TTL);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
// 获取会话
|
||||
public SessionData getSession(String sessionId) {
|
||||
String key = SESSION_PREFIX + sessionId;
|
||||
String json = redisTemplate.opsForValue().get(key);
|
||||
return json != null ? fromJson(json) : null;
|
||||
}
|
||||
|
||||
// 更新会话(刷新TTL)
|
||||
public void updateSession(SessionData session) {
|
||||
session.setLastActiveAt(LocalDateTime.now());
|
||||
String key = SESSION_PREFIX + session.getSessionId();
|
||||
redisTemplate.opsForValue().set(key, toJson(session), SESSION_TTL);
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
public void deleteSession(String sessionId) {
|
||||
String key = SESSION_PREFIX + sessionId;
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
||||
# 数据库设计文档
|
||||
|
||||
## 📚 文档导航
|
||||
|
||||
### 核心表设计
|
||||
- [diagnosis_record](tables/diagnosis_record.md) - 诊断记录表(核心)
|
||||
- [case_library](tables/case_library.md) - 案例库表
|
||||
- [api_document](tables/api_document.md) - 文档元数据表
|
||||
|
||||
### 架构设计
|
||||
- [Agent 架构设计](architecture/agent-architecture.md) - Agent 协作 + Skill + Harness
|
||||
- [会话管理](architecture/session-management.md) - Redis + MySQL 会话管理
|
||||
- [实施规划](architecture/implementation-plan.md) - 分阶段实施计划
|
||||
|
||||
---
|
||||
|
||||
## 一、设计原则
|
||||
|
||||
### 1.1 核心原则
|
||||
- ✅ **简单优先**:满足诊断流程需要,避免过度设计
|
||||
- ✅ **渐进增强**:先实现核心功能,再逐步扩展
|
||||
- ✅ **数据分离**:诊断结果持久化(MySQL),会话上下文临时化(Redis)
|
||||
- ✅ **适度冗余**:避免过度范式化,适当冗余提升查询性能
|
||||
|
||||
### 1.2 系统定位
|
||||
**自动化诊断系统**
|
||||
- 核心:一键诊断 → 返回完整报告
|
||||
- 辅助:支持追问,但不是主要场景
|
||||
- 特点:大部分用户单次诊断即结束,少数用户会追问细节
|
||||
|
||||
---
|
||||
|
||||
## 二、表结构总览
|
||||
|
||||
### 2.1 核心表关系
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ diagnosis_record │ 诊断记录(核心)
|
||||
│ - 每次诊断一条 │
|
||||
└──────────┬──────────┘
|
||||
│ 1:1
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ case_library │ 案例库(知识沉淀)
|
||||
│ - 诊断成功→案例 │
|
||||
└─────────────────────┘
|
||||
|
||||
┌─────────────────────┐
|
||||
│ api_document │ 文档元数据(管理层)
|
||||
│ - 状态追踪/去重 │
|
||||
└──────────┬──────────┘
|
||||
│ doc_id
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ Milvus │ 文档内容(检索层)
|
||||
│ - 向量检索 │
|
||||
└─────────────────────┘
|
||||
|
||||
┌─────────────────────┐
|
||||
│ Redis Session │ 会话管理(临时)
|
||||
│ - 30分钟过期 │
|
||||
│ - 支持追问 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 表统计
|
||||
|
||||
| 表名 | 类型 | 预估数据量 | 用途 |
|
||||
|------|------|-----------|------|
|
||||
| diagnosis_record | 核心 | 3.6万/年 | 诊断记录 |
|
||||
| case_library | 核心 | 500-1000 | 案例库 |
|
||||
| api_document | 核心 | 100-200 | 文档管理 |
|
||||
|
||||
---
|
||||
|
||||
## 三、技术栈
|
||||
|
||||
### 3.1 数据存储
|
||||
```
|
||||
MySQL 8.0+
|
||||
├─ 元数据管理
|
||||
├─ 事务支持
|
||||
└─ JSON 字段支持
|
||||
|
||||
Redis 6.0+
|
||||
├─ 会话存储
|
||||
├─ 缓存
|
||||
└─ TTL 自动过期
|
||||
|
||||
Milvus 2.6+
|
||||
├─ 向量存储
|
||||
├─ 语义检索
|
||||
└─ 混合检索
|
||||
```
|
||||
|
||||
### 3.2 开发框架
|
||||
```
|
||||
Spring Boot 3.2
|
||||
Spring AI Alibaba 1.1.0
|
||||
Milvus SDK Java 2.6.10
|
||||
DashScope SDK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、快速开始
|
||||
|
||||
### 4.1 创建数据库
|
||||
|
||||
```sql
|
||||
-- 1. 创建数据库
|
||||
CREATE DATABASE diagnosis_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 执行建表脚本(按顺序)
|
||||
SOURCE tables/diagnosis_record.sql;
|
||||
SOURCE tables/case_library.sql;
|
||||
SOURCE tables/api_document.sql;
|
||||
```
|
||||
|
||||
### 4.2 初始化 Milvus
|
||||
|
||||
```java
|
||||
// 创建 Collection
|
||||
MilvusClientFactory.createCollection();
|
||||
```
|
||||
|
||||
### 4.3 配置 Redis
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、版本历史
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| v1.0 | 2024-06-15 | 初版,定义核心表结构 |
|
||||
| v2.0 | 2024-06-15 | diagnosis_record 字段泛化,支持多种故障类型 |
|
||||
| v2.1 | 2024-06-22 | 文档拆分,增加 api_document 表 |
|
||||
|
||||
---
|
||||
|
||||
## 六、维护说明
|
||||
|
||||
- 每个表的详细设计在 `tables/` 目录下
|
||||
- 架构设计文档在 `architecture/` 目录下
|
||||
- 修改表结构时,同步更新对应的 Markdown 文档
|
||||
- 重大变更需记录在版本历史中
|
||||
@@ -0,0 +1,282 @@
|
||||
# 当前分片策略问题分析与根因
|
||||
|
||||
> 基于 `DocumentChunkServiceTest` 可视化测试的运行结果
|
||||
> 配置:`maxSize=800, overlap=100`(默认) / 可视化测试使用 `maxSize=300/200, overlap=50/30`
|
||||
|
||||
---
|
||||
|
||||
## 问题总览
|
||||
|
||||
| # | 问题 | 严重程度 | 根因归类 |
|
||||
|---|------|----------|----------|
|
||||
| 1 | 标题独立成空壳块 | 中 | 标题分割逻辑 |
|
||||
| 2 | 有序列表被拆散 | 高 | 段落级切割 + 缺少结构感知 |
|
||||
| 3 | 英文块 token 密度远低于中文块 | 高 | 字符计数代替 token 计数 |
|
||||
| 4 | 硬截断点在语义转折处无特殊处理 | 中 | 仅依赖 maxSize 触发 |
|
||||
| 5 | overlap 窗口对中文句号后截取命中率低 | 低 | 句子校准逻辑覆盖不全 |
|
||||
|
||||
---
|
||||
|
||||
## 问题 1:标题独立成空壳块
|
||||
|
||||
### 现象
|
||||
|
||||
运维文档 `maxSize=300` 下,H1 标题产生了一个只有 14 字符的分块:
|
||||
|
||||
```
|
||||
Chunk #0
|
||||
│ Title: CPU高负载问题排查指南
|
||||
│ Range: [0→14] (14字符)
|
||||
│ Content:
|
||||
│ │ # CPU高负载问题排查指南
|
||||
```
|
||||
|
||||
紧随其后的 `## 问题现象` 被分到下一个块。14 字符的块没有任何可检索的实质内容。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:71-83
|
||||
while (matcher.find()) {
|
||||
// 保存上一个章节
|
||||
if (lastEnd < matcher.start()) {
|
||||
String sectionContent = content.substring(lastEnd, matcher.start()).trim();
|
||||
if (!sectionContent.isEmpty()) { // ← 条件:content 非空
|
||||
sections.add(new Section(currentTitle, sectionContent, lastEnd));
|
||||
}
|
||||
}
|
||||
currentTitle = matcher.group(2).trim();
|
||||
lastEnd = matcher.start();
|
||||
}
|
||||
```
|
||||
|
||||
`splitByHeadings()` 遍历标题时,`lastEnd` 指向当前标题起始位置,`matcher.start()` 是下一个标题的起始位置。当 H1 后紧跟 H2(中间只有 `#` 行本身的内容),`content.substring(lastEnd, matcher.start())` 取出的是 **H1 标题行本身 + H1 标题行和 H2 之间的空白**。
|
||||
|
||||
关键问题:
|
||||
- H1 标题行被当作上一个 section 的 "content" 保存(因为中间文本不为空——标题行本身是文本)
|
||||
- 但实质上标题不应该独立成为一个可检索的分块
|
||||
|
||||
### 影响
|
||||
|
||||
- 向量库中出现大量无效向量(仅含标题、无实质内容)
|
||||
- 检索时可能召回标题块,Agent 得不到有用信息
|
||||
- 浪费 Milvus 存储空间
|
||||
|
||||
---
|
||||
|
||||
## 问题 2:有序列表被拆散
|
||||
|
||||
### 现象
|
||||
|
||||
排查步骤 1-4 在 Chunk #2,第 5 步被单独踢到 Chunk #3:
|
||||
|
||||
```
|
||||
Chunk #2 → 1. 登录服务器... 2. 使用 ps... 3. 查看应用日志... 4. 检查数据库...
|
||||
Chunk #3 → 5. 检查JVM内存...
|
||||
```
|
||||
|
||||
Agent 调用工具拿到 Chunk #2 时,排查步骤不完整,可能漏掉关键操作。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:174-188
|
||||
private List<String> splitByParagraphs(String content) {
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
String[] parts = content.split("\n\n+"); // ← 双换行分割
|
||||
for (String part : parts) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
paragraphs.add(trimmed);
|
||||
}
|
||||
}
|
||||
return paragraphs;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132-148
|
||||
if (currentChunk.length() > 0 &&
|
||||
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 触发切分——不关心这个段落属于什么语义结构
|
||||
String overlap = getOverlapText(chunkContent);
|
||||
currentChunk = new StringBuilder(overlap);
|
||||
}
|
||||
currentChunk.append(paragraph).append("\n\n");
|
||||
```
|
||||
|
||||
两层根因:
|
||||
1. `splitByParagraphs()` 只认 `\n\n+` 作为段落分割符,不识别 **有序列表**(`1. \n2. \n3.` 之间通常是单换行)
|
||||
2. `chunkSection()` 走到字符上限就切,完全不感知"这是一个列表的第几项"——列表项之间的语义强关联被忽略
|
||||
|
||||
### 影响
|
||||
|
||||
- 排查步骤、操作指南类文档的完整性被破坏
|
||||
- RAG 检索召回不完整的步骤列表,Agent 据此操作可能导致遗漏
|
||||
- 这是运维场景的致命问题——运维文档大量使用列表
|
||||
|
||||
---
|
||||
|
||||
## 问题 3:英文块 token 密度远低于中文块
|
||||
|
||||
### 现象
|
||||
|
||||
可视化测试数据:
|
||||
|
||||
```
|
||||
中文: 218字符 → 2个分块(约218 tokens,密度 ~1.0 token/字符)
|
||||
英文: 602字符 → 3个分块(约150 tokens,密度 ~0.25 token/字符)
|
||||
```
|
||||
|
||||
同样 `maxSize=200`,英文 602 字符装了 150 token 还产生 3 个分块;中文 218 字符装了 218 token 只产生 2 个分块。中文块的实际 token 负担是英文的 **~4x**。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkConfig.java:18
|
||||
private int maxSize = 800; // 字符数上限
|
||||
|
||||
// DocumentChunkService.java:132-133
|
||||
if (currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
// 这里比的是 Java String.length() — 字符数,不是 token 数
|
||||
```
|
||||
|
||||
Java 的 `String.length()` 对每个 Unicode 字符(包括中文)都返回 1。但 LLM tokenizer 对中文和英文的 token 化效率完全不同:
|
||||
|
||||
```
|
||||
"这是中文" → 4 字符 → ~4 tokens (1:1)
|
||||
"This is English" → 15 字符 → ~4 tokens (3.75:1)
|
||||
```
|
||||
|
||||
用字符数作为切割上限,相当于:
|
||||
- 中文块:可以装 800 token(甚至更多)
|
||||
- 英文块:只能装 ~200 token
|
||||
|
||||
LLM 上下文窗口是按 token 计费的,这种偏差意味着**中文知识库的 RAG 开销是英文的 4 倍**。
|
||||
|
||||
### 影响
|
||||
|
||||
- LLM 调用成本不可预测(中英混排时波动大)
|
||||
- 中文知识库的上下文窗口利用率极易超标
|
||||
- 无法对 prompt 的 token 预算做精确控制
|
||||
|
||||
---
|
||||
|
||||
## 问题 4:硬截断在语义转折处无特殊处理
|
||||
|
||||
### 现象
|
||||
|
||||
同问题 2 的根因延伸。当前逻辑:
|
||||
|
||||
```
|
||||
段落1 + 段落2 + 段落3 + ... + 段落N → 总字符数 < maxSize → 继续追加
|
||||
→ 总字符数 > maxSize → 立刻切
|
||||
```
|
||||
|
||||
不考虑「段落 N 和段落 N+1 是否属于同一语义单元」。两个语义上需要绑定的段落恰好越过 maxSize 边界就会被拆散。
|
||||
|
||||
### 根因
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:132
|
||||
if (currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
```
|
||||
|
||||
触发条件只有一个——字符数。不缺以下信号:
|
||||
- 相邻段落的语义相似度(可用 embedding 计算)
|
||||
- 当前缓冲区是否处于列表/表格/代码块内部
|
||||
- 当前位置是否是 Markdown 层级的自然边界(如 `##` 标题前)
|
||||
|
||||
### 影响
|
||||
|
||||
- 切出来的分块边界在语义上不可预测
|
||||
- 同一主题的内容可能跨越两个分块,召回时只能拿到一半上下文
|
||||
|
||||
---
|
||||
|
||||
## 问题 5:overlap 句子校准对中文覆盖不全
|
||||
|
||||
### 现象
|
||||
|
||||
测试用的中文句子边界校准场景中,文档字数不足 `maxSize=100`,未触发切分。但即便触发,当前校准逻辑存在盲区:
|
||||
|
||||
```java
|
||||
// DocumentChunkService.java:203-206
|
||||
int lastSentenceEnd = Math.max(
|
||||
overlap.lastIndexOf('。'), // 只有三个终止符
|
||||
Math.max(overlap.lastIndexOf('?'), overlap.lastIndexOf('!'))
|
||||
);
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
中文句子终止符不止 `。?!` 三种:
|
||||
|
||||
| 终止符 | 是否覆盖 | 遗漏场景 |
|
||||
|--------|----------|----------|
|
||||
| `。` | ✅ | — |
|
||||
| `?` | ✅ | — |
|
||||
| `!` | ✅ | — |
|
||||
| `;`(分号) | ❌ | 长复句的语义断点 |
|
||||
| `:`(冒号) | ❌ | 列表/说明的引入点 |
|
||||
| `……` | ❌ | 省略号表示语义未尽 |
|
||||
| `\n`(换行) | ❌ | 中文短句常用换行代替标点 |
|
||||
|
||||
阈值逻辑也有盲区:
|
||||
|
||||
```java
|
||||
if (lastSentenceEnd > overlapSize / 2) {
|
||||
// only apply if sentence boundary is in the LATER half of overlap
|
||||
}
|
||||
```
|
||||
|
||||
如果句子边界在重叠区的前半段(即离截断点不到 overlap/2),直接退回原始截取——但实际上即使在前半段,也比随机截取更好。
|
||||
|
||||
### 影响
|
||||
|
||||
- 中文内容的重叠窗口可能从句子中间截取
|
||||
- 新分块的"种子"文本不完整,影响该块的语义完整性
|
||||
|
||||
---
|
||||
|
||||
## 根因总结
|
||||
|
||||
所有 5 个问题的根源收敛到两点:
|
||||
|
||||
### 根因 A:切割触发器只有一个维度——字符数
|
||||
|
||||
```
|
||||
currentChunk.length() + paragraph.length() > maxSize → 切!
|
||||
```
|
||||
|
||||
这个条件不知道:
|
||||
- 这个"paragraph"是列表项还是普通段落?(问题 2)
|
||||
- 中文还是英文?(问题 3)
|
||||
- 和上一条内容语义紧密还是已经转移话题?(问题 4)
|
||||
- 这个位置是在 Markdown 结构树上的什么层级?(问题 1)
|
||||
|
||||
### 根因 B:文档结构感知仅限于正则标题
|
||||
|
||||
```java
|
||||
Pattern headingPattern = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
|
||||
```
|
||||
|
||||
这是唯一的结构感知入口。正则比 AST 脆弱,无法区分:
|
||||
- 代码块内的 `#` 注释 vs 真正的 Markdown 标题
|
||||
- 列表项 vs 段落
|
||||
- 代码块 vs 正文
|
||||
- 表格 vs 正文
|
||||
|
||||
---
|
||||
|
||||
## 修复优先级建议
|
||||
|
||||
| 优先级 | 问题 | 对策 | 改动量 |
|
||||
|--------|------|------|--------|
|
||||
| P0 | 问题 3(中英 token 密度) | 字符计数 → token 计数 | ~10 行 |
|
||||
| P0 | 问题 2(列表拆散) | 增加列表结构感知 | ~30 行 |
|
||||
| P1 | 问题 1(标题空壳) | 标题与下一个 H2 之间内容为空时合并 | ~15 行 |
|
||||
| P1 | 问题 4(硬截断) | 语义相似度辅助决策切点 | ~30 行 |
|
||||
| P2 | 问题 5(句子校准覆盖) | 增加终止符 + 降低阈值条件 | ~5 行 |
|
||||
|
||||
最终方案:替换为 Spring AI `TokenTextSplitter`,同时保留本项目特有的 `title` 元数据传播能力(因为 `TokenTextSplitter` 也不感知 Markdown 标题)。
|
||||
@@ -0,0 +1,200 @@
|
||||
# Plan: 分片策略第 4 步重构
|
||||
|
||||
> 分支: `refactor/rag-chunking-strategy`
|
||||
> 状态: 规划中
|
||||
> 范围: 仅改 `DocumentChunkService.chunkSection()` 一个方法
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
经 debug 确认,当前分片流程的 1/2/3 步逻辑正确:
|
||||
|
||||
```
|
||||
第1步 chunkDocument() → splitByHeadings(content) ✅ 保持不变
|
||||
第2步 for each Section → 循环章节 ✅ 保持不变
|
||||
第3步 chunkSection() 入口 → 容量短路判断 + splitByParagraphs ✅ 保持不变
|
||||
第4步 chunkSection() 累积循环 → 段落累积 + 字符触发切分 ❌ 需重构
|
||||
```
|
||||
|
||||
**第 4 步的两个核心问题:**
|
||||
|
||||
| 问题 | 现象 |
|
||||
|------|------|
|
||||
| A. 丢失顺序 | `trim()` + 手工拼接 `\n\n` 导致 `currentStartIndex` 漂移 |
|
||||
| B. 结构无感知 | 有序列表项被拆散到不同分块(排查步骤 1-4 在一块,第 5 步在另一块) |
|
||||
|
||||
---
|
||||
|
||||
## 目标
|
||||
|
||||
改造 `chunkSection()` 的段落累积循环,使其:
|
||||
|
||||
1. **不丢顺序** — 用原始文本索引替代手工拼装的 `currentStartIndex`
|
||||
2. **感知列表结构** — 有序/无序列表项之间不在中间切断
|
||||
3. **Token 感知** — 用启发式 token 估算替代纯字符计数(为后续 Spring AI TokenTextSplitter 做准备)
|
||||
4. **软边界** — 在接近上限时查找语义安全切点,而非硬截断
|
||||
|
||||
---
|
||||
|
||||
## 不改的部分
|
||||
|
||||
| 组件 | 理由 |
|
||||
|------|------|
|
||||
| `splitByHeadings()` | 标题分割正确,正则够用 |
|
||||
| `getOverlapText()` | 句子校准逻辑保留,作为安全网 |
|
||||
| `DocumentChunk` 数据结构 | 字段完备,无需新增 |
|
||||
| `DocumentChunkConfig` | 增加 `maxTokens` 字段,保留原字段兼容 |
|
||||
| `VectorIndexService` | 消费者改动延后到下一阶段 |
|
||||
|
||||
---
|
||||
|
||||
## 改动方案
|
||||
|
||||
### 改动 1: `DocumentChunkConfig` — 增加 token 配置
|
||||
|
||||
```java
|
||||
// 新增字段
|
||||
private int maxTokens = 500; // token 上限(中文约500字,英文约2000字符)
|
||||
private int maxTokensHard = 600; // 硬上限(maxTokens × 1.2)
|
||||
|
||||
// 保留原字段作为向后兼容
|
||||
private int maxSize = 800; // 保留但标记 @Deprecated
|
||||
```
|
||||
|
||||
### 改动 2: `chunkSection()` — 改造累积循环
|
||||
|
||||
**当前逻辑(伪代码):**
|
||||
|
||||
```
|
||||
for each paragraph:
|
||||
if length + paragraph > maxSize → 切分 → 从 overlap 开始新块
|
||||
append paragraph + "\n\n"
|
||||
```
|
||||
|
||||
**新逻辑(伪代码):**
|
||||
|
||||
```
|
||||
for each paragraph:
|
||||
currentTokens = estimateTokens(buffer)
|
||||
paraTokens = estimateTokens(paragraph)
|
||||
|
||||
if currentTokens + paraTokens > maxTokens:
|
||||
if isInUnbreakableContext(buffer, paragraph):
|
||||
if currentTokens + paraTokens > maxTokensHard:
|
||||
→ 必须切(硬上限保护)
|
||||
else:
|
||||
→ 不切,继续累积(容忍超出,保护列表完整性)
|
||||
else:
|
||||
→ 切分(段落边界 = 安全切点)
|
||||
→ 从 overlap 开始新块
|
||||
else:
|
||||
→ 不切,继续累积
|
||||
|
||||
append paragraph + "\n\n"
|
||||
```
|
||||
|
||||
### 改动 3: 新增 `estimateTokens()` — 启发式 token 估算
|
||||
|
||||
```java
|
||||
/**
|
||||
* 启发式 token 估算(无需外部依赖)
|
||||
* 中文: ~1 字符/token
|
||||
* 英文/数字: ~4 字符/token
|
||||
* 标点/空白: 忽略
|
||||
*/
|
||||
private int estimateTokens(String text) {
|
||||
int tokens = 0;
|
||||
for (char c : text.toCharArray()) {
|
||||
if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|
||||
|| Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) {
|
||||
tokens += 1; // 中文字符 1:1
|
||||
} else if (Character.isWhitespace(c)) {
|
||||
// 空白字符不计
|
||||
} else {
|
||||
tokens += 1; // 非中文凑 4 个算 1 token(简化)
|
||||
}
|
||||
}
|
||||
// 非中文部分 / 4
|
||||
return tokens;
|
||||
}
|
||||
```
|
||||
|
||||
### 改动 4: 新增 `isInUnbreakableContext()` — 结构感知
|
||||
|
||||
```java
|
||||
/**
|
||||
* 判断当前段落是否属于不可中断的结构
|
||||
* 返回 true = 不能在当前位置切分
|
||||
*/
|
||||
private boolean isInUnbreakableContext(String buffer, String nextParagraph) {
|
||||
// 有序列表: "1. " "2. " "3. " 格式
|
||||
if (nextParagraph.matches("^\\d{1,2}\\.\\s.*")) {
|
||||
// 前一个段落也是列表项 → 不切
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine.matches("^\\d{1,2}\\.\\s.*|.*\\n\\d{1,2}\\.\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 无序列表: "- " 或 "* " 格式
|
||||
if (nextParagraph.matches("^[-*]\\s.*")) {
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine.matches("^[-*]\\s.*|.*\\n[-*]\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 代码块: ``` 内部不切
|
||||
if (buffer.contains("```") && countOccurrences(buffer, "```") % 2 == 1) {
|
||||
return true; // 在未闭合的代码块内 → 不切
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### 改动 5: 修复 index 漂移
|
||||
|
||||
```java
|
||||
// 当前问题:用手工拼装的 chunkContent.length() 推算 offset
|
||||
// String chunkContent = currentChunk.toString().trim(); ← trim 丢字符
|
||||
// currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length(); ← 漂移
|
||||
|
||||
// 改为:用段落在原始文档中的实际位置
|
||||
// 对每个 paragraph 记录其在 section.content 中的 offset,切分时直接使用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
| 文件 | 改动 | 行数变化 |
|
||||
|------|------|----------|
|
||||
| `config/DocumentChunkConfig.java` | +2 字段 | +8 |
|
||||
| `service/DocumentChunkService.java` | 改造 `chunkSection()` + 3 个新方法 | ~+50 / -20 |
|
||||
| `test/.../DocumentChunkServiceTest.java` | 新增列表结构感知 + token 估算用例 | +40 |
|
||||
|
||||
总计改动约 80 行,仅影响一个核心方法。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
| # | 用例 | 预期 |
|
||||
|---|------|------|
|
||||
| 1 | 有序列表(5 项,每项 50 字符,maxTokens=180) | 5 项不拆散,容忍略超上限 |
|
||||
| 2 | 有序列表(20 项,超 maxTokensHard) | 在硬上限处切,但不在列表项中间切 |
|
||||
| 3 | 纯段落(10 段,每段 100 字符,maxTokens=300) | 在段落边界切 |
|
||||
| 4 | 中文 800 字 vs 英文 3200 字符 | 分块数接近 |
|
||||
| 5 | H1→空的→H2(标题空壳) | 仍有(不在本次修复范围) |
|
||||
| 6 | 原有测试:空文档、短文档、标题分割、重叠、chunkIndex | 全部通过 |
|
||||
|
||||
---
|
||||
|
||||
## 后续阶段
|
||||
|
||||
| 阶段 | 内容 | 依赖 |
|
||||
|------|------|------|
|
||||
| **Phase 1(本次)** | 改 `chunkSection()` — token + 列表感知 | 无 |
|
||||
| Phase 2 | 标题空壳问题修复(`splitByHeadings` 合并相邻空 section) | Phase 1 |
|
||||
| Phase 3 | 可选:切换到 Spring AI `TokenTextSplitter` | Phase 1/2 |
|
||||
| Phase 4 | 语义相似度辅助切点决策 | Phase 1 |
|
||||
| Phase 5 | Markdown AST 解析替代正则 | 低优先级 |
|
||||
@@ -1,332 +0,0 @@
|
||||
# api_document - 文档元数据表
|
||||
|
||||
## 表定位
|
||||
|
||||
**文档管理表**:管理接口文档的元信息,不负责文档检索(检索由 Milvus 负责)
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 文档管理,不是文档检索
|
||||
|
||||
**核心定位**:
|
||||
- MySQL 负责文档元数据管理(状态、版本、去重)
|
||||
- Milvus 负责文档内容存储和检索
|
||||
- 通过 doc_id 关联两者
|
||||
|
||||
**MVP版本原则**:
|
||||
- ✅ 最简字段,满足基本管理需求
|
||||
- ✅ 文件去重(基于 file_hash)
|
||||
- ✅ 状态追踪(索引进度)
|
||||
- ✅ 硬删除(同步删除 Milvus 数据)
|
||||
- ❌ 暂不支持:软删除、启用开关、版本管理(Phase 2)
|
||||
|
||||
---
|
||||
|
||||
## 表结构(MVP版)
|
||||
|
||||
```sql
|
||||
CREATE TABLE api_document (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
doc_id VARCHAR(64) UNIQUE NOT NULL COMMENT '文档唯一ID(UUID),关联Milvus',
|
||||
|
||||
-- 文档分类
|
||||
fault_category VARCHAR(32) DEFAULT 'EXTERNAL_API' COMMENT '文档类别',
|
||||
fault_source VARCHAR(128) COMMENT '文档归属(省份/服务名)',
|
||||
api_name VARCHAR(128) COMMENT '接口名称',
|
||||
version VARCHAR(32) DEFAULT 'v1.0' COMMENT '文档版本',
|
||||
|
||||
-- 文件信息
|
||||
file_name VARCHAR(256) NOT NULL COMMENT '原始文件名',
|
||||
file_path VARCHAR(512) COMMENT '文件存储路径',
|
||||
file_hash VARCHAR(64) COMMENT '文件MD5 hash(用于去重)',
|
||||
file_size BIGINT COMMENT '文件大小(字节)',
|
||||
|
||||
-- 索引状态
|
||||
status VARCHAR(16) DEFAULT 'PENDING' COMMENT '索引状态(PENDING/PROCESSING/INDEXED/FAILED)',
|
||||
chunk_count INT DEFAULT 0 COMMENT '分块数量',
|
||||
error_message TEXT COMMENT '失败原因',
|
||||
|
||||
-- 时间字段
|
||||
indexed_at DATETIME COMMENT '索引完成时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
UNIQUE INDEX uk_file_hash (file_hash),
|
||||
INDEX idx_doc_id (doc_id),
|
||||
INDEX idx_fault_source (fault_source),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档元数据表(MVP版)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| doc_id | VARCHAR(64) | 是 | **核心**:文档唯一ID,关联 Milvus |
|
||||
| fault_category | VARCHAR(32) | 否 | 文档类别 |
|
||||
| fault_source | VARCHAR(128) | 否 | 文档归属(省份/服务名)|
|
||||
| api_name | VARCHAR(128) | 否 | 接口名称 |
|
||||
| version | VARCHAR(32) | 否 | 文档版本 |
|
||||
| file_name | VARCHAR(256) | 是 | 原始文件名 |
|
||||
| file_path | VARCHAR(512) | 否 | 文件存储路径 |
|
||||
| file_hash | VARCHAR(64) | 否 | **去重关键**:文件MD5 |
|
||||
| file_size | BIGINT | 否 | 文件大小 |
|
||||
| status | VARCHAR(16) | 是 | **状态追踪**:PENDING/PROCESSING/INDEXED/FAILED |
|
||||
| chunk_count | INT | 否 | 分块数量 |
|
||||
| error_message | TEXT | 否 | 失败原因 |
|
||||
| indexed_at | DATETIME | 否 | 索引完成时间 |
|
||||
|
||||
---
|
||||
|
||||
## 核心设计决策
|
||||
|
||||
### 1. doc_id:MySQL 与 Milvus 的桥梁
|
||||
|
||||
```
|
||||
作用:
|
||||
- MySQL:通过 doc_id 管理文档元数据
|
||||
- Milvus:每个 chunk 的 metadata 中携带 doc_id
|
||||
|
||||
关联关系:
|
||||
api_document (MySQL)
|
||||
doc_id: doc-001
|
||||
↓ 1:N
|
||||
Milvus chunks
|
||||
chunk_1: {doc_id: 'doc-001', text: '...', vector: [...]}
|
||||
chunk_2: {doc_id: 'doc-001', text: '...', vector: [...]}
|
||||
|
||||
管理操作:
|
||||
- 删除文档:
|
||||
DELETE FROM milvus_collection WHERE metadata["doc_id"] == 'doc-001';
|
||||
DELETE FROM api_document WHERE doc_id = 'doc-001';
|
||||
```
|
||||
|
||||
### 2. file_hash:文件去重
|
||||
|
||||
```
|
||||
去重流程:
|
||||
1. 用户上传文件
|
||||
↓
|
||||
2. 计算文件 MD5
|
||||
file_hash = md5(file_content)
|
||||
↓
|
||||
3. 检查是否已存在
|
||||
SELECT * FROM api_document WHERE file_hash = 'abc123...';
|
||||
↓
|
||||
4a. 如果存在 → 提示"文档已存在"
|
||||
4b. 如果不存在 → 继续导入
|
||||
|
||||
唯一约束:UNIQUE INDEX uk_file_hash (file_hash)
|
||||
```
|
||||
|
||||
### 3. status:状态追踪
|
||||
|
||||
```
|
||||
状态流转:
|
||||
PENDING (待处理)
|
||||
↓
|
||||
PROCESSING (处理中)
|
||||
↓ 成功
|
||||
INDEXED (已索引)
|
||||
↓ 失败
|
||||
FAILED (失败)
|
||||
|
||||
用途:
|
||||
- 批量导入时监控进度
|
||||
- 失败重试
|
||||
- 统计索引成功率
|
||||
```
|
||||
|
||||
### 4. 硬删除策略(MVP)
|
||||
|
||||
```
|
||||
删除文档时:
|
||||
1. 删除 Milvus 中的所有分块
|
||||
2. 删除 MySQL 元数据
|
||||
3. 可选:删除原始文件
|
||||
|
||||
特点:
|
||||
- 简单直接
|
||||
- 数据彻底删除
|
||||
- 不可恢复(需谨慎)
|
||||
|
||||
Phase 2 可增强:
|
||||
- 软删除(archived_at)
|
||||
- 启用开关(enabled)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流
|
||||
|
||||
### 场景1:导入新文档
|
||||
|
||||
```
|
||||
1. 用户上传文件
|
||||
↓
|
||||
2. 计算 hash
|
||||
↓
|
||||
3. 检查去重(MySQL)
|
||||
↓
|
||||
4. 插入元数据(status=PROCESSING)
|
||||
↓
|
||||
5. 后台处理:解析 → 分块 → 向量化 → 存入 Milvus
|
||||
↓
|
||||
6. 更新状态(status=INDEXED, chunk_count=15)
|
||||
```
|
||||
|
||||
### 场景2:删除文档
|
||||
|
||||
```
|
||||
1. 用户删除文档
|
||||
↓
|
||||
2. 删除 Milvus 数据(WHERE metadata["doc_id"] == 'xxx')
|
||||
↓
|
||||
3. 删除 MySQL 元数据
|
||||
↓
|
||||
4. 可选:删除原始文件
|
||||
```
|
||||
|
||||
### 场景3:重新索引
|
||||
|
||||
```
|
||||
1. 删除旧数据(Milvus + MySQL)
|
||||
↓
|
||||
2. 重新导入(同场景1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型查询
|
||||
|
||||
```sql
|
||||
-- 查看文档列表
|
||||
SELECT doc_id, file_name, version, status, chunk_count, indexed_at
|
||||
FROM api_document
|
||||
WHERE fault_source = '广东'
|
||||
AND status = 'INDEXED'
|
||||
ORDER BY indexed_at DESC;
|
||||
|
||||
-- 查询失败的文档
|
||||
SELECT doc_id, file_name, error_message
|
||||
FROM api_document
|
||||
WHERE status = 'FAILED';
|
||||
|
||||
-- 统计各状态文档数量
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM api_document
|
||||
GROUP BY status;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与 Milvus 的协作
|
||||
|
||||
### Milvus Collection Schema
|
||||
|
||||
```python
|
||||
{
|
||||
"collection_name": "api_doc_collection",
|
||||
"fields": [
|
||||
{"name": "id", "type": "VARCHAR", "is_primary": true},
|
||||
{"name": "content", "type": "VARCHAR"},
|
||||
{"name": "vector", "type": "FLOAT_VECTOR", "dim": 1536},
|
||||
{"name": "metadata", "type": "JSON"}
|
||||
]
|
||||
}
|
||||
|
||||
# metadata 结构
|
||||
{
|
||||
"doc_id": "doc-001", # 关联 MySQL
|
||||
"_source": "/path/to/file",
|
||||
"_file_name": "xxx.docx",
|
||||
"chunkIndex": 0,
|
||||
"totalChunks": 15
|
||||
}
|
||||
```
|
||||
|
||||
### Java 代码示例
|
||||
|
||||
```java
|
||||
// 插入时携带 doc_id
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("doc_id", docId); // 关联 MySQL
|
||||
metadata.put("_source", filePath);
|
||||
metadata.put("chunkIndex", chunkIndex);
|
||||
|
||||
// 删除文档的所有分块
|
||||
String expr = String.format("metadata[\"doc_id\"] == \"%s\"", docId);
|
||||
milvusClient.delete(DeleteParam.newBuilder()
|
||||
.withCollectionName(COLLECTION_NAME)
|
||||
.withExpr(expr)
|
||||
.build());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据示例
|
||||
|
||||
```sql
|
||||
-- 外部接口文档
|
||||
INSERT INTO api_document VALUES
|
||||
(1, 'doc-001', 'EXTERNAL_API', '广东', '社保查询', 'v2.1',
|
||||
'广东社保查询v2.1.docx', '/docs/guangdong/social-v2.1.docx',
|
||||
'abc123...', 1048576,
|
||||
'INDEXED', 15, NULL, '2024-06-15 10:30:00', NOW(), NOW());
|
||||
|
||||
-- 内部服务文档
|
||||
INSERT INTO api_document VALUES
|
||||
(2, 'doc-002', 'INTERNAL_ERROR', 'order-service', '订单服务API', 'v1.0',
|
||||
'订单服务API文档.pdf', '/docs/internal/order-service-api.pdf',
|
||||
'def456...', 2097152,
|
||||
'INDEXED', 20, NULL, '2024-06-14 15:20:00', NOW(), NOW());
|
||||
|
||||
-- 处理失败的文档
|
||||
INSERT INTO api_document VALUES
|
||||
(3, 'doc-003', 'EXTERNAL_API', '江苏', '公积金查询', 'v1.5',
|
||||
'江苏公积金查询.html', '/docs/jiangsu/fund-v1.5.html',
|
||||
'ghi789...', 512000,
|
||||
'FAILED', 0, '不支持HTML格式', NULL, NOW(), NOW());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据量预估
|
||||
|
||||
```
|
||||
预估:100-200 条
|
||||
- 外部接口文档:50-100 条
|
||||
- 内部服务文档:20-50 条
|
||||
- 其他文档:30-50 条
|
||||
|
||||
存储:
|
||||
- 单条记录:约 1KB
|
||||
- 200 条:约 200KB
|
||||
|
||||
结论:数据量很小
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP 版本的简化
|
||||
|
||||
```
|
||||
Phase 1(当前):
|
||||
✅ 基础字段和表结构
|
||||
✅ 文件去重(file_hash)
|
||||
✅ 状态追踪(status)
|
||||
✅ 硬删除
|
||||
✅ 通过 doc_id 关联 Milvus
|
||||
|
||||
Phase 2(未来增强):
|
||||
❌ enabled(启用开关)
|
||||
❌ archived_at(软删除)
|
||||
❌ batch_id(批次管理)
|
||||
❌ status 细化
|
||||
❌ tags(标签分类)
|
||||
```
|
||||
@@ -1,265 +0,0 @@
|
||||
# case_library - 案例库表
|
||||
|
||||
## 表定位
|
||||
|
||||
**知识沉淀表**:存储高质量诊断案例,支持相似案例推荐
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 知识沉淀,系统越用越智能
|
||||
|
||||
**核心价值**:
|
||||
- 质量过滤:只存储高质量案例(成功诊断 + 用户反馈有用)
|
||||
- 知识沉淀:历史诊断经验可复用
|
||||
- 提升准确率:相似问题提供历史参考
|
||||
- 加速诊断:快速推荐相似案例
|
||||
|
||||
**MVP版本设计原则**:
|
||||
- ✅ 能用:满足基本案例推荐功能
|
||||
- ✅ 简单:字段不多,逻辑清晰
|
||||
- ✅ 可扩展:后续可增加字段
|
||||
|
||||
---
|
||||
|
||||
## 表结构(MVP版)
|
||||
|
||||
```sql
|
||||
CREATE TABLE case_library (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
case_id VARCHAR(64) UNIQUE NOT NULL COMMENT '案例唯一ID(UUID)',
|
||||
|
||||
-- 来源关联
|
||||
diagnosis_id VARCHAR(64) COMMENT '关联诊断记录(可选,人工录入时为空)',
|
||||
source_type VARCHAR(16) DEFAULT 'AUTO' COMMENT '来源类型(AUTO:自动生成/MANUAL:人工录入)',
|
||||
|
||||
-- 案例分类
|
||||
fault_category VARCHAR(32) COMMENT '故障类别(EXTERNAL_API/INTERNAL_ERROR/DATABASE...)',
|
||||
fault_source VARCHAR(128) COMMENT '故障源(省份/服务名/类名...)',
|
||||
fault_target VARCHAR(256) COMMENT '故障目标(接口URL/方法名/SQL...)',
|
||||
error_code VARCHAR(64) COMMENT '错误码',
|
||||
|
||||
-- 案例内容
|
||||
title VARCHAR(256) NOT NULL COMMENT '案例标题(简短描述)',
|
||||
root_cause TEXT NOT NULL COMMENT '根因分析',
|
||||
solution TEXT NOT NULL COMMENT '解决方案',
|
||||
|
||||
-- 简单统计
|
||||
reference_count INT DEFAULT 0 COMMENT '引用次数(被推荐的次数)',
|
||||
|
||||
-- 元数据
|
||||
created_by VARCHAR(64) COMMENT '创建人',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_fault_category (fault_category),
|
||||
INDEX idx_error_code (error_code),
|
||||
INDEX idx_fault_source (fault_source),
|
||||
INDEX idx_fault_target (fault_target(100)),
|
||||
INDEX idx_diagnosis_id (diagnosis_id),
|
||||
INDEX idx_reference_count (reference_count),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='案例库表(MVP版)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| case_id | VARCHAR(64) | 是 | 案例唯一标识(UUID)|
|
||||
| diagnosis_id | VARCHAR(64) | 否 | 关联诊断记录(人工录入时为空)|
|
||||
| source_type | VARCHAR(16) | 是 | 来源:AUTO(自动)/MANUAL(人工)|
|
||||
| fault_category | VARCHAR(32) | 否 | 故障类别 |
|
||||
| fault_source | VARCHAR(128) | 否 | 故障源 |
|
||||
| fault_target | VARCHAR(256) | 否 | 故障目标(与 diagnosis_record 一致)|
|
||||
| error_code | VARCHAR(64) | 否 | 错误码 |
|
||||
| title | VARCHAR(256) | 是 | 案例标题 |
|
||||
| root_cause | TEXT | 是 | 根因分析(核心内容)|
|
||||
| solution | TEXT | 是 | 解决方案(核心内容)|
|
||||
| reference_count | INT | 是 | 引用次数(用于排序)|
|
||||
|
||||
---
|
||||
|
||||
## 核心设计决策
|
||||
|
||||
### 1. 案例来源
|
||||
|
||||
```
|
||||
来源1:自动生成(source_type=AUTO)
|
||||
├─ 触发条件:诊断成功 + 用户反馈"有用"
|
||||
├─ 关联诊断:diagnosis_id 不为空
|
||||
└─ 质量保证:用户验证过
|
||||
|
||||
来源2:人工录入(source_type=MANUAL)
|
||||
├─ 运维团队总结的经典案例
|
||||
├─ diagnosis_id 为空
|
||||
└─ 质量最高
|
||||
|
||||
注意:诊断失败或用户反馈"无用"的不自动生成案例
|
||||
```
|
||||
|
||||
### 2. 简化的评分机制(MVP)
|
||||
|
||||
```
|
||||
MVP版本:只按 reference_count 排序
|
||||
- 引用次数多的排前面
|
||||
- 简单有效
|
||||
|
||||
Phase 2 可增强:
|
||||
- 增加 useful_count(用户反馈有用次数)
|
||||
- 增加 score(综合评分)
|
||||
- 增加 is_featured(人工标记的经典案例)
|
||||
```
|
||||
|
||||
### 3. 与 diagnosis_record 的关系
|
||||
|
||||
```
|
||||
关系:一对一(可选)
|
||||
- 一次诊断 → 可以生成一个案例
|
||||
- 通过 diagnosis_id 关联
|
||||
- diagnosis_id 可为空(人工录入案例)
|
||||
|
||||
流程:
|
||||
diagnosis_record(成功)
|
||||
↓
|
||||
用户反馈"有用"
|
||||
↓
|
||||
自动生成 case_library
|
||||
↓
|
||||
后续可人工修正、合并相似案例
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据示例
|
||||
|
||||
### 示例1:外部接口故障案例
|
||||
```sql
|
||||
INSERT INTO case_library VALUES
|
||||
(1, 'case-001', 'diag-001', 'AUTO', 'EXTERNAL_API', '广东', '/api/v1/guangdong/social-security', '40003',
|
||||
'广东社保查询idCard字段缺失',
|
||||
'请求报文中未传入idCard字段,导致参数校验失败',
|
||||
'前端表单增加idCard必填校验;后端增加参数校验提示',
|
||||
15, 'system', NOW(), NOW());
|
||||
```
|
||||
|
||||
### 示例2:内部错误案例
|
||||
```sql
|
||||
INSERT INTO case_library VALUES
|
||||
(2, 'case-002', 'diag-045', 'AUTO', 'INTERNAL_ERROR', 'order-service', 'OrderController.createOrder()', 'NullPointerException',
|
||||
'订单服务创建订单空指针异常',
|
||||
'OrderController.createOrder()方法中user对象为null,未做空判断',
|
||||
'在第45行添加空判断:if (user == null) throw new BizException("用户信息不存在")',
|
||||
8, 'system', NOW(), NOW());
|
||||
```
|
||||
|
||||
### 示例3:人工录入案例
|
||||
```sql
|
||||
INSERT INTO case_library VALUES
|
||||
(3, 'case-003', NULL, 'MANUAL', 'DATABASE', 'mysql-master-01', 'UPDATE orders SET status=? WHERE order_id=?', '1213',
|
||||
'订单库存更新死锁通用处理',
|
||||
'两个事务互相等待对方释放锁',
|
||||
'调整事务加锁顺序:统一先锁订单,再锁库存;或使用乐观锁',
|
||||
3, 'admin', NOW(), NOW());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型查询
|
||||
|
||||
### 精确匹配查询
|
||||
```sql
|
||||
-- 按错误码查询
|
||||
SELECT * FROM case_library
|
||||
WHERE error_code = '40003'
|
||||
ORDER BY reference_count DESC
|
||||
LIMIT 5;
|
||||
|
||||
-- 按故障类别 + 错误码 + 故障目标查询
|
||||
SELECT * FROM case_library
|
||||
WHERE fault_category = 'INTERNAL_ERROR'
|
||||
AND error_code = 'NullPointerException'
|
||||
AND fault_target = 'OrderController.createOrder()'
|
||||
ORDER BY reference_count DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
### 统计分析
|
||||
```sql
|
||||
-- 统计案例分布
|
||||
SELECT
|
||||
fault_category,
|
||||
COUNT(*) as count,
|
||||
AVG(reference_count) as avg_reference
|
||||
FROM case_library
|
||||
GROUP BY fault_category
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- Top 引用案例
|
||||
SELECT title, reference_count, created_at
|
||||
FROM case_library
|
||||
ORDER BY reference_count DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与 Milvus 的配合
|
||||
|
||||
### 混合检索策略
|
||||
|
||||
```
|
||||
1. 精确匹配(MySQL)
|
||||
- 按 error_code 查询
|
||||
- 按 fault_category + fault_source 查询
|
||||
- 优点:快速、准确
|
||||
|
||||
2. 语义检索(Milvus)
|
||||
- 将案例内容向量化
|
||||
- 按语义相似度查询
|
||||
- 优点:能找到相似但不同错误码的案例
|
||||
|
||||
3. 混合策略(推荐)
|
||||
Step 1: 先精确匹配(MySQL)
|
||||
Step 2: 如果结果 < 3 个,补充语义检索(Milvus)
|
||||
Step 3: 合并去重,按 reference_count 排序
|
||||
Step 4: 返回 Top 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据量预估
|
||||
|
||||
```
|
||||
预估:500-1000 条
|
||||
- 初期:每月新增 10-20 条
|
||||
- 稳定期:每月新增 5-10 条
|
||||
- 总量:1-2 年达到稳定
|
||||
|
||||
存储:
|
||||
- 单条记录:约 2KB
|
||||
- 1000 条:约 2MB
|
||||
|
||||
结论:数据量很小
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP 版本的简化
|
||||
|
||||
```
|
||||
Phase 1(当前):
|
||||
✅ 基础字段和表结构
|
||||
✅ 自动生成案例
|
||||
✅ 人工录入案例
|
||||
✅ 按 reference_count 简单排序
|
||||
|
||||
Phase 2(未来增强):
|
||||
❌ useful_count + score(复杂评分)
|
||||
❌ 版本管理
|
||||
❌ 标签分类(tags)
|
||||
❌ 案例合并功能
|
||||
```
|
||||
@@ -1,240 +0,0 @@
|
||||
# diagnosis_record - 诊断记录表
|
||||
|
||||
## 表定位
|
||||
|
||||
**核心业务表**:存储每次诊断任务的完整记录
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 兼容多种故障类型
|
||||
|
||||
**问题背景**:
|
||||
- 初始设计过于聚焦"外部接口故障"
|
||||
- 实际故障类型更丰富:空指针异常、数据库死锁、缓存穿透、线程池耗尽等
|
||||
|
||||
**解决方案**:
|
||||
- 字段泛化:business_id 替代 order_id,fault_source 替代 province
|
||||
- 增加分类:fault_category 显式区分故障类别
|
||||
- 增强错误信息:error_message、stack_trace 支持内部错误
|
||||
|
||||
---
|
||||
|
||||
## 表结构(v2.0)
|
||||
|
||||
```sql
|
||||
CREATE TABLE diagnosis_record (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
diagnosis_id VARCHAR(64) UNIQUE NOT NULL COMMENT '诊断唯一ID(UUID)',
|
||||
|
||||
-- 关联信息
|
||||
session_id VARCHAR(64) COMMENT '会话ID(关联Redis)',
|
||||
business_id VARCHAR(128) COMMENT '业务标识(订单号/请求ID/线程ID/任务ID...)',
|
||||
trace_id VARCHAR(64) COMMENT '链路追踪ID',
|
||||
|
||||
-- 故障分类(泛化设计)
|
||||
fault_category VARCHAR(32) COMMENT '故障类别(EXTERNAL_API/INTERNAL_ERROR/DATABASE/CACHE/NETWORK/THREAD/MEMORY/CONFIG)',
|
||||
fault_source VARCHAR(128) COMMENT '故障源(省份/服务名/类名/数据库实例...)',
|
||||
fault_target VARCHAR(256) COMMENT '故障目标(接口URL/方法名/SQL语句/缓存键...)',
|
||||
|
||||
-- 错误信息(通用)
|
||||
error_code VARCHAR(64) COMMENT '错误码(业务错误码/HTTP状态码/异常类名)',
|
||||
error_message TEXT COMMENT '错误消息',
|
||||
stack_trace TEXT COMMENT '堆栈信息(内部错误时记录)',
|
||||
|
||||
-- 诊断结果
|
||||
problem_type VARCHAR(32) COMMENT '问题类型(参数/网络/权限/逻辑/空指针/死锁...)',
|
||||
root_cause TEXT COMMENT '根因分析',
|
||||
solution TEXT COMMENT '修复方案',
|
||||
report_markdown TEXT COMMENT '完整诊断报告(Markdown格式)',
|
||||
|
||||
-- 评估指标
|
||||
status VARCHAR(16) DEFAULT 'PENDING' COMMENT '诊断状态(PENDING/RUNNING/SUCCESS/FAILED)',
|
||||
confidence INT COMMENT '诊断置信度(0-100)',
|
||||
duration INT COMMENT '诊断耗时(毫秒)',
|
||||
|
||||
-- 用户反馈
|
||||
feedback VARCHAR(16) COMMENT '用户反馈(useful/not_useful/null)TODO: 后续可拆分为独立反馈表',
|
||||
|
||||
-- 调试字段
|
||||
tool_calls JSON COMMENT '工具调用记录',
|
||||
|
||||
-- 元数据
|
||||
created_by VARCHAR(64) COMMENT '创建人',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_business_id (business_id),
|
||||
INDEX idx_trace_id (trace_id),
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_fault_category (fault_category),
|
||||
INDEX idx_fault_source_target (fault_source, fault_target(100)),
|
||||
INDEX idx_error_code (error_code),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诊断记录表(v2.0 泛化版)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字段说明
|
||||
|
||||
### 核心字段
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| diagnosis_id | 诊断唯一标识 | diag-001 |
|
||||
| session_id | 会话ID(支持追问) | sess-abc |
|
||||
| business_id | **泛化**:业务标识 | 订单号/请求ID/线程ID |
|
||||
| trace_id | 链路追踪ID | trace-xyz |
|
||||
|
||||
### 故障分类字段(泛化设计)
|
||||
|
||||
| 字段 | 说明 | 外部接口示例 | 内部错误示例 |
|
||||
|------|------|-------------|-------------|
|
||||
| fault_category | 故障类别 | EXTERNAL_API | INTERNAL_ERROR |
|
||||
| fault_source | 故障源 | 广东 | order-service |
|
||||
| fault_target | 故障目标 | /api/v1/social | OrderController.create() |
|
||||
| error_code | 错误码 | 40003 | NullPointerException |
|
||||
|
||||
### fault_category 枚举值
|
||||
|
||||
```
|
||||
EXTERNAL_API - 外部接口调用失败
|
||||
INTERNAL_ERROR - 系统内部错误(空指针、NPE)
|
||||
DATABASE - 数据库问题(死锁、慢查询)
|
||||
CACHE - 缓存问题(穿透、雪崩)
|
||||
NETWORK - 网络问题(超时、连接失败)
|
||||
THREAD - 线程问题(线程池满、死锁)
|
||||
MEMORY - 内存问题(OOM、内存泄漏)
|
||||
CONFIG - 配置问题(配置错误、缺失)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据示例
|
||||
|
||||
### 示例1:外部接口故障
|
||||
```sql
|
||||
INSERT INTO diagnosis_record VALUES (
|
||||
NULL, 'diag-001', 'sess-abc', '202406150001', 'trace-001',
|
||||
'EXTERNAL_API', '广东', '/api/v1/guangdong/social-security', '40003',
|
||||
'参数缺失:idCard', NULL,
|
||||
'参数问题', 'idCard字段缺失', '补充前端校验', '完整报告...',
|
||||
'SUCCESS', 85, 5234, NULL,
|
||||
NULL, NOW(), NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 示例2:空指针异常
|
||||
```sql
|
||||
INSERT INTO diagnosis_record VALUES (
|
||||
NULL, 'diag-002', 'sess-def', 'req-xyz789', NULL,
|
||||
'INTERNAL_ERROR', 'order-service', 'OrderController.createOrder()', 'NullPointerException',
|
||||
'Cannot invoke "User.getName()" because "user" is null',
|
||||
'java.lang.NullPointerException: ...\n at OrderController.java:45\n ...',
|
||||
'空指针异常', 'createOrder方法中user对象为null', '添加空判断', '完整报告...',
|
||||
'SUCCESS', 90, 3456, NULL,
|
||||
NULL, NOW(), NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 示例3:数据库死锁
|
||||
```sql
|
||||
INSERT INTO diagnosis_record VALUES (
|
||||
NULL, 'diag-003', 'sess-ghi', 'txn-20240615-001', NULL,
|
||||
'DATABASE', 'mysql-master-01', 'UPDATE orders SET status=? WHERE order_id=?', '1213',
|
||||
'Deadlock found when trying to get lock', NULL,
|
||||
'数据库死锁', '两个事务互相等待对方释放锁', '调整事务加锁顺序', '完整报告...',
|
||||
'SUCCESS', 88, 4567, NULL,
|
||||
NULL, NOW(), NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型查询
|
||||
|
||||
### 按故障类别统计
|
||||
```sql
|
||||
SELECT
|
||||
fault_category,
|
||||
COUNT(*) as count,
|
||||
ROUND(AVG(duration), 2) as avg_duration_ms,
|
||||
ROUND(AVG(confidence), 2) as avg_confidence
|
||||
FROM diagnosis_record
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
GROUP BY fault_category
|
||||
ORDER BY count DESC;
|
||||
```
|
||||
|
||||
### 内部错误Top异常
|
||||
```sql
|
||||
SELECT
|
||||
error_code,
|
||||
fault_target,
|
||||
COUNT(*) as count
|
||||
FROM diagnosis_record
|
||||
WHERE fault_category = 'INTERNAL_ERROR'
|
||||
AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
GROUP BY error_code, fault_target
|
||||
ORDER BY count DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### 诊断成功率
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) as success,
|
||||
ROUND(SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
|
||||
FROM diagnosis_record
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心设计决策
|
||||
|
||||
### 1. 一次诊断 = 一条记录
|
||||
- 用户发起一次诊断任务,创建一条记录
|
||||
- 不是聊天记录(不存多轮对话)
|
||||
- 追问对话上下文暂存 Redis(30分钟过期)
|
||||
|
||||
### 2. report_markdown 字段的必要性
|
||||
- 固化结果:Prompt变化不影响历史报告
|
||||
- 快速展示:不需要重新生成
|
||||
- 历史审计:可以看到当时的诊断结果
|
||||
|
||||
### 3. 字段泛化的好处
|
||||
- 支持多种故障类型(不限于外部接口)
|
||||
- 灵活填写(根据故障类型选择字段值)
|
||||
- 易于扩展(新增故障类型只需增加枚举值)
|
||||
|
||||
---
|
||||
|
||||
## 数据量预估
|
||||
|
||||
```
|
||||
场景:中型企业运维团队
|
||||
- 日均诊断:100 次
|
||||
- 月均诊断:3000 次
|
||||
- 年均诊断:36000 次
|
||||
|
||||
存储预估:
|
||||
- 单条记录:约 5KB(含报告)
|
||||
- 年存储量:36000 × 5KB = 180MB
|
||||
- 三年存储:540MB
|
||||
|
||||
结论:数据量不大,可以全量保留
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| v1.0 | 2024-06-15 | 初版,基础字段 |
|
||||
| v2.0 | 2024-06-22 | 字段泛化,支持多种故障类型 |
|
||||
@@ -0,0 +1,40 @@
|
||||
# ChatModel + Embedding 解耦 Design
|
||||
|
||||
## 架构摘要
|
||||
|
||||
当前代码直接使用 DashScope 具体实现类 → 改为面向 Spring AI 抽象接口编程,通过 Spring Boot 自动注入切换实现。
|
||||
|
||||
## 关键决策
|
||||
|
||||
- ChatModel:Spring Boot Starter 自动注册 Bean,通过 `@Autowired ChatModel` 注入,不再手动工厂创建
|
||||
- EmbeddingModel:Spring Boot Starter 自动注册 Bean,通过 `@Autowired EmbeddingModel` 注入,替代 DashScope TextEmbedding SDK
|
||||
- RagService 流式对话:用 `ChatModel.stream(Prompt)` 返回 `Flux<ChatResponse>` 替代 DashScope Generation
|
||||
- VECTOR_DIM:从 `application.yml` 配置读取,替代 `MilvusConstants.VECTOR_DIM` 常量
|
||||
|
||||
## 模块地图
|
||||
|
||||
| 模块 | 职责 | 改动 |
|
||||
| --- | --- | --- |
|
||||
| ChatService | 封装 ChatModel + ReactAgent | 删除工厂方法,注入 ChatModel |
|
||||
| ChatController | HTTP API 入口 | 删除 DashScope import,使用注入 ChatModel |
|
||||
| AiOpsService | 多 Agent 协作 | DashScopeChatModel → ChatModel |
|
||||
| VectorEmbeddingService | 向量化 | DashScope SDK → EmbeddingModel 接口 |
|
||||
| RagService | RAG 流式对话 | DashScope Generation → ChatModel.stream() |
|
||||
| MilvusConstants | Milvus 常量 | VECTOR_DIM 改为配置化 |
|
||||
| MilvusProperties | Milvus 配置 | 新增 vectorDim 字段 |
|
||||
| application.yml | 配置 | 新增 vector-dim 配置项 |
|
||||
|
||||
## 接口影响
|
||||
|
||||
- 级别:L2 内部接口(所有消费者在同一实现范围内)
|
||||
- 判级原因:方法签名从具体类改为接口,调用方需同步修改,但都在本项目内
|
||||
- 不改变外部 API(/api/chat, /api/chat_stream, /api/ai_ops 的 HTTP 响应不变)
|
||||
|
||||
## 架构风险
|
||||
|
||||
- RagService 流式适配最复杂:DashScope Generation 返回 Flowable<GenerationResult>,Spring AI ChatModel.stream() 返回 Flux<ChatResponse>,需适配 StreamCallback 接口
|
||||
- 缓解:Spring AI 的 Flux 与项目已有的 SSE 推送逻辑天然兼容
|
||||
- ChatModel Bean 冲突:多 starter 并存时需 @Primary 或条件注解区分默认实现
|
||||
- 缓解:当前只保留 DashScope starter,不引入多 starter;未来切换时删除旧 starter 即可
|
||||
- DashScopeConfig 通用性:`spring.ai.dashscope.chat.options.timeout` 是厂商绑定配置键
|
||||
- 缓解:本次保留该配置(只做解耦不换实现);换模型时改配置键
|
||||
@@ -0,0 +1,49 @@
|
||||
# ChatModel + Embedding 解耦 Proposal
|
||||
|
||||
## 问题
|
||||
|
||||
项目 5 个 Java 文件硬编码 DashScope 具体实现类,而非 Spring AI 抽象接口:
|
||||
|
||||
- ChatService/ChatController/AiOpsService:方法签名用 `DashScopeChatModel` 而非 `ChatModel`
|
||||
- VectorEmbeddingService:完全绕过 Spring AI,直接用 DashScope SDK 的 `TextEmbedding`
|
||||
- RagService:完全绕过 Spring AI,直接用 DashScope SDK 的 `Generation`(流式对话)
|
||||
|
||||
导致替换 LLM 或 Embedding 模型需要改代码而非改配置。
|
||||
|
||||
## 建议方案
|
||||
|
||||
**面向 Spring AI 报表接口编程**:
|
||||
- Chat 部分:`DashScopeChatModel` → `ChatModel` 接口,通过 Spring Boot 自动注入
|
||||
- Embedding 部分:DashScope SDK `TextEmbedding` → Spring AI `EmbeddingModel` 接口
|
||||
- RagService 流式对话:DashScope SDK `Generation` → Spring AI `ChatModel` 流式接口 (`stream()`)
|
||||
|
||||
通过 Spring Boot Starter + `application.yml` 配置切换模型实现,无需改代码。
|
||||
|
||||
## 范围
|
||||
|
||||
- 本次要做:
|
||||
- ChatService:删除 `createDashScopeApi()` / `createChatModel()` 工厂方法,改为注入 `ChatModel`
|
||||
- ChatController:删除 DashScope import 和手动构建,改为使用注入的 `ChatModel`
|
||||
- AiOpsService:方法签名 `DashScopeChatModel` → `ChatModel`
|
||||
- VectorEmbeddingService:DashScope SDK → Spring AI `EmbeddingModel`
|
||||
- RagService:DashScope SDK `Generation` → Spring AI `ChatModel` stream
|
||||
- DashScopeConfig:通用化配置(保留 DashScope starter 配置,但代码层不再硬编码 DashScope 类)
|
||||
- application.yml:保持现有 DashScope 配置,增加模型切换说明
|
||||
|
||||
- 本次不做:
|
||||
- 不替换 DashScope 为其他提供商(只做解耦,不换实现)
|
||||
- 不修改 Agent Framework 本身
|
||||
- 不改 Milvus 相关代码
|
||||
- 不改 MCP 客户端配置
|
||||
|
||||
## 关键约束
|
||||
|
||||
- ReactAgent.builder().model() 已接受 ChatModel 接口(已验证)
|
||||
- Spring AI 的 EmbeddingModel 接口可替代 DashScope TextEmbedding
|
||||
- Spring AI 的 ChatModel.stream() 可替代 DashScope Generation 流式接口
|
||||
- DashScope starter 仍需保留作为默认实现(通过 pom 依赖 + yml 配置)
|
||||
|
||||
## 风险
|
||||
|
||||
- RagService 流式对话的迁移可能最复杂:DashScope SDK 返回 RxJava Flowable,Spring AI ChatModel.stream() 返回 Flux,需要适配 SSE 推送逻辑
|
||||
- VectorEmbeddingService 维度可能变化:DashScope text-embedding-v4 输出 1024 维,替换模型后维度不同,需要同步修改 Milvus VECTOR_DIM 常量
|
||||
@@ -0,0 +1,24 @@
|
||||
# ChatModel + Embedding 解耦 Specs
|
||||
|
||||
## 可观察行为规格
|
||||
|
||||
### S1: Chat 接口不变
|
||||
- `/api/chat`, `/api/chat_stream`, `/api/ai_ops` 的 HTTP 入参/出参/响应结构完全不变
|
||||
- 功能行为不变:工具调用、Agent 协作、SSE 流式推送照旧工作
|
||||
|
||||
### S2: 模型切换只需改配置
|
||||
- 替换 DashScope starter 为 OpenAI starter + 改 yml 配置 → ChatModel 自动注入不同实现
|
||||
- 替换 embedding 模型只需改 yml 的 `dashscope.embedding.model` 和 `milvus.vector-dim`
|
||||
- 不需要改任何 Java 代码
|
||||
|
||||
### S3: VECTOR_DIM 从配置读取
|
||||
- `MilvusClientFactory.createBizCollection()` 使用 MilvusProperties.getVectorDim() 而非 MilvusConstants.VECTOR_DIM
|
||||
- 切换 embedding 模型后改 yml 的 `milvus.vector-dim` 即可适配新维度
|
||||
|
||||
### S4: VectorEmbeddingService 行为不变
|
||||
- generateEmbedding/generateEmbeddings/generateQueryVector 的签名和返回类型不变
|
||||
- 内部实现从 DashScope SDK 切换到 Spring AI EmbeddingModel
|
||||
|
||||
### S5: RagService 流式对话行为不变
|
||||
- queryStream 方法签名和 StreamCallback 接口不变
|
||||
- 内部实现从 DashScope Generation 切换到 Spring AI ChatModel.stream()
|
||||
@@ -0,0 +1,22 @@
|
||||
# ChatModel + Embedding 解耦 Tasks
|
||||
|
||||
## 需求追踪
|
||||
|
||||
| 需求 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| ChatService 解耦 DashScopeChatModel | 待处理 | 改为注入 ChatModel |
|
||||
| ChatController 解耦 DashScope | 待处理 | 删除手动构建逻辑 |
|
||||
| AiOpsService 解耦 DashScopeChatModel | 待处理 | 方法签名改为 ChatModel |
|
||||
| VectorEmbeddingService 解耦 DashScope SDK | 待处理 | 改为注入 EmbeddingModel |
|
||||
| RagService 解耦 DashScope Generation | 待处理 | 改为 ChatModel.stream() |
|
||||
| VECTOR_DIM 配置化 | 待处理 | 从 yml 读取 |
|
||||
|
||||
## 实现任务
|
||||
|
||||
- [ ] T1: MilvusProperties 新增 vectorDim 字段 + getter/setter,application.yml 新增 `milvus.vector-dim: 1024`
|
||||
- [ ] T2: MilvusConstants.VECTOR_DIM 改为从 MilvusProperties 动态读取(MilvusClientFactory 传入)
|
||||
- [ ] T3: ChatService — 删除 createDashScopeApi/createChatModel/createStandardChatModel,新增 @Autowired ChatModel;createReactAgent 参数改为 ChatModel
|
||||
- [ ] T4: ChatController — 删除 DashScope import 和手动构建(行83-84, 171-172, 292-301),改为使用注入 ChatModel 或 ChatService 传入
|
||||
- [ ] T5: AiOpsService — executeAiOpsAnalysis/buildPlannerAgent/buildExecutorAgent 参数类型 DashScopeChatModel → ChatModel
|
||||
- [ ] T6: VectorEmbeddingService — 删除 DashScope SDK import + TextEmbedding 字段 + @PostConstruct init(),改为 @Autowired EmbeddingModel;generateEmbedding 改为调用 EmbeddingModel.embed()
|
||||
- [ ] T7: RagService — 删除 DashScope SDK import + Generation 字段 + Constants.apiKey,改为 @Autowired ChatModel;generateAnswerStream 改为 ChatModel.stream(Prompt) + Flux 适配 StreamCallback
|
||||
@@ -1 +0,0 @@
|
||||
COMMITTED
|
||||
@@ -1,112 +0,0 @@
|
||||
# Phase 1 Infrastructure - Decisions Log
|
||||
|
||||
## Grill 阶段澄清记录
|
||||
|
||||
### 2026-06-23
|
||||
|
||||
#### Q1: SessionContext 字段设计
|
||||
**问题**: Redis 会话需要存储哪些字段?
|
||||
|
||||
**决策**:
|
||||
```java
|
||||
class SessionContext {
|
||||
String sessionId;
|
||||
String diagnosisId;
|
||||
String currentStep;
|
||||
Map<String, Object> collectedEvidence;
|
||||
List<ToolCall> toolCallHistory;
|
||||
String intentType; // 预留 Phase 2 意图识别
|
||||
LocalDateTime createdAt;
|
||||
LocalDateTime lastAccessAt;
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 支持多轮对话恢复上下文
|
||||
- intentType 预留 Phase 2,避免后续修改结构
|
||||
- tool_calls 同时存 Redis(临时)和 MySQL(持久)
|
||||
|
||||
**用户确认**: 已确认
|
||||
|
||||
---
|
||||
|
||||
#### Q2: 包名重构策略
|
||||
**问题**: org.example → com.superbiz.agent 是否需要兼容层?
|
||||
|
||||
**决策**: 直接全量替换,不保留兼容层
|
||||
|
||||
**理由**:
|
||||
- 内部项目,无外部依赖者
|
||||
- 兼容层增加复杂度
|
||||
- MVP 阶段保持简单
|
||||
|
||||
**用户确认**: 已确认
|
||||
|
||||
---
|
||||
|
||||
#### Q3: Redis 降级策略
|
||||
**问题**: Redis 故障时如何处理?
|
||||
|
||||
**决策**: Phase 1 不做降级,Redis 故障直接失败
|
||||
|
||||
**理由**:
|
||||
- MVP 优先跑通核心流程
|
||||
- 降级策略增加复杂度
|
||||
- 单元测试可用内存 Mock
|
||||
|
||||
**备选方案** (Phase 2/3):
|
||||
- 自动降级到内存实现
|
||||
- 返回友好错误提示
|
||||
|
||||
**用户确认**: 已确认(先跑通 MVP)
|
||||
|
||||
---
|
||||
|
||||
## Evidence-Driven 查证结果
|
||||
|
||||
### 诊断记录 vs 案例的边界
|
||||
**查证文件**: docs/tables/diagnosis_record.md, docs/tables/case_library.md
|
||||
|
||||
**结论**:
|
||||
- 诊断记录:每次诊断都记录
|
||||
- 案例:从诊断记录中筛选(成功诊断 + 用户反馈 useful)
|
||||
- 转换触发:diagnosis_record.feedback = 'useful' + confidence >= 80
|
||||
|
||||
**状态**: 已查证,边界清晰
|
||||
|
||||
---
|
||||
|
||||
### 文档范围
|
||||
**查证文件**: docs/tables/api_document.md
|
||||
|
||||
**结论**:
|
||||
- Phase 1: 只处理接口文档(API 文档、错误码说明)
|
||||
- Phase 2/3: 可扩展为其他类型(运维手册、FAQ)
|
||||
|
||||
**状态**: 已查证,范围明确
|
||||
|
||||
---
|
||||
|
||||
### 单元测试覆盖率标准
|
||||
**查证文件**: docs/architecture/implementation-detail.md
|
||||
|
||||
**结论**:
|
||||
- 目标:行覆盖率 70%+
|
||||
- Repository: 100%
|
||||
- Service: 80%+
|
||||
- Tool: 80%+
|
||||
- Controller: 70%+
|
||||
|
||||
**状态**: 已查证,标准明确
|
||||
|
||||
---
|
||||
|
||||
## 待写入 CONTEXT.md 的术语
|
||||
|
||||
无新增术语。现有术语已在 docs/ 中定义清楚。
|
||||
|
||||
---
|
||||
|
||||
## 待创建 ADR
|
||||
|
||||
无。Phase 1 都是标准技术选型,无需 ADR。
|
||||
@@ -1,267 +0,0 @@
|
||||
# Phase 1 Infrastructure - Design
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 数据持久化层
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (Service / Controller / Agent) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Repository Layer (JPA) │
|
||||
│ - DiagnosisRecordRepository │
|
||||
│ - CaseLibraryRepository │
|
||||
│ - ApiDocumentRepository │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ MySQL 8.0+ │
|
||||
│ - diagnosis_record (诊断记录) │
|
||||
│ - case_library (案例库) │
|
||||
│ - api_document (文档元数据) │
|
||||
│ - flyway_schema_history (版本管理) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Flyway 迁移流程**:
|
||||
1. 启动时自动扫描 `db/migration/V*.sql`
|
||||
2. 检查 `flyway_schema_history` 表
|
||||
3. 执行未运行的脚本
|
||||
4. 记录版本号
|
||||
|
||||
### 2. 会话管理层
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Diagnosis Flow │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SessionManager (Interface) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ RedisSessionManager (Impl) │
|
||||
│ - get(sessionId): SessionContext │
|
||||
│ - save(context): void │
|
||||
│ - delete(sessionId): void │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Redis 6.0+ │
|
||||
│ Key: session:{sessionId} │
|
||||
│ Value: SessionContext (JSON) │
|
||||
│ TTL: 30 minutes │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**SessionContext 结构**:
|
||||
```java
|
||||
{
|
||||
"sessionId": "uuid",
|
||||
"diagnosisId": "uuid",
|
||||
"currentStep": "queryOrder",
|
||||
"collectedEvidence": {
|
||||
"orderInfo": {...},
|
||||
"logs": [...]
|
||||
},
|
||||
"toolCallHistory": [
|
||||
{
|
||||
"toolName": "queryOrder",
|
||||
"params": {...},
|
||||
"result": {...},
|
||||
"timestamp": "2026-06-23T10:00:00"
|
||||
}
|
||||
],
|
||||
"intentType": "诊断",
|
||||
"createdAt": "2026-06-23T09:55:00",
|
||||
"lastAccessAt": "2026-06-23T10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 包结构设计
|
||||
|
||||
```
|
||||
com.superbiz.agent/
|
||||
├── SuperBizAgentApplication.java # 启动类
|
||||
│
|
||||
├── controller/ # REST 控制器
|
||||
│ ├── DiagnosisController.java
|
||||
│ ├── DocumentController.java
|
||||
│ └── CaseController.java
|
||||
│
|
||||
├── service/ # 业务服务
|
||||
│ ├── DiagnosisService.java
|
||||
│ ├── DocumentService.java
|
||||
│ ├── CaseService.java
|
||||
│ ├── TextExtractor.java # 文本提取
|
||||
│ └── VectorService.java # 向量化服务
|
||||
│
|
||||
├── repository/ # 数据访问
|
||||
│ ├── DiagnosisRecordRepository.java
|
||||
│ ├── CaseLibraryRepository.java
|
||||
│ └── ApiDocumentRepository.java
|
||||
│
|
||||
├── domain/ # 领域模型
|
||||
│ ├── entity/ # JPA 实体
|
||||
│ │ ├── DiagnosisRecord.java
|
||||
│ │ ├── CaseLibrary.java
|
||||
│ │ └── ApiDocument.java
|
||||
│ ├── dto/ # 数据传输对象
|
||||
│ │ ├── DiagnosisRequest.java
|
||||
│ │ ├── DiagnosisResponse.java
|
||||
│ │ ├── DocumentUploadRequest.java
|
||||
│ │ └── DocumentQueryResponse.java
|
||||
│ └── enums/ # 枚举
|
||||
│ ├── FaultCategory.java
|
||||
│ ├── DiagnosisStatus.java
|
||||
│ └── SourceType.java
|
||||
│
|
||||
├── session/ # 会话管理
|
||||
│ ├── SessionManager.java # 接口
|
||||
│ ├── RedisSessionManager.java # Redis 实现
|
||||
│ ├── SessionContext.java # 会话上下文
|
||||
│ └── ToolCall.java # 工具调用记录
|
||||
│
|
||||
├── tool/ # 工具层
|
||||
│ ├── DocumentSearchTool.java # 混合检索
|
||||
│ └── (其他 tool 保留 Phase 2)
|
||||
│
|
||||
├── config/ # 配置
|
||||
│ ├── JpaConfig.java
|
||||
│ ├── RedisConfig.java
|
||||
│ ├── MilvusConfig.java # 保留现有
|
||||
│ └── DashScopeConfig.java # 保留现有
|
||||
│
|
||||
└── exception/ # 异常处理
|
||||
├── GlobalExceptionHandler.java
|
||||
├── SessionNotFoundException.java
|
||||
└── DocumentProcessException.java
|
||||
```
|
||||
|
||||
### 4. 文档管理流程
|
||||
|
||||
```
|
||||
文档上传流程:
|
||||
User → POST /api/documents/upload
|
||||
↓
|
||||
DocumentController.upload()
|
||||
↓
|
||||
DocumentService.uploadDocument()
|
||||
↓ (并行)
|
||||
├─→ TextExtractor.extract() # 提取文本
|
||||
├─→ chunkText() # 分块
|
||||
├─→ VectorService.embed() # 向量化
|
||||
├─→ ApiDocumentRepository.save() # 存 MySQL
|
||||
└─→ MilvusClient.insert() # 存 Milvus
|
||||
↓
|
||||
返回 document_id
|
||||
```
|
||||
|
||||
```
|
||||
混合检索流程:
|
||||
Agent → DocumentSearchTool.search(errorCode, province)
|
||||
↓
|
||||
├─→ MySQL 精确匹配
|
||||
│ SELECT * FROM api_document
|
||||
│ WHERE error_code = ? AND province = ?
|
||||
│
|
||||
├─→ Milvus 语义检索
|
||||
│ 向量化查询 → 相似度搜索 → Top 10
|
||||
│
|
||||
└─→ RRF 融合排序
|
||||
(精确匹配优先 + 语义补漏)
|
||||
↓
|
||||
返回 Top 3 文档片段
|
||||
```
|
||||
|
||||
### 5. 数据库配置
|
||||
|
||||
**application.yml 新增**:
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/superbiz_agent?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:your-password}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate # 生产用 validate,Flyway 管理表结构
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 0
|
||||
timeout: 3000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
```
|
||||
|
||||
### 6. 测试策略
|
||||
|
||||
**Repository 测试**:
|
||||
- 使用 @DataJpaTest + H2 内存数据库
|
||||
- 测试 CRUD + 自定义查询
|
||||
|
||||
**Service 测试**:
|
||||
- 使用 @SpringBootTest + Mockito
|
||||
- Mock Repository 和外部依赖
|
||||
|
||||
**Controller 测试**:
|
||||
- 使用 @WebMvcTest + MockMvc
|
||||
- Mock Service 层
|
||||
|
||||
**集成测试**:
|
||||
- 使用 @SpringBootTest + Testcontainers(可选)
|
||||
- 测试完整流程
|
||||
|
||||
## 技术决策
|
||||
|
||||
### Flyway vs Liquibase
|
||||
**选择**:Flyway
|
||||
|
||||
**理由**:
|
||||
- 更简单,SQL-first
|
||||
- Spring Boot 官方推荐
|
||||
- 社区活跃
|
||||
|
||||
### Jackson vs Gson
|
||||
**选择**:Jackson(Spring Boot 默认)
|
||||
|
||||
**理由**:
|
||||
- Spring Boot 内置
|
||||
- 性能更好
|
||||
- 与 Spring MVC 集成好
|
||||
|
||||
### Lettuce vs Jedis
|
||||
**选择**:Lettuce(Spring Data Redis 默认)
|
||||
|
||||
**理由**:
|
||||
- 异步支持
|
||||
- 线程安全
|
||||
- Spring Boot 默认
|
||||
@@ -1,171 +0,0 @@
|
||||
# Proposal: Phase 1 基础设施搭建
|
||||
|
||||
## 问题
|
||||
|
||||
当前项目是一个 Demo,需要改造为 MVP 诊断 Agent 系统。Phase 1 需要搭建基础设施:
|
||||
- 缺少持久化层(MySQL + JPA)
|
||||
- 缺少分布式会话管理(Redis)
|
||||
- 代码结构需要重构(包名、分层)
|
||||
- 缺少文档管理基础功能
|
||||
|
||||
## 建议方案
|
||||
|
||||
### 1. 数据持久化
|
||||
|
||||
**方案**:Spring Data JPA + MySQL + Flyway
|
||||
|
||||
**理由**:
|
||||
- JPA 是 Spring Boot 标准持久化方案
|
||||
- Flyway 管理数据库版本,团队协作友好
|
||||
- 3 张表设计已完成(docs/tables/)
|
||||
|
||||
**实现**:
|
||||
1. 添加依赖(spring-boot-starter-data-jpa, mysql-connector-j, flyway-core)
|
||||
2. 创建 3 个 Flyway 迁移脚本(V001/V002/V003)
|
||||
3. 创建 JPA 实体类(DiagnosisRecord, CaseLibrary, ApiDocument)
|
||||
4. 创建 Repository 接口(继承 JpaRepository)
|
||||
|
||||
### 2. 会话管理
|
||||
|
||||
**方案**:Redis 替代内存 HashMap
|
||||
|
||||
**理由**:
|
||||
- 支持分布式部署
|
||||
- 自动 TTL(30 分钟)
|
||||
- Spring Data Redis 集成简单
|
||||
|
||||
**实现**:
|
||||
1. 添加 spring-boot-starter-data-redis 依赖
|
||||
2. 创建 SessionManager 接口 + RedisSessionManager 实现
|
||||
3. SessionContext 使用 JSON 序列化
|
||||
|
||||
### 3. 代码结构重构
|
||||
|
||||
**方案**:包名重构 + 分层优化 + DTO 抽离
|
||||
|
||||
**包名重构**:
|
||||
- `org.example` → `com.superbiz.agent`
|
||||
- 工具:IDEA Refactor → Rename Package
|
||||
|
||||
**分层结构**:
|
||||
```
|
||||
com.superbiz.agent/
|
||||
├── controller/ # REST API
|
||||
├── service/ # 业务逻辑
|
||||
├── repository/ # 数据访问
|
||||
├── domain/
|
||||
│ ├── entity/ # JPA 实体
|
||||
│ ├── dto/ # DTO
|
||||
│ └── enums/ # 枚举
|
||||
├── agent/ # Agent 层(Phase 2)
|
||||
├── tool/ # 工具层
|
||||
├── session/ # 会话管理
|
||||
└── config/ # 配置
|
||||
```
|
||||
|
||||
**DTO 抽离**:
|
||||
- Controller 不直接依赖 Entity
|
||||
- 使用 MapStruct 做对象转换
|
||||
|
||||
### 4. 文档管理
|
||||
|
||||
**方案**:CRUD + Milvus 向量同步
|
||||
|
||||
**功能**:
|
||||
1. 上传接口:文件 → 文本提取 → 分块 → 向量化 → MySQL + Milvus
|
||||
2. 查询接口:分页、过滤
|
||||
3. 删除接口:MySQL + Milvus 同步删除
|
||||
4. 检索工具:精确匹配(MySQL)+ 语义检索(Milvus)+ RRF 融合
|
||||
|
||||
## 范围
|
||||
|
||||
**包含**:
|
||||
- Day 1-2: MySQL 表 + JPA + Repository + Redis 会话
|
||||
- Day 3: 包名重构 + 分层优化 + DTO 抽离
|
||||
- Day 4-5: 文档管理 4 个接口 + 混合检索工具
|
||||
|
||||
**不包含**:
|
||||
- Agent 功能(Phase 2)
|
||||
- 意图识别和 RAG(Phase 2)
|
||||
- Verifier 和 Harness(Phase 3)
|
||||
|
||||
## 非目标
|
||||
|
||||
- 性能优化(后续优化)
|
||||
- 完整的权限控制(MVP 不需要)
|
||||
- 前端界面(只做后端 API)
|
||||
|
||||
## 来自 devflow 的上下文约束
|
||||
|
||||
无(这是首个 OpenSpec,devflow 目录为空)
|
||||
|
||||
## 风险
|
||||
|
||||
1. **包名重构影响范围大**
|
||||
- 缓解:先提交当前代码,独立分支重构
|
||||
- 验证:重构后编译通过 + 启动成功
|
||||
|
||||
2. **Flyway 首次运行可能失败**
|
||||
- 缓解:本地 MySQL 先手动测试
|
||||
- 回退:Flyway 支持 repair 修复
|
||||
|
||||
3. **Redis 本地环境依赖**
|
||||
- 缓解:提供 Docker Compose 配置
|
||||
- 回退:可降级为内存实现(测试用)
|
||||
|
||||
## 关键假设
|
||||
|
||||
1. MySQL 8.0+ 和 Redis 6.0+ 可用(本地或 Docker)
|
||||
2. 现有 Milvus 集成不需要改动
|
||||
3. 单元测试覆盖率目标:70%+
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 3 张表创建成功,索引完整
|
||||
2. ✅ Repository 层单元测试通过
|
||||
3. ✅ Redis 会话存取正常,TTL 生效
|
||||
4. ✅ 包名重构完成,编译通过
|
||||
5. ✅ 文档上传/查询/删除接口可用
|
||||
6. ✅ 混合检索工具返回正确结果
|
||||
7. ✅ 整体测试覆盖率 ≥ 70%
|
||||
|
||||
## 产出文件(预期)
|
||||
|
||||
**数据库迁移**:
|
||||
- `src/main/resources/db/migration/V001__create_diagnosis_record.sql`
|
||||
- `src/main/resources/db/migration/V002__create_case_library.sql`
|
||||
- `src/main/resources/db/migration/V003__create_api_document.sql`
|
||||
|
||||
**实体类**:
|
||||
- `com.superbiz.agent.domain.entity.DiagnosisRecord`
|
||||
- `com.superbiz.agent.domain.entity.CaseLibrary`
|
||||
- `com.superbiz.agent.domain.entity.ApiDocument`
|
||||
|
||||
**Repository**:
|
||||
- `com.superbiz.agent.repository.DiagnosisRecordRepository`
|
||||
- `com.superbiz.agent.repository.CaseLibraryRepository`
|
||||
- `com.superbiz.agent.repository.ApiDocumentRepository`
|
||||
|
||||
**会话管理**:
|
||||
- `com.superbiz.agent.session.SessionManager`
|
||||
- `com.superbiz.agent.session.RedisSessionManager`
|
||||
- `com.superbiz.agent.session.SessionContext`
|
||||
|
||||
**文档管理**:
|
||||
- `com.superbiz.agent.controller.DocumentController`
|
||||
- `com.superbiz.agent.service.DocumentService`
|
||||
- `com.superbiz.agent.service.TextExtractor`
|
||||
- `com.superbiz.agent.tool.DocumentSearchTool`
|
||||
|
||||
**配置**:
|
||||
- `pom.xml`(增加依赖)
|
||||
- `application.yml`(增加 MySQL + Redis 配置)
|
||||
|
||||
**测试**:
|
||||
- `*RepositoryTest.java`
|
||||
- `*ServiceTest.java`
|
||||
- `*ControllerTest.java`
|
||||
|
||||
## 工期估算
|
||||
|
||||
5 天(按实施计划)
|
||||
@@ -1,312 +0,0 @@
|
||||
# Phase 1 Infrastructure - Specifications
|
||||
|
||||
## 功能规格
|
||||
|
||||
### 1. 数据库表创建
|
||||
|
||||
#### 1.1 diagnosis_record 表
|
||||
**输入**:Flyway 迁移脚本 V001
|
||||
**输出**:MySQL 表创建成功
|
||||
**验收标准**:
|
||||
- ✅ 表结构与 docs/tables/diagnosis_record.md 一致
|
||||
- ✅ 所有索引创建成功
|
||||
- ✅ JSON 字段类型正确
|
||||
- ✅ 默认值和注释完整
|
||||
|
||||
#### 1.2 case_library 表
|
||||
**输入**:Flyway 迁移脚本 V002
|
||||
**输出**:MySQL 表创建成功
|
||||
**验收标准**:
|
||||
- ✅ 表结构与 docs/tables/case_library.md 一致
|
||||
- ✅ 外键约束正确
|
||||
- ✅ 索引覆盖查询场景
|
||||
|
||||
#### 1.3 api_document 表
|
||||
**输入**:Flyway 迁移脚本 V003
|
||||
**输出**:MySQL 表创建成功
|
||||
**验收标准**:
|
||||
- ✅ 表结构与 docs/tables/api_document.md 一致
|
||||
- ✅ province 和 category 索引就绪
|
||||
|
||||
---
|
||||
|
||||
### 2. JPA 实体与 Repository
|
||||
|
||||
#### 2.1 DiagnosisRecord 实体
|
||||
**字段映射**:
|
||||
- `@Id @GeneratedValue` - id
|
||||
- `@Column(unique=true)` - diagnosis_id
|
||||
- `@JdbcTypeCode(SqlTypes.JSON)` - tool_calls
|
||||
- `@Enumerated(EnumType.STRING)` - fault_category, status
|
||||
- `LocalDateTime` - created_at, updated_at
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 所有字段与数据库一致
|
||||
- ✅ JSON 字段序列化正确
|
||||
- ✅ 枚举映射正确
|
||||
- ✅ Lombok 注解完整
|
||||
|
||||
#### 2.2 Repository 查询方法
|
||||
**DiagnosisRecordRepository**:
|
||||
```java
|
||||
Optional<DiagnosisRecord> findByDiagnosisId(String diagnosisId);
|
||||
Optional<DiagnosisRecord> findByBusinessId(String businessId);
|
||||
Optional<DiagnosisRecord> findByTraceId(String traceId);
|
||||
List<DiagnosisRecord> findByFaultCategoryAndErrorCode(
|
||||
FaultCategory category, String errorCode);
|
||||
Page<DiagnosisRecord> findByCreatedAtBetween(
|
||||
LocalDateTime start, LocalDateTime end, Pageable pageable);
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 单元测试通过(@DataJpaTest + H2)
|
||||
- ✅ 分页查询正确
|
||||
- ✅ 复杂查询性能可接受(< 100ms)
|
||||
|
||||
---
|
||||
|
||||
### 3. Redis 会话管理
|
||||
|
||||
#### 3.1 SessionManager 接口
|
||||
```java
|
||||
public interface SessionManager {
|
||||
SessionContext get(String sessionId);
|
||||
void save(SessionContext context);
|
||||
void delete(String sessionId);
|
||||
boolean exists(String sessionId);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 RedisSessionManager 实现
|
||||
**存储格式**:
|
||||
- Key: `session:{sessionId}`
|
||||
- Value: SessionContext 的 JSON 字符串
|
||||
- TTL: 1800 秒(30 分钟)
|
||||
|
||||
**异常处理**:
|
||||
- Redis 连接失败 → 抛出 RedisConnectionException
|
||||
- 序列化失败 → 抛出 SessionSerializationException
|
||||
- Session 不存在 → 返回 null(get 方法)
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 存取删操作成功
|
||||
- ✅ TTL 自动刷新(每次 get/save)
|
||||
- ✅ JSON 序列化/反序列化正确
|
||||
- ✅ 单元测试覆盖(Mock RedisTemplate)
|
||||
|
||||
---
|
||||
|
||||
### 4. 文档管理
|
||||
|
||||
#### 4.1 文档上传接口
|
||||
**接口**:`POST /api/documents/upload`
|
||||
|
||||
**请求**:
|
||||
```json
|
||||
{
|
||||
"file": "multipart/form-data",
|
||||
"province": "广东",
|
||||
"category": "社保接口"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "上传成功",
|
||||
"data": {
|
||||
"documentId": "uuid",
|
||||
"fileName": "社保接口文档.docx",
|
||||
"chunkCount": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**处理流程**:
|
||||
1. 文件类型校验(.txt, .md, .docx, .pdf)
|
||||
2. 文本提取
|
||||
3. 分块(chunk_size=500, overlap=50)
|
||||
4. DashScope 向量化
|
||||
5. MySQL 存元数据
|
||||
6. Milvus 存向量
|
||||
|
||||
**错误处理**:
|
||||
- 文件类型不支持 → 400 Bad Request
|
||||
- 文件大小超限(10MB) → 413 Payload Too Large
|
||||
- 向量化失败 → 500 Internal Server Error(回滚 MySQL)
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 支持 .txt, .md, .docx, .pdf
|
||||
- ✅ MySQL + Milvus 事务一致
|
||||
- ✅ 单元测试覆盖
|
||||
|
||||
#### 4.2 文档查询接口
|
||||
**接口**:`GET /api/documents?province=广东&category=社保接口&page=0&size=10`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"content": [
|
||||
{
|
||||
"documentId": "uuid",
|
||||
"fileName": "社保接口文档.docx",
|
||||
"province": "广东",
|
||||
"category": "社保接口",
|
||||
"createdAt": "2026-06-23T10:00:00"
|
||||
}
|
||||
],
|
||||
"totalElements": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 分页正确
|
||||
- ✅ 过滤生效
|
||||
- ✅ 性能可接受(< 100ms)
|
||||
|
||||
#### 4.3 文档删除接口
|
||||
**接口**:`DELETE /api/documents/{documentId}`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
**处理流程**:
|
||||
1. 删除 MySQL 记录
|
||||
2. 根据 document_id 删除 Milvus 向量
|
||||
|
||||
**事务性**:
|
||||
- MySQL 删除失败 → 不删除 Milvus
|
||||
- Milvus 删除失败 → 记录日志(容忍)
|
||||
|
||||
**验收标准**:
|
||||
- ✅ MySQL 记录删除
|
||||
- ✅ Milvus 向量删除
|
||||
- ✅ 幂等性(重复删除不报错)
|
||||
|
||||
#### 4.4 混合检索工具
|
||||
**接口**:`DocumentSearchTool.search(errorCode, province)`
|
||||
|
||||
**输入**:
|
||||
```java
|
||||
{
|
||||
"errorCode": "40003",
|
||||
"province": "广东"
|
||||
}
|
||||
```
|
||||
|
||||
**输出**:
|
||||
```java
|
||||
List<DocumentChunk> {
|
||||
"documentId": "uuid",
|
||||
"chunkId": "uuid",
|
||||
"content": "错误码 40003 表示...",
|
||||
"score": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
**检索策略**:
|
||||
1. **精确匹配**(MySQL):
|
||||
```sql
|
||||
SELECT * FROM api_document
|
||||
WHERE error_code = '40003' AND province = '广东'
|
||||
```
|
||||
2. **语义检索**(Milvus):
|
||||
- 向量化查询文本
|
||||
- 相似度搜索 Top 10
|
||||
3. **RRF 融合**:
|
||||
- 精确匹配分数 = 1.0
|
||||
- 语义检索分数 = Milvus 相似度
|
||||
- 合并排序,返回 Top 3
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 精确匹配优先
|
||||
- ✅ 语义检索补漏
|
||||
- ✅ 返回 Top 3
|
||||
- ✅ 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
## 接口规格
|
||||
|
||||
### API 设计原则
|
||||
- RESTful 风格
|
||||
- 统一响应格式 `Result<T>`
|
||||
- HTTP 状态码语义化
|
||||
- 异常统一处理
|
||||
|
||||
### 统一响应格式
|
||||
```java
|
||||
class Result<T> {
|
||||
int code; // 业务状态码
|
||||
String message; // 提示信息
|
||||
T data; // 数据
|
||||
long timestamp; // 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
### 错误码约定
|
||||
- 200: 成功
|
||||
- 400: 参数错误
|
||||
- 404: 资源不存在
|
||||
- 500: 服务器错误
|
||||
|
||||
---
|
||||
|
||||
## 性能规格
|
||||
|
||||
### 响应时间要求
|
||||
- 文档上传:< 5s(单文件 < 5MB)
|
||||
- 文档查询:< 100ms
|
||||
- 文档删除:< 200ms
|
||||
- 混合检索:< 500ms
|
||||
- Repository 查询:< 50ms
|
||||
|
||||
### 并发要求
|
||||
- 支持 10 QPS(Phase 1 目标)
|
||||
- 后续扩展至 100 QPS(Phase 2/3)
|
||||
|
||||
---
|
||||
|
||||
## 安全规格
|
||||
|
||||
### 输入校验
|
||||
- 文件类型白名单
|
||||
- 文件大小限制(10MB)
|
||||
- SQL 注入防护(JPA Prepared Statement)
|
||||
- XSS 防护(输入转义)
|
||||
|
||||
### 数据安全
|
||||
- Redis 密码保护
|
||||
- MySQL 用户权限最小化
|
||||
- 敏感日志脱敏
|
||||
|
||||
---
|
||||
|
||||
## 测试规格
|
||||
|
||||
### 单元测试覆盖率
|
||||
- Repository: 100%
|
||||
- Service: 80%+
|
||||
- Tool: 80%+
|
||||
- Controller: 70%+
|
||||
|
||||
### 测试类型
|
||||
- 单元测试(JUnit 5 + Mockito)
|
||||
- 集成测试(@SpringBootTest)
|
||||
- 接口测试(MockMvc)
|
||||
|
||||
### 必须覆盖的场景
|
||||
- 正常流程
|
||||
- 边界条件
|
||||
- 异常处理
|
||||
- 并发安全
|
||||
@@ -1,400 +0,0 @@
|
||||
# Phase 1 Infrastructure - Tasks
|
||||
|
||||
## 任务清单
|
||||
|
||||
### Day 1-2: 数据库 + 实体 + 会话(8 个任务)
|
||||
|
||||
#### Task 1.1: 添加依赖到 pom.xml
|
||||
**优先级**: P0(阻塞后续任务)
|
||||
**预估时间**: 15 分钟
|
||||
**产出**:
|
||||
- 修改 `pom.xml`
|
||||
- 添加:spring-boot-starter-data-jpa, mysql-connector-j, flyway-core, flyway-mysql, spring-boot-starter-data-redis, spring-boot-starter-test, h2
|
||||
**验收**: `mvn clean compile` 成功
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.2: 创建 Flyway 迁移脚本 - diagnosis_record
|
||||
**优先级**: P0
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `src/main/resources/db/migration/V001__create_diagnosis_record.sql`
|
||||
**依据**: `docs/tables/diagnosis_record.md`
|
||||
**验收**:
|
||||
- 表结构与文档一致
|
||||
- 索引完整
|
||||
- 注释完整
|
||||
- 本地 MySQL 执行成功
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.3: 创建 Flyway 迁移脚本 - case_library
|
||||
**优先级**: P0
|
||||
**预估时间**: 20 分钟
|
||||
**产出**:
|
||||
- `src/main/resources/db/migration/V002__create_case_library.sql`
|
||||
**依据**: `docs/tables/case_library.md`
|
||||
**验收**: 同 Task 1.2
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.4: 创建 Flyway 迁移脚本 - api_document
|
||||
**优先级**: P0
|
||||
**预估时间**: 20 分钟
|
||||
**产出**:
|
||||
- `src/main/resources/db/migration/V003__create_api_document.sql`
|
||||
**依据**: `docs/tables/api_document.md`
|
||||
**验收**: 同 Task 1.2
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.5: 配置 MySQL + Redis + Flyway
|
||||
**优先级**: P0
|
||||
**预估时间**: 20 分钟
|
||||
**产出**:
|
||||
- 修改 `src/main/resources/application.yml`
|
||||
- 添加 spring.datasource, spring.jpa, spring.flyway, spring.data.redis 配置
|
||||
**验收**:
|
||||
- 应用启动成功
|
||||
- Flyway 自动执行迁移
|
||||
- 3 张表创建成功
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.6: 创建 JPA 实体类
|
||||
**优先级**: P0
|
||||
**预估时间**: 45 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.domain.entity.DiagnosisRecord`
|
||||
- `com.superbiz.agent.domain.entity.CaseLibrary`
|
||||
- `com.superbiz.agent.domain.entity.ApiDocument`
|
||||
**依赖**: Task 1.2, 1.3, 1.4
|
||||
**验收**:
|
||||
- 字段与数据库一致
|
||||
- Lombok 注解完整
|
||||
- JSON 字段序列化正确
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.7: 创建 Repository 接口
|
||||
**优先级**: P0
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.repository.DiagnosisRecordRepository`
|
||||
- `com.superbiz.agent.repository.CaseLibraryRepository`
|
||||
- `com.superbiz.agent.repository.ApiDocumentRepository`
|
||||
**依赖**: Task 1.6
|
||||
**验收**:
|
||||
- 继承 JpaRepository
|
||||
- 常用查询方法定义
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.8: Repository 单元测试
|
||||
**优先级**: P1
|
||||
**预估时间**: 60 分钟
|
||||
**产出**:
|
||||
- `DiagnosisRecordRepositoryTest`
|
||||
- `CaseLibraryRepositoryTest`
|
||||
- `ApiDocumentRepositoryTest`
|
||||
**依赖**: Task 1.7
|
||||
**测试框架**: @DataJpaTest + H2
|
||||
**验收**:
|
||||
- 测试覆盖率 100%
|
||||
- CRUD 测试通过
|
||||
- 自定义查询测试通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.9: 创建会话管理接口
|
||||
**优先级**: P0
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.session.SessionManager` (接口)
|
||||
- `com.superbiz.agent.session.SessionContext` (数据类)
|
||||
- `com.superbiz.agent.session.ToolCall` (数据类)
|
||||
**验收**:
|
||||
- 接口定义清晰
|
||||
- SessionContext 字段完整(含 intentType)
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.10: Redis 会话管理实现
|
||||
**优先级**: P0
|
||||
**预估时间**: 45 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.session.RedisSessionManager`
|
||||
- `com.superbiz.agent.session.SessionConfiguration`
|
||||
**依赖**: Task 1.9
|
||||
**验收**:
|
||||
- 实现 SessionManager 接口
|
||||
- TTL 设置为 30 分钟
|
||||
- JSON 序列化配置正确
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.11: Redis 会话管理单元测试
|
||||
**优先级**: P1
|
||||
**预估时间**: 45 分钟
|
||||
**产出**:
|
||||
- `RedisSessionManagerTest`
|
||||
**依赖**: Task 1.10
|
||||
**测试框架**: @SpringBootTest + Mock RedisTemplate
|
||||
**验收**:
|
||||
- 存取删测试通过
|
||||
- TTL 测试通过
|
||||
- 序列化测试通过
|
||||
|
||||
---
|
||||
|
||||
### Day 3: 代码结构重构(3 个任务)
|
||||
|
||||
#### Task 3.1: 包名重构
|
||||
**优先级**: P0
|
||||
**预估时间**: 30 分钟
|
||||
**操作**:
|
||||
1. IDEA Refactor → Rename Package
|
||||
2. `org.example` → `com.superbiz.agent`
|
||||
3. 更新 `pom.xml` 中的 mainClass
|
||||
4. 全局搜索确认无遗漏
|
||||
**验收**:
|
||||
- 编译通过
|
||||
- 启动成功
|
||||
- 无遗漏的 org.example
|
||||
|
||||
---
|
||||
|
||||
#### Task 3.2: 分层结构优化
|
||||
**优先级**: P1
|
||||
**预估时间**: 45 分钟
|
||||
**产出**:
|
||||
- 创建目录结构(controller/service/repository/domain/tool/config/exception)
|
||||
- 移动现有类到对应目录
|
||||
**验收**:
|
||||
- 目录结构符合 design.md
|
||||
- 编译通过
|
||||
- 启动成功
|
||||
|
||||
---
|
||||
|
||||
#### Task 3.3: DTO 抽离
|
||||
**优先级**: P1
|
||||
**预估时间**: 60 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.domain.dto.DiagnosisRequest`
|
||||
- `com.superbiz.agent.domain.dto.DiagnosisResponse`
|
||||
- `com.superbiz.agent.domain.dto.DocumentUploadRequest`
|
||||
- `com.superbiz.agent.domain.dto.DocumentQueryResponse`
|
||||
- `com.superbiz.agent.domain.dto.Result<T>` (统一响应)
|
||||
**验收**:
|
||||
- Controller 不 import Entity
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### Day 4-5: 文档管理(7 个任务)
|
||||
|
||||
#### Task 4.1: 创建 TextExtractor 服务
|
||||
**优先级**: P0
|
||||
**预估时间**: 60 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.service.TextExtractor`
|
||||
**功能**:
|
||||
- 支持 .txt, .md, .docx, .pdf
|
||||
- 提取纯文本
|
||||
**依赖**: 可能需要添加 Apache POI / PDFBox 依赖
|
||||
**验收**:
|
||||
- 4 种格式提取成功
|
||||
- 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.2: 文档分块服务
|
||||
**优先级**: P0
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.service.DocumentChunkService` (可能已存在,重构)
|
||||
**功能**:
|
||||
- chunk_size=500
|
||||
- overlap=50
|
||||
**验收**:
|
||||
- 分块逻辑正确
|
||||
- 单元测试通过
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.3: 文档上传接口
|
||||
**优先级**: P0
|
||||
**预估时间**: 90 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.controller.DocumentController#upload`
|
||||
- `com.superbiz.agent.service.DocumentService#uploadDocument`
|
||||
**依赖**: Task 4.1, 4.2
|
||||
**验收**:
|
||||
- 上传成功返回 documentId
|
||||
- MySQL + Milvus 数据一致
|
||||
- 异常处理完整
|
||||
- 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.4: 文档查询接口
|
||||
**优先级**: P1
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `DocumentController#query`
|
||||
- `DocumentService#queryDocuments`
|
||||
**验收**:
|
||||
- 分页查询正确
|
||||
- 过滤条件生效
|
||||
- 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.5: 文档删除接口
|
||||
**优先级**: P1
|
||||
**预估时间**: 45 分钟
|
||||
**产出**:
|
||||
- `DocumentController#delete`
|
||||
- `DocumentService#deleteDocument`
|
||||
**验收**:
|
||||
- MySQL 删除成功
|
||||
- Milvus 删除成功
|
||||
- 幂等性保证
|
||||
- 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.6: 混合检索工具
|
||||
**优先级**: P0
|
||||
**预估时间**: 90 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.tool.DocumentSearchTool`
|
||||
**功能**:
|
||||
- 精确匹配(MySQL)
|
||||
- 语义检索(Milvus)
|
||||
- RRF 融合
|
||||
**验收**:
|
||||
- 精确匹配优先
|
||||
- 语义检索补漏
|
||||
- 返回 Top 3
|
||||
- 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### Task 4.7: 集成测试
|
||||
**优先级**: P1
|
||||
**预估时间**: 60 分钟
|
||||
**产出**:
|
||||
- `DocumentIntegrationTest`
|
||||
**测试场景**:
|
||||
- 上传 → 查询 → 检索 → 删除 完整流程
|
||||
**验收**:
|
||||
- 端到端测试通过
|
||||
|
||||
---
|
||||
|
||||
### 全局任务
|
||||
|
||||
#### Task G.1: 统一异常处理
|
||||
**优先级**: P1
|
||||
**预估时间**: 30 分钟
|
||||
**产出**:
|
||||
- `com.superbiz.agent.exception.GlobalExceptionHandler`
|
||||
- `com.superbiz.agent.exception.SessionNotFoundException`
|
||||
- `com.superbiz.agent.exception.DocumentProcessException`
|
||||
**验收**:
|
||||
- 异常统一捕获
|
||||
- 返回格式统一
|
||||
|
||||
---
|
||||
|
||||
#### Task G.2: Docker Compose 配置
|
||||
**优先级**: P2
|
||||
**预估时间**: 20 分钟
|
||||
**产出**:
|
||||
- `docker-compose.yml` (MySQL + Redis + Milvus)
|
||||
**验收**:
|
||||
- `docker-compose up -d` 启动成功
|
||||
- 应用连接成功
|
||||
|
||||
---
|
||||
|
||||
#### Task G.3: README 更新
|
||||
**优先级**: P2
|
||||
**预估时间**: 15 分钟
|
||||
**产出**:
|
||||
- 更新 `README.md`
|
||||
- 添加 Phase 1 安装说明
|
||||
- 添加本地开发指南
|
||||
|
||||
---
|
||||
|
||||
## 任务依赖关系图
|
||||
|
||||
```
|
||||
Day 1-2:
|
||||
Task 1.1 → Task 1.5
|
||||
↓
|
||||
Task 1.2, 1.3, 1.4 → Task 1.6 → Task 1.7 → Task 1.8
|
||||
↓
|
||||
Task 1.5 → Task 1.9 → Task 1.10 → Task 1.11
|
||||
|
||||
Day 3:
|
||||
Task 3.1 (阻塞) → Task 3.2 → Task 3.3
|
||||
|
||||
Day 4-5:
|
||||
Task 4.1, 4.2 → Task 4.3 → Task 4.7
|
||||
↓
|
||||
Task 4.4
|
||||
↓
|
||||
Task 4.5
|
||||
↓
|
||||
Task 4.6 → Task 4.7
|
||||
|
||||
全局:
|
||||
Task G.1 (并行)
|
||||
Task G.2 (并行)
|
||||
Task G.3 (最后)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键路径
|
||||
|
||||
```
|
||||
Task 1.1 → 1.5 → 1.6 → 1.7 → 3.1 → 3.2 → 4.1 → 4.3 → 4.6 → 4.7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 预估总工时
|
||||
|
||||
- Day 1-2: 5.5 小时(11 个任务)
|
||||
- Day 3: 2 小时(3 个任务)
|
||||
- Day 4-5: 6 小时(7 个任务)
|
||||
- 全局: 1 小时(3 个任务)
|
||||
|
||||
**总计**: 14.5 小时(约 2 个完整工作日)
|
||||
|
||||
---
|
||||
|
||||
## 里程碑
|
||||
|
||||
**Milestone 1**: Day 2 结束
|
||||
- ✅ 数据库表就绪
|
||||
- ✅ JPA + Repository 可用
|
||||
- ✅ Redis 会话管理可用
|
||||
|
||||
**Milestone 2**: Day 3 结束
|
||||
- ✅ 包名重构完成
|
||||
- ✅ 代码结构清晰
|
||||
|
||||
**Milestone 3**: Day 5 结束
|
||||
- ✅ 文档管理 CRUD 完整
|
||||
- ✅ 混合检索工具可用
|
||||
- ✅ 单元测试覆盖率达标(70%+)
|
||||
@@ -138,50 +138,18 @@
|
||||
<version>4.36.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI MCP Client - 使用 WebFlux 版本 -->
|
||||
<!-- 注意:spring-ai-starter-mcp-client-webflux 已经包含了 mcp-annotations,无需单独引入 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL + JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway 数据库迁移 -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI MCP Client - 使用 WebFlux 版本 -->
|
||||
<!-- 注意:spring-ai-starter-mcp-client-webflux 已经包含了 mcp-annotations,无需单独引入 -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.superbiz.agent.domain.enums;
|
||||
|
||||
/**
|
||||
* 诊断状态枚举
|
||||
*/
|
||||
public enum DiagnosisStatus {
|
||||
PENDING("待处理"),
|
||||
RUNNING("诊断中"),
|
||||
SUCCESS("成功"),
|
||||
FAILED("失败");
|
||||
|
||||
private final String description;
|
||||
|
||||
DiagnosisStatus(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.superbiz.agent.domain.enums;
|
||||
|
||||
/**
|
||||
* 故障类别枚举
|
||||
*/
|
||||
public enum FaultCategory {
|
||||
EXTERNAL_API("外部接口调用失败"),
|
||||
INTERNAL_ERROR("系统内部错误"),
|
||||
DATABASE("数据库问题"),
|
||||
CACHE("缓存问题"),
|
||||
NETWORK("网络问题"),
|
||||
THREAD("线程问题"),
|
||||
MEMORY("内存问题"),
|
||||
CONFIG("配置问题");
|
||||
|
||||
private final String description;
|
||||
|
||||
FaultCategory(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.superbiz.agent.domain.enums;
|
||||
|
||||
/**
|
||||
* 案例来源类型枚举
|
||||
*/
|
||||
public enum SourceType {
|
||||
AUTO("自动生成"),
|
||||
MANUAL("人工录入");
|
||||
|
||||
private final String description;
|
||||
|
||||
SourceType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,16 @@ public class MilvusClientFactory {
|
||||
ConnectParam.Builder builder = ConnectParam.newBuilder()
|
||||
.withHost(milvusProperties.getHost())
|
||||
.withPort(milvusProperties.getPort())
|
||||
.withDatabaseName(milvusProperties.getDatabase())
|
||||
.withConnectTimeout(milvusProperties.getTimeout(), TimeUnit.MILLISECONDS);
|
||||
|
||||
// 如果配置了用户名和密码
|
||||
if (milvusProperties.getUsername() != null && !milvusProperties.getUsername().isEmpty()) {
|
||||
// Zilliz Cloud: token + SSL
|
||||
if (milvusProperties.getToken() != null && !milvusProperties.getToken().isEmpty()) {
|
||||
builder.withToken(milvusProperties.getToken());
|
||||
builder.withSecure(true);
|
||||
}
|
||||
// 本地 Milvus: username + password
|
||||
else if (milvusProperties.getUsername() != null && !milvusProperties.getUsername().isEmpty()) {
|
||||
builder.withAuthorization(milvusProperties.getUsername(), milvusProperties.getPassword());
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,29 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "document.chunk")
|
||||
public class DocumentChunkConfig {
|
||||
|
||||
|
||||
/**
|
||||
* 每个分片的最大字符数
|
||||
* 每个分片的最大字符数(保留向后兼容)
|
||||
*/
|
||||
private int maxSize = 800;
|
||||
|
||||
|
||||
/**
|
||||
* 分片之间的重叠字符数
|
||||
*/
|
||||
private int overlap = 100;
|
||||
|
||||
/**
|
||||
* 每个分片的最大 token 数(中文~1:1,英文~0.25:1)
|
||||
* 替代 maxSize 作为切割触发器
|
||||
*/
|
||||
private int maxTokens = 500;
|
||||
|
||||
/**
|
||||
* 硬上限 token 数 = maxTokens × 1.2
|
||||
* 仅在不可中断上下文(列表、代码块)内触发
|
||||
*/
|
||||
private int maxTokensHard = 600;
|
||||
|
||||
public void setMaxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
@@ -29,4 +41,12 @@ public class DocumentChunkConfig {
|
||||
public void setOverlap(int overlap) {
|
||||
this.overlap = overlap;
|
||||
}
|
||||
|
||||
public void setMaxTokens(int maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public void setMaxTokensHard(int maxTokensHard) {
|
||||
this.maxTokensHard = maxTokensHard;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ public class MilvusProperties {
|
||||
private String password = "";
|
||||
private String database = "default";
|
||||
private Long timeout = 10000L;
|
||||
private String token = "";
|
||||
private boolean secure = false;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
@@ -62,6 +64,22 @@ public class MilvusProperties {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return secure;
|
||||
}
|
||||
|
||||
public void setSecure(boolean secure) {
|
||||
this.secure = secure;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return host + ":" + port;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class DocumentChunkService {
|
||||
/**
|
||||
* 智能分片文档
|
||||
* 优先按照标题、段落边界进行分割,保持语义完整性
|
||||
*
|
||||
*
|
||||
* @param content 文档内容
|
||||
* @param filePath 文件路径(用于日志)
|
||||
* @return 文档分片列表
|
||||
@@ -42,7 +42,7 @@ public class DocumentChunkService {
|
||||
|
||||
// 1. 首先尝试按标题分割(Markdown格式)
|
||||
List<Section> sections = splitByHeadings(content);
|
||||
|
||||
|
||||
// 2. 对每个章节进行进一步分片
|
||||
int globalChunkIndex = 0;
|
||||
for (Section section : sections) {
|
||||
@@ -60,7 +60,7 @@ public class DocumentChunkService {
|
||||
*/
|
||||
private List<Section> splitByHeadings(String content) {
|
||||
List<Section> sections = new ArrayList<>();
|
||||
|
||||
|
||||
// 匹配 Markdown 标题:# 标题, ## 标题, ### 标题等
|
||||
Pattern headingPattern = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
|
||||
Matcher matcher = headingPattern.matcher(content);
|
||||
@@ -100,18 +100,25 @@ public class DocumentChunkService {
|
||||
|
||||
/**
|
||||
* 对单个章节进行分片
|
||||
* <p>
|
||||
* 核心改造(Phase 1):
|
||||
* - Token 估算替代字符计数
|
||||
* - 感知有序/无序列表结构,不在列表中间切断
|
||||
* - 软边界(maxTokens)+ 硬上限(maxTokensHard)双重控制
|
||||
* - 修复 currentStartIndex 漂移:用段落原始位置而非手工推算
|
||||
*/
|
||||
private List<DocumentChunk> chunkSection(Section section, int startChunkIndex) {
|
||||
List<DocumentChunk> chunks = new ArrayList<>();
|
||||
String content = section.content;
|
||||
String title = section.title;
|
||||
|
||||
// 如果章节内容小于最大尺寸,直接作为一个分片
|
||||
if (content.length() <= chunkConfig.getMaxSize()) {
|
||||
// 短章节直接作为一个分片(用 token 估算替代字符数做短路判断)
|
||||
if (content.length() <= chunkConfig.getMaxSize()
|
||||
&& estimateTokens(content) <= chunkConfig.getMaxTokens()) {
|
||||
DocumentChunk chunk = new DocumentChunk(
|
||||
content,
|
||||
section.startIndex,
|
||||
section.startIndex + content.length(),
|
||||
content,
|
||||
section.startIndex,
|
||||
section.startIndex + content.length(),
|
||||
startChunkIndex
|
||||
);
|
||||
chunk.setTitle(title);
|
||||
@@ -120,45 +127,71 @@ public class DocumentChunkService {
|
||||
}
|
||||
|
||||
// 章节内容较长,需要进一步分片
|
||||
// 优先在段落边界分割
|
||||
List<String> paragraphs = splitByParagraphs(content);
|
||||
|
||||
StringBuilder currentChunk = new StringBuilder();
|
||||
int currentStartIndex = section.startIndex;
|
||||
if (paragraphs.isEmpty()) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// 定位每个段落在 section.content 中的位置(修复 index 漂移)
|
||||
List<ParagraphPos> paraPositions = locateParagraphPositions(paragraphs, content);
|
||||
|
||||
// 当前分片的段落范围
|
||||
int chunkParaStart = 0; // 当前分片第一个段落的索引(在 paragraphs 中)
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
int tokenCount = 0;
|
||||
int chunkIndex = startChunkIndex;
|
||||
|
||||
for (String paragraph : paragraphs) {
|
||||
// 如果当前分片加上新段落超过最大尺寸
|
||||
if (currentChunk.length() > 0 &&
|
||||
currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
|
||||
|
||||
// 保存当前分片
|
||||
String chunkContent = currentChunk.toString().trim();
|
||||
DocumentChunk chunk = new DocumentChunk(
|
||||
chunkContent,
|
||||
currentStartIndex,
|
||||
currentStartIndex + chunkContent.length(),
|
||||
chunkIndex++
|
||||
);
|
||||
chunk.setTitle(title);
|
||||
chunks.add(chunk);
|
||||
for (int i = 0; i < paragraphs.size(); i++) {
|
||||
String paragraph = paragraphs.get(i);
|
||||
int paraTokens = estimateTokens(paragraph);
|
||||
|
||||
// 开始新分片,包含重叠部分
|
||||
String overlap = getOverlapText(chunkContent);
|
||||
currentChunk = new StringBuilder(overlap);
|
||||
currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length();
|
||||
// 判断是否需要切分
|
||||
if (buffer.length() > 0 && tokenCount + paraTokens > chunkConfig.getMaxTokens()) {
|
||||
|
||||
// 检查是否处于不可中断的上下文中
|
||||
if (isInUnbreakableContext(buffer.toString(), paragraph)) {
|
||||
// 硬上限保护:即使不可中断也不能无限膨胀
|
||||
if (tokenCount + paraTokens > chunkConfig.getMaxTokensHard()) {
|
||||
logger.debug(" 触及硬上限 ({} tokens),强制切分", tokenCount + paraTokens);
|
||||
chunkParaStart = saveChunkAndGetNextStart(
|
||||
chunks, section, paraPositions,
|
||||
chunkParaStart, i, title, chunkIndex);
|
||||
chunkIndex++;
|
||||
|
||||
String prevChunkContent = chunks.get(chunks.size() - 1).getContent();
|
||||
String overlap = getOverlapText(prevChunkContent);
|
||||
buffer = new StringBuilder(overlap);
|
||||
tokenCount = estimateTokens(overlap);
|
||||
}
|
||||
// 否则:容忍超出(软边界)
|
||||
} else {
|
||||
// 安全切点:段落边界
|
||||
chunkParaStart = saveChunkAndGetNextStart(
|
||||
chunks, section, paraPositions,
|
||||
chunkParaStart, i, title, chunkIndex);
|
||||
chunkIndex++;
|
||||
|
||||
// 新分片以重叠文本开头
|
||||
String prevChunkContent = chunks.get(chunks.size() - 1).getContent();
|
||||
String overlap = getOverlapText(prevChunkContent);
|
||||
buffer = new StringBuilder(overlap);
|
||||
tokenCount = estimateTokens(overlap);
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk.append(paragraph).append("\n\n");
|
||||
buffer.append(paragraph).append("\n\n");
|
||||
tokenCount += paraTokens;
|
||||
}
|
||||
|
||||
// 保存最后一个分片
|
||||
if (currentChunk.length() > 0) {
|
||||
String chunkContent = currentChunk.toString().trim();
|
||||
if (buffer.length() > 0 && chunkParaStart < paragraphs.size()) {
|
||||
String chunkContent = buffer.toString().trim();
|
||||
int actualStart = paraPositions.get(chunkParaStart).start;
|
||||
int actualEnd = paraPositions.get(paragraphs.size() - 1).end;
|
||||
DocumentChunk chunk = new DocumentChunk(
|
||||
chunkContent,
|
||||
currentStartIndex,
|
||||
currentStartIndex + chunkContent.length(),
|
||||
section.startIndex + actualStart,
|
||||
section.startIndex + actualEnd,
|
||||
chunkIndex
|
||||
);
|
||||
chunk.setTitle(title);
|
||||
@@ -168,12 +201,42 @@ public class DocumentChunkService {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前分块,返回下一个分块的起始段落索引
|
||||
* <p>
|
||||
* 从 section.content 中提取原始文本(而非手工拼装),修复 index 漂移问题
|
||||
*/
|
||||
private int saveChunkAndGetNextStart(
|
||||
List<DocumentChunk> chunks,
|
||||
Section section,
|
||||
List<ParagraphPos> paraPositions,
|
||||
int fromPara,
|
||||
int toPara,
|
||||
String title,
|
||||
int chunkIndex) {
|
||||
|
||||
int actualStart = paraPositions.get(fromPara).start;
|
||||
int actualEnd = paraPositions.get(toPara - 1).end;
|
||||
String originalText = section.content.substring(actualStart, actualEnd);
|
||||
|
||||
DocumentChunk chunk = new DocumentChunk(
|
||||
originalText,
|
||||
section.startIndex + actualStart,
|
||||
section.startIndex + actualEnd,
|
||||
chunkIndex
|
||||
);
|
||||
chunk.setTitle(title);
|
||||
chunks.add(chunk);
|
||||
|
||||
return toPara; // 下一个分块的起始段落索引
|
||||
}
|
||||
|
||||
/**
|
||||
* 按段落分割文本
|
||||
*/
|
||||
private List<String> splitByParagraphs(String content) {
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
|
||||
|
||||
// 按双换行符分割段落
|
||||
String[] parts = content.split("\n\n+");
|
||||
for (String part : parts) {
|
||||
@@ -186,6 +249,106 @@ public class DocumentChunkService {
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位每个段落在原始文本中的字符偏移
|
||||
*/
|
||||
private List<ParagraphPos> locateParagraphPositions(List<String> paragraphs, String sectionContent) {
|
||||
List<ParagraphPos> positions = new ArrayList<>();
|
||||
int searchFrom = 0;
|
||||
for (String p : paragraphs) {
|
||||
int idx = sectionContent.indexOf(p, searchFrom);
|
||||
if (idx >= 0) {
|
||||
positions.add(new ParagraphPos(idx, idx + p.length()));
|
||||
searchFrom = idx + p.length();
|
||||
} else {
|
||||
// fallback: 段落在原文中找不到(不应该发生)
|
||||
positions.add(new ParagraphPos(searchFrom, searchFrom + p.length()));
|
||||
searchFrom += p.length();
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启发式 token 估算(无需外部依赖)
|
||||
* <p>
|
||||
* 中文(BMP): ~1 字符/token
|
||||
* 英文/数字/标点: ~4 字符/token
|
||||
* 空白字符忽略
|
||||
*/
|
||||
private int estimateTokens(String text) {
|
||||
int nonCjkCount = 0;
|
||||
int cjkCount = 0;
|
||||
for (char c : text.toCharArray()) {
|
||||
if (Character.isWhitespace(c)) {
|
||||
continue;
|
||||
}
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
|
||||
if (block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|
||||
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|
||||
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|
||||
|| block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS) {
|
||||
cjkCount++;
|
||||
} else {
|
||||
nonCjkCount++;
|
||||
}
|
||||
}
|
||||
return cjkCount + (nonCjkCount + 3) / 4; // 非中文每 4 字符算 1 token,向上取整
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前段落是否属于不可中断的结构
|
||||
* <p>
|
||||
* 不可中断结构包括:
|
||||
* - 有序列表项("1. ", "2. " 格式)
|
||||
* - 无序列表项("- " 或 "* " 格式)
|
||||
* - 未闭合的代码块(``` 内)
|
||||
*/
|
||||
private boolean isInUnbreakableContext(String buffer, String nextParagraph) {
|
||||
// 有序列表:判断 buffer 末尾和下一段是否都是列表项
|
||||
if (nextParagraph.matches("^\\d{1,2}\\.\\s.*")) {
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine != null && lastLine.matches("^\\d{1,2}\\.\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 无序列表:"- " 或 "* " 格式
|
||||
if (nextParagraph.matches("^[-*]\\s.*")) {
|
||||
String lastLine = getLastNonEmptyLine(buffer);
|
||||
if (lastLine != null && lastLine.matches("^[-*]\\s.*")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 代码块:``` 未闭合
|
||||
if (buffer.contains("```")) {
|
||||
int count = 0;
|
||||
for (int i = 0; i <= buffer.length() - 3; i++) {
|
||||
if (buffer.substring(i).startsWith("```")) {
|
||||
count++;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
if (count % 2 == 1) {
|
||||
return true; // 奇数个 ``` → 在代码块内部
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 buffer 中最后一行非空白文本
|
||||
*/
|
||||
private String getLastNonEmptyLine(String buffer) {
|
||||
String[] lines = buffer.split("\n");
|
||||
for (int i = lines.length - 1; i >= 0; i--) {
|
||||
String line = lines[i].trim();
|
||||
if (!line.isEmpty()) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重叠文本
|
||||
* 从文本末尾提取指定长度的内容作为下一个分片的开头
|
||||
@@ -198,13 +361,13 @@ public class DocumentChunkService {
|
||||
|
||||
// 从末尾提取重叠内容
|
||||
String overlap = text.substring(text.length() - overlapSize);
|
||||
|
||||
|
||||
// 尝试在句子边界截断(查找最后一个句号、问号、感叹号)
|
||||
int lastSentenceEnd = Math.max(
|
||||
overlap.lastIndexOf('。'),
|
||||
Math.max(overlap.lastIndexOf('?'), overlap.lastIndexOf('!'))
|
||||
);
|
||||
|
||||
|
||||
if (lastSentenceEnd > overlapSize / 2) {
|
||||
return overlap.substring(lastSentenceEnd + 1).trim();
|
||||
}
|
||||
@@ -212,6 +375,19 @@ public class DocumentChunkService {
|
||||
return overlap.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 段落在原文中的位置
|
||||
*/
|
||||
private static class ParagraphPos {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
ParagraphPos(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 章节数据类
|
||||
*/
|
||||
|
||||
@@ -6,56 +6,38 @@ server:
|
||||
enabled: true
|
||||
force: true
|
||||
|
||||
# 数据库配置
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads
|
||||
allowed-extensions: txt,md
|
||||
|
||||
milvus:
|
||||
host: in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com
|
||||
port: 443
|
||||
username: ""
|
||||
password: ""
|
||||
database: db_4a578da0f27ce9d
|
||||
timeout: 10000
|
||||
token: ${MILVUS_TOKEN:}
|
||||
secure: true
|
||||
|
||||
# Spring AI Alibaba DashScope 配置
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://119.29.78.52:33306/superbiz_agent?useUnicode=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: '!Fucker123..'
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: 119.29.78.52
|
||||
port: 6379
|
||||
password: ''
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# Spring AI Alibaba DashScope 配置
|
||||
ai:
|
||||
dashscope:
|
||||
api-key: ${DASHSCOPE_API_KEY:your-api-key-here}
|
||||
api-key: ${DASHSCOPE_API_KEY:your-api-key-here} # 从环境变量读取或使用默认值
|
||||
chat:
|
||||
options:
|
||||
timeout: 180000
|
||||
timeout: 180000 # 超时时间180秒(3分钟)
|
||||
retry:
|
||||
max-attempts: 3
|
||||
max-attempts: 3 # 最大重试次数
|
||||
backoff:
|
||||
initial-interval: 2000
|
||||
multiplier: 2
|
||||
max-interval: 10000
|
||||
|
||||
initial-interval: 2000 # 初始重试间隔2秒
|
||||
multiplier: 2 # 重试间隔倍数
|
||||
max-interval: 10000 # 最大重试间隔10秒
|
||||
|
||||
# Spring AI MCP 客户端配置
|
||||
# 如果使用mock数据,请注释这部分内容
|
||||
mcp:
|
||||
client:
|
||||
enabled: true
|
||||
@@ -67,20 +49,7 @@ spring:
|
||||
connections:
|
||||
tencent-cls:
|
||||
url: https://mcp-api.tencent-cloud.com
|
||||
sse-endpoint: /sse/92XXXXXXXXb4
|
||||
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads
|
||||
allowed-extensions: txt,md
|
||||
|
||||
milvus:
|
||||
host: localhost
|
||||
port: 19530
|
||||
username: ""
|
||||
password: ""
|
||||
database: default
|
||||
timeout: 10000
|
||||
sse-endpoint: /sse/92XXXXXXXXb4 # 完整的SSE端点路径
|
||||
|
||||
# 阿里云 DashScope Embedding API 配置
|
||||
dashscope:
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
-- V001: 创建诊断记录表
|
||||
-- 核心业务表,存储每次诊断任务的完整记录
|
||||
-- 设计理念:兼容多种故障类型(外部接口、内部错误、数据库、缓存等)
|
||||
|
||||
CREATE TABLE diagnosis_record (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
diagnosis_id VARCHAR(64) UNIQUE NOT NULL COMMENT '诊断唯一ID(UUID)',
|
||||
|
||||
-- 关联信息
|
||||
session_id VARCHAR(64) COMMENT '会话ID(关联Redis)',
|
||||
business_id VARCHAR(128) COMMENT '业务标识(订单号/请求ID/线程ID/任务ID...)',
|
||||
trace_id VARCHAR(64) COMMENT '链路追踪ID',
|
||||
|
||||
-- 故障分类(泛化设计)
|
||||
fault_category VARCHAR(32) COMMENT '故障类别(EXTERNAL_API/INTERNAL_ERROR/DATABASE/CACHE/NETWORK/THREAD/MEMORY/CONFIG)',
|
||||
fault_source VARCHAR(128) COMMENT '故障源(省份/服务名/类名/数据库实例...)',
|
||||
fault_target VARCHAR(256) COMMENT '故障目标(接口URL/方法名/SQL语句/缓存键...)',
|
||||
|
||||
-- 错误信息(通用)
|
||||
error_code VARCHAR(64) COMMENT '错误码(业务错误码/HTTP状态码/异常类名)',
|
||||
error_message TEXT COMMENT '错误消息',
|
||||
stack_trace TEXT COMMENT '堆栈信息(内部错误时记录)',
|
||||
|
||||
-- 诊断结果
|
||||
problem_type VARCHAR(32) COMMENT '问题类型(参数/网络/权限/逻辑/空指针/死锁...)',
|
||||
root_cause TEXT COMMENT '根因分析',
|
||||
solution TEXT COMMENT '修复方案',
|
||||
report_markdown TEXT COMMENT '完整诊断报告(Markdown格式)',
|
||||
|
||||
-- 评估指标
|
||||
status VARCHAR(16) DEFAULT 'PENDING' COMMENT '诊断状态(PENDING/RUNNING/SUCCESS/FAILED)',
|
||||
confidence INT COMMENT '诊断置信度(0-100)',
|
||||
duration INT COMMENT '诊断耗时(毫秒)',
|
||||
|
||||
-- 用户反馈
|
||||
feedback VARCHAR(16) COMMENT '用户反馈(useful/not_useful/null)',
|
||||
|
||||
-- 调试字段
|
||||
tool_calls JSON COMMENT '工具调用记录',
|
||||
|
||||
-- 元数据
|
||||
created_by VARCHAR(64) COMMENT '创建人',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_business_id (business_id),
|
||||
INDEX idx_trace_id (trace_id),
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_fault_category (fault_category),
|
||||
INDEX idx_fault_source_target (fault_source, fault_target(100)),
|
||||
INDEX idx_error_code (error_code),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诊断记录表(v2.0 泛化版)';
|
||||
@@ -1,41 +0,0 @@
|
||||
-- V002: 创建案例库表
|
||||
-- 知识沉淀表,存储高质量诊断案例,支持相似案例推荐
|
||||
-- 设计理念:质量过滤,只存储成功诊断 + 用户反馈有用的案例
|
||||
|
||||
CREATE TABLE case_library (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
case_id VARCHAR(64) UNIQUE NOT NULL COMMENT '案例唯一ID(UUID)',
|
||||
|
||||
-- 来源关联
|
||||
diagnosis_id VARCHAR(64) COMMENT '关联诊断记录(可选,人工录入时为空)',
|
||||
source_type VARCHAR(16) DEFAULT 'AUTO' COMMENT '来源类型(AUTO:自动生成/MANUAL:人工录入)',
|
||||
|
||||
-- 案例分类
|
||||
fault_category VARCHAR(32) COMMENT '故障类别(EXTERNAL_API/INTERNAL_ERROR/DATABASE...)',
|
||||
fault_source VARCHAR(128) COMMENT '故障源(省份/服务名/类名...)',
|
||||
fault_target VARCHAR(256) COMMENT '故障目标(接口URL/方法名/SQL...)',
|
||||
error_code VARCHAR(64) COMMENT '错误码',
|
||||
|
||||
-- 案例内容
|
||||
title VARCHAR(256) NOT NULL COMMENT '案例标题(简短描述)',
|
||||
root_cause TEXT NOT NULL COMMENT '根因分析',
|
||||
solution TEXT NOT NULL COMMENT '解决方案',
|
||||
|
||||
-- 简单统计
|
||||
reference_count INT DEFAULT 0 COMMENT '引用次数(被推荐的次数)',
|
||||
|
||||
-- 元数据
|
||||
created_by VARCHAR(64) COMMENT '创建人',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_fault_category (fault_category),
|
||||
INDEX idx_error_code (error_code),
|
||||
INDEX idx_fault_source (fault_source),
|
||||
INDEX idx_fault_target (fault_target(100)),
|
||||
INDEX idx_diagnosis_id (diagnosis_id),
|
||||
INDEX idx_reference_count (reference_count),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='案例库表(MVP版)';
|
||||
@@ -1,38 +0,0 @@
|
||||
-- V003: 创建文档元数据表
|
||||
-- 文档管理表,管理接口文档的元信息
|
||||
-- 设计理念:MySQL 负责元数据管理,Milvus 负责内容检索,通过 doc_id 关联
|
||||
|
||||
CREATE TABLE api_document (
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
doc_id VARCHAR(64) UNIQUE NOT NULL COMMENT '文档唯一ID(UUID),关联Milvus',
|
||||
|
||||
-- 文档分类
|
||||
fault_category VARCHAR(32) DEFAULT 'EXTERNAL_API' COMMENT '文档类别',
|
||||
fault_source VARCHAR(128) COMMENT '文档归属(省份/服务名)',
|
||||
api_name VARCHAR(128) COMMENT '接口名称',
|
||||
version VARCHAR(32) DEFAULT 'v1.0' COMMENT '文档版本',
|
||||
|
||||
-- 文件信息
|
||||
file_name VARCHAR(256) NOT NULL COMMENT '原始文件名',
|
||||
file_path VARCHAR(512) COMMENT '文件存储路径',
|
||||
file_hash VARCHAR(64) COMMENT '文件MD5 hash(用于去重)',
|
||||
file_size BIGINT COMMENT '文件大小(字节)',
|
||||
|
||||
-- 索引状态
|
||||
status VARCHAR(16) DEFAULT 'PENDING' COMMENT '索引状态(PENDING/PROCESSING/INDEXED/FAILED)',
|
||||
chunk_count INT DEFAULT 0 COMMENT '分块数量',
|
||||
error_message TEXT COMMENT '失败原因',
|
||||
|
||||
-- 时间字段
|
||||
indexed_at DATETIME COMMENT '索引完成时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 索引
|
||||
UNIQUE INDEX uk_file_hash (file_hash),
|
||||
INDEX idx_doc_id (doc_id),
|
||||
INDEX idx_fault_source (fault_source),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档元数据表(MVP版)';
|
||||
@@ -0,0 +1,539 @@
|
||||
package org.example.service;
|
||||
|
||||
import org.example.config.DocumentChunkConfig;
|
||||
import org.example.dto.DocumentChunk;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 当前分片策略的单元测试 — 覆盖旧能力回归 + Phase 1 新增能力
|
||||
*/
|
||||
@DisplayName("DocumentChunkService 分片策略")
|
||||
class DocumentChunkServiceTest {
|
||||
|
||||
private DocumentChunkService service;
|
||||
private DocumentChunkConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
config = new DocumentChunkConfig();
|
||||
config.setMaxSize(800);
|
||||
config.setMaxTokens(500);
|
||||
config.setMaxTokensHard(600);
|
||||
config.setOverlap(100);
|
||||
service = new DocumentChunkService();
|
||||
try {
|
||||
var field = DocumentChunkService.class.getDeclaredField("chunkConfig");
|
||||
field.setAccessible(true);
|
||||
field.set(service, config);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回归:边界条件 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("边界条件")
|
||||
class BoundaryTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("null 内容 → 空列表")
|
||||
void nullContent_returnsEmpty() {
|
||||
List<DocumentChunk> chunks = service.chunkDocument(null, "/test/null.md");
|
||||
assertTrue(chunks.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空字符串 → 空列表")
|
||||
void emptyContent_returnsEmpty() {
|
||||
List<DocumentChunk> chunks = service.chunkDocument(" \n ", "/test/empty.md");
|
||||
assertTrue(chunks.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("短文档(≤maxSize)→ 1个分块")
|
||||
void shortDocument_singleChunk() {
|
||||
String content = "这是一篇短文档,内容不超过800个字符。";
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/short.md");
|
||||
|
||||
assertEquals(1, chunks.size());
|
||||
assertEquals(content, chunks.get(0).getContent());
|
||||
assertEquals(0, chunks.get(0).getChunkIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("恰好 maxSize 边界 → 1个分块")
|
||||
void exactlyMaxSize_singleChunk() {
|
||||
String content = "A".repeat(800);
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/boundary.md");
|
||||
assertEquals(1, chunks.size());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回归:标题分割 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Markdown 标题分割")
|
||||
class HeadingSplitTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("单个 H1 标题 → section 继承标题")
|
||||
void singleHeading_titlePropagates() {
|
||||
String content = "# CPU高负载问题\n\n这是CPU高负载的描述内容。";
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/cpu.md");
|
||||
|
||||
assertEquals(1, chunks.size());
|
||||
assertEquals("CPU高负载问题", chunks.get(0).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多个标题 → 按标题边界分割")
|
||||
void multipleHeadings_splitAtHeadings() {
|
||||
String content =
|
||||
"# CPU高负载\n\nCPU问题的详细描述。\n\n" +
|
||||
"# 内存高负载\n\n内存问题的详细描述。";
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/multi.md");
|
||||
|
||||
assertEquals(2, chunks.size());
|
||||
assertEquals("CPU高负载", chunks.get(0).getTitle());
|
||||
assertEquals("内存高负载", chunks.get(1).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多级标题(H1/H2/H3)→ 标题独立不冲突")
|
||||
void multiLevelHeadings() {
|
||||
String content =
|
||||
"# 一级标题\n\n一级内容。\n\n" +
|
||||
"## 二级标题\n\n二级内容。\n\n" +
|
||||
"### 三级标题\n\n三级内容。";
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/levels.md");
|
||||
assertEquals(3, chunks.size());
|
||||
assertEquals("一级标题", chunks.get(0).getTitle());
|
||||
assertEquals("二级标题", chunks.get(1).getTitle());
|
||||
assertEquals("三级标题", chunks.get(2).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("H1-H6 全部支持")
|
||||
void allHeadingLevels() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
sb.append("#".repeat(i)).append(" 标题").append(i).append("\n\n内容").append(i).append("。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/h1h6.md");
|
||||
assertEquals(6, chunks.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无标题文档 → 整个文档作为1个 section")
|
||||
void noHeadings_entireAsOneSection() {
|
||||
String content = "纯文本没有标题。\n\n第二段内容。\n\n第三段内容。";
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/nohead.md");
|
||||
assertFalse(chunks.isEmpty());
|
||||
assertNull(chunks.get(0).getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回归:段落边界切分 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("超长章节 — 段落边界切分")
|
||||
class ParagraphSplitTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("短章节(≤maxSize)→ 不进入段落切割")
|
||||
void shortSection_noParagraphSplit() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 测试\n\n");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sb.append("段落").append(i).append(":这是一段短内容。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/short_sec.md");
|
||||
assertEquals(1, chunks.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("超长章节 → 在段落边界切分")
|
||||
void longSection_splitsAtParagraphBoundaries() {
|
||||
config.setMaxSize(50);
|
||||
config.setMaxTokens(30);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 长章节\n\n");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb.append("段落").append(i).append(":ABCDEFGHIJKLMNOPQRSTUVWXYZ。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/long_sec.md");
|
||||
assertTrue(chunks.size() >= 2, "超长章节应切分为多个分块,实际: " + chunks.size());
|
||||
|
||||
// 所有分块携带相同的 title
|
||||
for (DocumentChunk c : chunks) {
|
||||
assertEquals("长章节", c.getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回归:chunkIndex 元数据 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("分块元数据")
|
||||
class ChunkMetadataTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("chunkIndex 自增且唯一")
|
||||
void chunkIndexSequential() {
|
||||
config.setMaxSize(50);
|
||||
config.setMaxTokens(30);
|
||||
|
||||
StringBuilder sb = new StringBuilder("# Meta\n\n");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sb.append("段落").append(i).append(":填充内容以触发切分机制。ABCDE。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/meta.md");
|
||||
assertTrue(chunks.size() >= 2);
|
||||
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
assertEquals(i, chunks.get(i).getChunkIndex(),
|
||||
"chunkIndex 应从0开始连续递增");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("startIndex/endIndex 范围合法 — 无漂移")
|
||||
void indexRangeValid_noDrift() {
|
||||
String content = "# 标题\n\n测试内容。";
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/index.md");
|
||||
|
||||
for (DocumentChunk c : chunks) {
|
||||
assertTrue(c.getStartIndex() >= 0);
|
||||
assertTrue(c.getEndIndex() > c.getStartIndex(),
|
||||
"endIndex(" + c.getEndIndex() + ") 应 > startIndex(" + c.getStartIndex() + ")");
|
||||
assertTrue(c.getEndIndex() <= content.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新增:Token 估算 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 估算")
|
||||
class TokenEstimationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("纯中文 800 字符 ≈ 800 tokens → 短章节不切")
|
||||
void pureChinese_fewerTokensThanMax() {
|
||||
config.setMaxTokens(400);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 中文测试\n\n");
|
||||
// 纯中文 ~300 字符 ≈ 300 tokens
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sb.append("这是纯中文测试内容的第十").append(i).append("段落。");
|
||||
sb.append("每个中文字符大约占用一个令牌的位置。");
|
||||
sb.append("因此这段文本的令牌数大致等于字符数。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/cn_tokens.md");
|
||||
// 300 字符 ≈ 300 tokens < 400 maxTokens → 1 个分块
|
||||
assertEquals(1, chunks.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("纯英文 2000 字符 ≈ 500 tokens → 刚好不超过上限")
|
||||
void pureEnglish_moreCharactersSameTokens() {
|
||||
config.setMaxTokens(200);
|
||||
config.setMaxTokensHard(250);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# English Test\n\n");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append("This is paragraph number ").append(i)
|
||||
.append(" containing English text. ")
|
||||
.append("English characters are much cheaper in tokens. ")
|
||||
.append("More filler text here to reach the limit properly. ")
|
||||
.append("Yet another sentence for good measure. ")
|
||||
.append("Still more words needed to reach token limit here.\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/en_tokens.md");
|
||||
// 大量英文才占少量 token → 分块数应少于用字符计数的版本
|
||||
assertTrue(chunks.size() >= 2, "1200+ 字符英文应切分");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新增:列表结构感知 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("列表结构感知")
|
||||
class ListStructureTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("有序列表项之间不切分 — 即使超过 maxTokens")
|
||||
void orderedList_notSplitBetweenItems() {
|
||||
config.setMaxTokens(80);
|
||||
config.setMaxTokensHard(200);
|
||||
config.setOverlap(30);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 排查步骤\n\n");
|
||||
// 5个有序列表项,每项 ~40 字符 ≈ 40 tokens,总共 ~200 tokens
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
sb.append(i).append(". 这是排查步骤第").append(i)
|
||||
.append("项,包含具体的操作指引和注意事项说明。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/ordered_list.md");
|
||||
|
||||
// 5项应保持在一起(未触及 hard 上限)
|
||||
assertEquals(1, chunks.size(),
|
||||
"有序列表项不应被拆散,实际分块数: " + chunks.size());
|
||||
|
||||
String content = chunks.get(0).getContent();
|
||||
assertTrue(content.contains("1. "), "应包含第1项");
|
||||
assertTrue(content.contains("5. "), "应包含第5项");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有序列表触及硬上限 → 在列表项边界强制切分")
|
||||
void orderedList_hardLimitSplits() {
|
||||
config.setMaxTokens(50);
|
||||
config.setMaxTokensHard(100);
|
||||
config.setOverlap(20);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 长列表\n\n");
|
||||
// 每项 ~60 tokens,硬上限 100 → 最多装 1 项多
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
sb.append(i).append(". 这是很长的排查步骤内容,包含详细的说明信息。")
|
||||
.append("每个步骤都要执行多个检查操作。继续填充文本以增加令牌计数。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/long_list.md");
|
||||
|
||||
System.out.println(" 长列表硬上限测试 — 实际分块数: " + chunks.size());
|
||||
for (DocumentChunk c : chunks) {
|
||||
System.out.println(" Chunk #" + c.getChunkIndex() + ": " + c.getContent().length() + "字符 "
|
||||
+ "| start=" + c.getStartIndex() + " end=" + c.getEndIndex()
|
||||
+ " | preview=" + c.getContent().substring(0, Math.min(60, c.getContent().length())).replace("\n", "\\n"));
|
||||
}
|
||||
|
||||
// 硬上限会强制切分,但每个分块内的列表项应保持连续
|
||||
assertTrue(chunks.size() >= 2, "长列表应至少触发1次切分,实际: " + chunks.size());
|
||||
|
||||
// 验证:除了第一个分块(可能是标题),其余应包含列表项
|
||||
for (int i = 1; i < chunks.size(); i++) {
|
||||
DocumentChunk c = chunks.get(i);
|
||||
assertFalse(c.getContent().isEmpty());
|
||||
assertTrue(c.getContent().matches("(?s).*\\d+\\.\\s.*"),
|
||||
"非标题分块应包含列表项,Chunk #" + c.getChunkIndex()
|
||||
+ " preview: " + c.getContent().substring(0, Math.min(60, c.getContent().length())));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无序列表项之间不切分")
|
||||
void unorderedList_notSplitBetweenItems() {
|
||||
config.setMaxTokens(80);
|
||||
config.setMaxTokensHard(200);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 检查清单\n\n");
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
sb.append("- 检查项").append(i).append(":确认服务运行状态正常并记录相关指标。\n\n");
|
||||
}
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/unordered_list.md");
|
||||
assertEquals(1, chunks.size(), "无序列表项不应被拆散");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("列表结束后普通段落应从下一段落开始新分块")
|
||||
void listEnds_normalParagraphStartsNewChunk() {
|
||||
config.setMaxTokens(150);
|
||||
config.setMaxTokensHard(250);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 文档\n\n");
|
||||
// 先一个普通段落
|
||||
sb.append("这是介绍段落,描述系统的整体架构和设计思路。\n\n");
|
||||
// 有序列表
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
sb.append(i).append(". 列表项第").append(i).append("条,包含操作说明。\n\n");
|
||||
}
|
||||
// 普通段落
|
||||
sb.append("这是总结段落,包含上述操作完成后需要关注的监控指标。\n\n");
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(sb.toString(), "/test/list_mixed.md");
|
||||
assertTrue(chunks.size() >= 1);
|
||||
// 列表项应保持在一起
|
||||
for (DocumentChunk c : chunks) {
|
||||
String content = c.getContent();
|
||||
// 分块中不应有孤立的单个列表项(除非只有一个)
|
||||
if (content.contains("1. ") && content.contains("3. ")) {
|
||||
// 这个分块包含了全部3个列表项 → 正确
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新增:代码块结构感知 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("代码块结构感知")
|
||||
class CodeBlockTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("代码块内部不切分")
|
||||
void codeBlock_notSplitInside() {
|
||||
config.setMaxTokens(60);
|
||||
config.setMaxTokensHard(200);
|
||||
config.setOverlap(20);
|
||||
|
||||
String content =
|
||||
"# 代码示例\n\n" +
|
||||
"以下是配置代码:\n\n" +
|
||||
"```yaml\n" +
|
||||
"server:\n" +
|
||||
" port: 8080\n" +
|
||||
" host: localhost\n" +
|
||||
" timeout: 30s\n" +
|
||||
"```\n\n" +
|
||||
"配置说明结束。";
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(content, "/test/code.md");
|
||||
|
||||
// 代码块应保持完整(未触及硬上限)
|
||||
// 验证:至少有一个分块包含完整的 ```...```
|
||||
boolean foundCompleteBlock = false;
|
||||
for (DocumentChunk c : chunks) {
|
||||
String text = c.getContent();
|
||||
if (text.contains("```yaml") && text.contains("```") &&
|
||||
text.indexOf("```yaml") < text.lastIndexOf("```")) {
|
||||
foundCompleteBlock = true;
|
||||
}
|
||||
}
|
||||
// 可能整体在一个分块中
|
||||
assertTrue(chunks.size() >= 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 可视化 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("可视化 — 打印切分结果")
|
||||
class VisualInspectionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("模拟运维文档 — 展示新策略效果")
|
||||
void realWorldAIOpsDoc() {
|
||||
config.setMaxTokens(150);
|
||||
config.setMaxTokensHard(200);
|
||||
config.setOverlap(40);
|
||||
|
||||
String doc = """
|
||||
# CPU高负载问题排查指南
|
||||
|
||||
## 问题现象
|
||||
|
||||
服务器CPU使用率持续超过90%,系统响应变慢,用户反馈页面加载超时。
|
||||
监控告警系统连续发出多条CPU使用率告警。
|
||||
|
||||
## 排查步骤
|
||||
|
||||
1. 登录服务器,执行 top 命令查看当前CPU使用率最高的进程。记录进程ID和CPU占用百分比。
|
||||
|
||||
2. 使用 ps aux | grep {进程名} 确认相关服务的运行状态。检查是否有异常进程占用资源。
|
||||
|
||||
3. 查看应用日志,重点关注最近15分钟的ERROR级别日志。使用 tail -n 500 命令。
|
||||
|
||||
4. 检查数据库连接池状态,确认是否有慢查询或连接泄漏。查看慢查询日志。
|
||||
|
||||
5. 检查JVM内存使用情况和GC日志。使用 jstat -gcutil {pid} 1000 命令观察GC频率。
|
||||
|
||||
## 常见原因
|
||||
|
||||
1. 死循环或递归调用导致CPU满载。检查是否有未设置退出条件的循环逻辑。
|
||||
2. 大量正则表达式匹配操作。检查是否有未编译的正则在循环中使用。
|
||||
|
||||
## 解决方案
|
||||
|
||||
根据排查结果采取对应措施:代码问题则回滚或热修复;资源不足则扩容。
|
||||
处理完成后持续观察监控指标30分钟,确认CPU使用率恢复正常。
|
||||
""";
|
||||
|
||||
List<DocumentChunk> chunks = service.chunkDocument(doc, "/kb/cpu_high_usage.md");
|
||||
|
||||
System.out.println("========================================");
|
||||
System.out.println(" Phase 1 新策略效果 — 模拟运维文档");
|
||||
System.out.println(" 配置: maxTokens=150, hard=200, overlap=40");
|
||||
System.out.println(" 总字符数: " + doc.length());
|
||||
System.out.println(" 总分块数: " + chunks.size());
|
||||
System.out.println("========================================\n");
|
||||
|
||||
for (DocumentChunk c : chunks) {
|
||||
System.out.println("┌─ Chunk #" + c.getChunkIndex());
|
||||
System.out.println("│ Title: " + (c.getTitle() != null ? c.getTitle() : "(无)"));
|
||||
System.out.println("│ Range: [" + c.getStartIndex() + "→" + c.getEndIndex() + "] (" + c.getContent().length() + "字符)");
|
||||
// 显示前150字符
|
||||
String preview = c.getContent().length() > 120
|
||||
? c.getContent().substring(0, 120).replace("\n", "\\n") + "..."
|
||||
: c.getContent().replace("\n", "\\n");
|
||||
System.out.println("│ Preview: " + preview);
|
||||
System.out.println("└──────────────────────\n");
|
||||
}
|
||||
|
||||
assertTrue(chunks.size() >= 3, "应产生多个分块");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("中英混排对比 — token vs 字符计数差异")
|
||||
void mixedContentComparison() {
|
||||
config.setMaxTokens(100);
|
||||
config.setMaxTokensHard(150);
|
||||
config.setOverlap(30);
|
||||
|
||||
String chinese = "这是中文内容示范。中文每个字符在LLM中约占用1个token。" +
|
||||
"因此这段文本在上下文窗口中占用的token数较多。" +
|
||||
"继续填充文字以触发切分逻辑,验证中文token估算是否合理。" +
|
||||
"更多中文文本来增加令牌计数。";
|
||||
|
||||
String english = "This is English content. Each word may take one or two tokens. " +
|
||||
"A sentence like this one actually consumes relatively few tokens compared to " +
|
||||
"Chinese characters. More English text to reach the same token count as above. " +
|
||||
"Still need more words because English is very efficient in tokenization. " +
|
||||
"Adding even more content to make this paragraph long enough to test properly.";
|
||||
|
||||
List<DocumentChunk> cnChunks = service.chunkDocument("# CN\n\n" + chinese + "\n\n" + chinese, "/test/cn.md");
|
||||
List<DocumentChunk> enChunks = service.chunkDocument("# EN\n\n" + english + "\n\n" + english, "/test/en.md");
|
||||
|
||||
System.out.println("========================================");
|
||||
System.out.println(" Token 计数对比");
|
||||
System.out.println(" 配置: maxTokens=100, overlap=30");
|
||||
System.out.println("========================================");
|
||||
System.out.println(" 中文文档: " + (chinese.length() * 2) + "字符 → " + cnChunks.size() + "个分块");
|
||||
System.out.println(" 英文文档: " + (english.length() * 2) + "字符 → " + enChunks.size() + "个分块");
|
||||
|
||||
for (DocumentChunk c : cnChunks) {
|
||||
System.out.println(" 中文Chunk#" + c.getChunkIndex() + ": " + c.getContent().length() + "字符");
|
||||
}
|
||||
for (DocumentChunk c : enChunks) {
|
||||
System.out.println(" 英文Chunk#" + c.getChunkIndex() + ": " + c.getContent().length() + "字符");
|
||||
}
|
||||
System.out.println(" ★ 现在中文和英文的分块数更接近(基于 token 而非字符)");
|
||||
System.out.println("========================================");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.example.service;
|
||||
|
||||
import io.milvus.client.MilvusServiceClient;
|
||||
import io.milvus.grpc.DataType;
|
||||
import io.milvus.grpc.FlushResponse;
|
||||
import io.milvus.grpc.MutationResult;
|
||||
import io.milvus.grpc.SearchResults;
|
||||
import io.milvus.grpc.ShowCollectionsResponse;
|
||||
import io.milvus.common.clientenum.ConsistencyLevelEnum;
|
||||
import io.milvus.param.ConnectParam;
|
||||
import io.milvus.param.IndexType;
|
||||
import io.milvus.param.MetricType;
|
||||
import io.milvus.param.R;
|
||||
import io.milvus.param.RpcStatus;
|
||||
import io.milvus.param.collection.*;
|
||||
import io.milvus.param.dml.InsertParam;
|
||||
import io.milvus.param.dml.SearchParam;
|
||||
import io.milvus.param.index.CreateIndexParam;
|
||||
import io.milvus.response.SearchResultsWrapper;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Milvus 连接验证")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class MilvusConnectionTest {
|
||||
|
||||
private static final String COLLECTION = "conn_test";
|
||||
private static final int DIM = 128;
|
||||
|
||||
private static MilvusServiceClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void connect() {
|
||||
String host = envOrDefault("MILVUS_HOST",
|
||||
"in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com");
|
||||
int port = Integer.parseInt(envOrDefault("MILVUS_PORT", "443"));
|
||||
String token = System.getenv("MILVUS_TOKEN");
|
||||
|
||||
assertNotNull(token, "环境变量 MILVUS_TOKEN 未设置");
|
||||
|
||||
ConnectParam connectParam = ConnectParam.newBuilder()
|
||||
.withHost(host)
|
||||
.withPort(port)
|
||||
.withToken(token)
|
||||
.withSecure(true)
|
||||
.withDatabaseName("db_4a578da0f27ce9d")
|
||||
.withConnectTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
client = new MilvusServiceClient(connectParam);
|
||||
System.out.println("连接目标: " + host + ":" + port);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void disconnect() {
|
||||
if (client != null) {
|
||||
try {
|
||||
client.dropCollection(DropCollectionParam.newBuilder()
|
||||
.withCollectionName(COLLECTION).build());
|
||||
} catch (Exception ignored) {}
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeMsg(R<?> resp) {
|
||||
try {
|
||||
return resp.getMessage();
|
||||
} catch (Exception e) {
|
||||
return "(no message)";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("1. 连接成功 - 能列出 collection")
|
||||
void listCollections() {
|
||||
R<ShowCollectionsResponse> resp = client.showCollections(
|
||||
ShowCollectionsParam.newBuilder().build());
|
||||
|
||||
System.out.println("listCollections status: " + resp.getStatus() + ", msg: " + safeMsg(resp));
|
||||
assertEquals(0, resp.getStatus(), "连接失败,status=" + resp.getStatus());
|
||||
|
||||
List<String> names = resp.getData().getCollectionNamesList();
|
||||
System.out.println("现有 collections: " + names);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("2. 创建测试 collection")
|
||||
void createCollection() {
|
||||
client.dropCollection(DropCollectionParam.newBuilder()
|
||||
.withCollectionName(COLLECTION).build());
|
||||
|
||||
FieldType idField = FieldType.newBuilder()
|
||||
.withName("id")
|
||||
.withDataType(DataType.Int64)
|
||||
.withPrimaryKey(true)
|
||||
.withAutoID(true)
|
||||
.build();
|
||||
|
||||
FieldType vectorField = FieldType.newBuilder()
|
||||
.withName("vector")
|
||||
.withDataType(DataType.FloatVector)
|
||||
.withDimension(DIM)
|
||||
.build();
|
||||
|
||||
CollectionSchemaParam schema = CollectionSchemaParam.newBuilder()
|
||||
.addFieldType(idField)
|
||||
.addFieldType(vectorField)
|
||||
.build();
|
||||
|
||||
R<RpcStatus> resp = client.createCollection(
|
||||
CreateCollectionParam.newBuilder()
|
||||
.withCollectionName(COLLECTION)
|
||||
.withSchema(schema)
|
||||
.build());
|
||||
|
||||
System.out.println("createCollection status: " + resp.getStatus() + ", msg: " + safeMsg(resp));
|
||||
assertEquals(0, resp.getStatus(), "创建 collection 失败");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("3. 插入数据 + flush")
|
||||
void insertAndFlush() {
|
||||
List<Float> vec1 = makeVector(1.0f);
|
||||
List<Float> vec2 = makeVector(2.0f);
|
||||
List<Float> vec3 = makeVector(3.0f);
|
||||
|
||||
List<InsertParam.Field> fields = Collections.singletonList(
|
||||
new InsertParam.Field("vector", Arrays.asList(vec1, vec2, vec3))
|
||||
);
|
||||
|
||||
R<MutationResult> insertResp = client.insert(
|
||||
InsertParam.newBuilder()
|
||||
.withCollectionName(COLLECTION)
|
||||
.withFields(fields)
|
||||
.build());
|
||||
|
||||
System.out.println("insert status: " + insertResp.getStatus() + ", msg: " + safeMsg(insertResp));
|
||||
assertEquals(0, insertResp.getStatus(), "插入失败");
|
||||
|
||||
// 官方示例要求:insert 后必须 flush,数据才对搜索可见
|
||||
R<FlushResponse> flushResp = client.flush(FlushParam.newBuilder()
|
||||
.withCollectionNames(Collections.singletonList(COLLECTION))
|
||||
.withSyncFlush(true)
|
||||
.withSyncFlushWaitingTimeout(30L)
|
||||
.build());
|
||||
|
||||
System.out.println("flush status: " + flushResp.getStatus() + ", msg: " + safeMsg(flushResp));
|
||||
assertEquals(0, flushResp.getStatus(), "flush 失败");
|
||||
System.out.println("插入 3 条数据并 flush 完成");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("4. 创建索引 + 加载")
|
||||
void createIndexAndLoad() {
|
||||
R<RpcStatus> indexResp = client.createIndex(
|
||||
CreateIndexParam.newBuilder()
|
||||
.withCollectionName(COLLECTION)
|
||||
.withFieldName("vector")
|
||||
.withIndexType(IndexType.AUTOINDEX)
|
||||
.withMetricType(MetricType.L2)
|
||||
.build());
|
||||
|
||||
System.out.println("createIndex status: " + indexResp.getStatus() + ", msg: " + safeMsg(indexResp));
|
||||
assertEquals(0, indexResp.getStatus(), "创建索引失败");
|
||||
|
||||
R<RpcStatus> loadResp = client.loadCollection(
|
||||
LoadCollectionParam.newBuilder()
|
||||
.withCollectionName(COLLECTION)
|
||||
.withSyncLoad(true)
|
||||
.withSyncLoadWaitingTimeout(30L)
|
||||
.build());
|
||||
|
||||
System.out.println("load status: " + loadResp.getStatus() + ", msg: " + safeMsg(loadResp));
|
||||
assertEquals(0, loadResp.getStatus(), "加载失败");
|
||||
System.out.println("索引创建 + 加载完成");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("5. 向量搜索")
|
||||
void search() throws InterruptedException {
|
||||
Thread.sleep(3000);
|
||||
|
||||
List<Float> queryVec = makeVector(1.1f);
|
||||
|
||||
R<SearchResults> resp = null;
|
||||
for (int retry = 0; retry < 10; retry++) {
|
||||
resp = client.search(
|
||||
SearchParam.newBuilder()
|
||||
.withCollectionName(COLLECTION)
|
||||
.withMetricType(MetricType.L2)
|
||||
.withTopK(2)
|
||||
.withVectors(Collections.singletonList(queryVec))
|
||||
.withVectorFieldName("vector")
|
||||
.withParams("{}")
|
||||
.withConsistencyLevel(ConsistencyLevelEnum.STRONG)
|
||||
.build());
|
||||
|
||||
if (resp.getStatus() == 0) break;
|
||||
System.out.println("search retry " + (retry + 1) + ": status=" + resp.getStatus() + ", msg=" + safeMsg(resp));
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
|
||||
System.out.println("search status: " + resp.getStatus() + ", msg: " + safeMsg(resp));
|
||||
assertEquals(0, resp.getStatus(), "搜索失败");
|
||||
|
||||
SearchResultsWrapper wrapper = new SearchResultsWrapper(resp.getData().getResults());
|
||||
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0);
|
||||
|
||||
assertFalse(scores.isEmpty(), "搜索结果不应为空");
|
||||
System.out.println("搜索结果 (top " + scores.size() + "):");
|
||||
for (SearchResultsWrapper.IDScore idScore : scores) {
|
||||
System.out.println(" score=" + idScore.getScore() + ", id=" + idScore.getLongID());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Float> makeVector(float val) {
|
||||
Float[] arr = new Float[DIM];
|
||||
Arrays.fill(arr, val);
|
||||
return Arrays.asList(arr);
|
||||
}
|
||||
|
||||
private static String envOrDefault(String key, String defaultVal) {
|
||||
String val = System.getenv(key);
|
||||
return (val != null && !val.isEmpty()) ? val : defaultVal;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user