Compare commits

..
Author SHA1 Message Date
zhuyongxin d4b5015beb commit 2026-05-29 21:38:16 +08:00
53 changed files with 3309 additions and 7842 deletions
@@ -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
```
-1
View File
@@ -54,4 +54,3 @@ uploads/
### docker
/volumes
/server.pid
.claude/settings.local.json
+2 -2
View File
@@ -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 -->
+2 -116
View File
@@ -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 -->
+29
View File
@@ -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
+7
View File
@@ -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 直接替换无问题
-81
View File
@@ -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
View File
@@ -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/>&gt; maxSize?"}
CHECK -->|否| APPEND["追加段落<br/>继续累积"]
CHECK -->|是| L4{"第4层: getOverlapText()<br/>句子边界校准"}
APPEND --> CHECK
L4 --> FIND["在重叠区末尾100字符<br/>找最近的 。?!"]
FIND --> EVAL{"句子边界位置<br/>&gt; overlapSize/2?"}
EVAL -->|是| ALIGN["从句号后截取<br/>保证新块以完整句开头"]
EVAL -->|否| RAW["退回原始截取<br/>直接用末尾100字符"]
ALIGN --> SEED["种子 + 当前段落<br/>→ 新缓冲区"]
RAW --> SEED
SEED --> CHECK
CHUNK --> RESULT[/"List&lt;DocumentChunk&gt;<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
```
+314
View File
@@ -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
```
+352
View File
@@ -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 团队,这是一个很好的学习起点和脚手架。
-529
View File
@@ -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
-715
View File
@@ -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。
-211
View File
@@ -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%
- 评估:用户反馈
```
-210
View File
@@ -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
-155
View File
@@ -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 文档
- 重大变更需记录在版本历史中
+282
View File
@@ -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 标题)。
+200
View File
@@ -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 解析替代正则 | 低优先级 |
-332
View File
@@ -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(标签分类)
```
-265
View File
@@ -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)
❌ 案例合并功能
```
-240
View File
@@ -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%+)
+5 -37
View File
@@ -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;
}
}
/**
* 章节数据类
*/
+25 -56
View File
@@ -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;
}
}