README · demo 2
AI Agent Demo 工程说明
这个目录是一个最小 Spring Boot demo,不是完整 Agent 产品。它把笔记里的 RAG / Trace / Agent API 思路落到一个可运行后端切片:本地 Markdown 检索、基于来源的 deterministic 回答、trace 查询。
当前结构
| 路径 | 作用 | 当前状态 |
|---|---|---|
pom.xml | Maven 项目定义,Spring Boot 4.1.0,Java 17 | 基础 starter、web、test |
src/main/java/com/example/demo/DemoApplication.java | 应用入口 | 默认启动类 |
src/main/java/com/example/demo/agent | Agent API 编排 | 请求、回答、来源、trace 串联 |
src/main/java/com/example/demo/notes | 本地 Markdown 检索 | 扫描 技术栈/AI Agent 笔记并返回片段 |
src/main/java/com/example/demo/trace | trace 存储 | 内存索引 + JSONL 持久化每次请求的阶段记录 |
src/main/java/com/example/demo/eval | 固定用例评测 | 检查拒答、来源数量、答案关键词 |
src/main/java/com/example/demo/tool | 工具契约与执行 | search/read/trace/debug/eval/memory 的 tool schema |
src/main/java/com/example/demo/memory | 记忆存储 | MemoryRecord / MemoryWriteRequest / JSONL 追加存储 |
src/main/java/com/example/demo/mcp | MCP JSON-RPC transport | initialize、tools/list、tools/call 的最小包装 |
src/main/java/com/example/demo/web | REST 接口 | /API/chat、/API/notes/*、/API/tools/*、/API/traces/{id}、/API/eval |
src/main/resources/application.properties | 配置入口 | agent.notes-root=..、agent.memory-path、agent.trace-path |
src/test/java/com/example/demo | 测试 | context、search、agent/refusal/trace |
eval-cases.json | 固定评测用例 | smoke 后跑 eval gate |
接口
| 接口 | 方法 | 作用 | 验收点 |
|---|---|---|---|
/API/notes/search?q=RAG&limit=5 | GET | 检索本地 Markdown | 返回 path/title/score/snippet |
/API/notes/read?path=README.md | GET | 安全读取一篇 Markdown | 返回 path/title/content |
/API/chat | POST | 输入问题,返回带来源回答 | 返回 traceId/refused/answer/sources/trace |
代码块收起展开
| `/API/traces/{traceId}` | GET | 查看一次请求的 trace | 能看到检索、拒答或回答阶段 |
| `/API/traces/{traceId}/debug` | GET | 查看 trace 派生诊断报告 | 能看到状态、失败归因、证据数量、下一步建议 || /API/eval | POST | 跑固定用例 | 返回 total/passed/failed/results |
| /API/tools | GET | 查看工具 schema | 返回带 readOnly 标记的 tool definitions |
| /API/tools/call | POST | 调用工具 | 返回 ok/result/error |
| /API/memories | GET/POST | 查询或写入 demo memory | id/type/content/source/confidence/createdAt |
| /mcp | POST | MCP JSON-RPC 入口 | initialize、tools/list、tools/call |
/API/chat 示例:
代码块收起展开
{
"message": "MCP 的 tool schema 为什么重要?",
"limit": 3
}/mcp 当前对齐 MCP 2025-11-25 稳定规范的工具子集,参考:Model Context Protocol specification。它只实现教学必需的三类 JSON-RPC 方法:
| MCP method | 当前行为 | 底层实现 |
|---|---|---|
initialize | 返回协议版本、serverInfo、tools capability | McpService.initializeResult() |
tools/list | 暴露 search_notes/read_note/get_trace/debug_trace/run_eval/list_memories/write_memory | ToolService.definitions() |
tools/call | 调用工具,失败时返回 isError=true | ToolService.call() |
它暂时不做 session、resources、prompts、sampling、完整 Streamable HTTP 细节;这些属于后续生产化 MCP。
返回字段:
| 字段 | 含义 |
|---|---|
traceId | 一次请求的唯一 ID,用于复盘 |
refused | 是否因为没有证据而拒答 |
answer | deterministic demo 回答,不代表真实 LLM |
sources | 本次回答使用的笔记来源 |
trace | validate / retrieve / answer / refuse 等阶段 |
代码阅读顺序
| 顺序 | 文件 | 先看什么 | 读懂后应该能回答 |
|---|---|---|---|
| 1 | web/AgentController.java | 三个 REST 入口如何分流 | HTTP 请求怎么进入应用层 |
| 2 | agent/ChatRequest.java | message、limit 和 effectiveLimit() | 外部输入在哪里被收紧 |
| 3 | agent/AgentService.java | validate -> retrieve -> answer/refuse -> save trace | Agent 最小运行链路是什么 |
| 4 | notes/NoteSearchService.java | terms()、score()、snippet() | 本地笔记怎样被召回和排序 |
| 5 | trace/TraceService.java | save()、find()、loadExisting()、JSONL 边界 | 为什么服务重启后仍能复盘 |
| 6 | trace/DebugReportService.java | report()、issue()、recommendations() | 怎么把 trace 转成失败归因和下一步动作 |
| 7 | notes/NoteReadService.java | resolve()、read() | 怎么防止路径逃逸和未授权读取 |
| 8 | tool/ToolService.java | definitions()、call() | search/read/trace/debug/eval 如何变成 tool |
| 9 | mcp/McpService.java | initializeResult()、toolsListResult()、callTool() | ToolService 怎么映射成 MCP |
| 10 | memory/MemoryService.java | write()、list()、append()、readAll() | 记忆如何校验、落盘、再查询 |
| 11 | eval/EvalService.java | run()、reasons() | 怎么用固定用例抓退化 |
| 12 | AgentControllerTests.java / McpControllerTests.java / MemoryControllerTests.java | MockMvc 端到端断言 | 接口层是否真的串起来 |
核心字段和函数
| 类 | 字段/函数 | 作用 | 后续升级点 |
|---|---|---|---|
ChatRequest | message | 用户问题原文 | 加入 session/userId/taskType |
ChatRequest | limit / effectiveLimit() | 控制检索证据数量 | 变成 context budget 计算 |
AgentResponse | refused | 标记是否因为缺少证据拒答 | 加 refusalReason |
AgentResponse | sources | 回答引用的证据 | 加 chunkId、scoreDetail、line range |
AgentService | answer() | 单次 Agent 请求主流程 | 接入 LLM、planner、tool call |
NoteSearchService | search() | 扫描 Markdown 并返回排序片段 | 替换为 embedding/vector/hybrid search |
NoteSearchService | score() | 关键词评分 | 替换为 BM25 + rerank |
NoteReadService | read() | 只读可检索 Markdown,并阻止路径逃逸 | 加 line range 和 frontmatter |
TraceService | save() / find() / loadExisting() | 保存、查询、重载请求复盘记录 | 替换为数据库或 OpenTelemetry |
DebugReport | status / issue / failedPhase / recommendations | 把原始 trace 变成可读诊断结果 | 加检索质量、prompt 版本、tool error 分类 |
DebugReportService | report() | 根据 traceId 生成失败归因和下一步建议 | 加 trace 对比、聚合统计、报告落盘 |
EvalCase | expectRefused / minSources / requiredAnswerTerms | 固定质量门槛 | 加 latency、forbiddenTerms、sourcePath 断言 |
EvalService | run() | 批量跑用例并输出 pass/fail | 加报告落盘和 CI gate |
ToolDefinition | inputSchema / readOnly | 描述工具参数和风险边界 | 映射成 MCP tool schema |
ToolService | definitions() / call() | 工具注册与统一执行入口 | 加权限确认、超时、审计 |
McpRequest | jsonrpc / id / method / params | MCP/JSON-RPC 请求对象 | 加 notification 和 session |
McpService | handle() / mcpTool() / toolResult() | 把本地工具映射成 MCP initialize/list/call | 加 resources、prompts、streamable HTTP |
MemoryWriteRequest | type / content / source / confidence | 记忆写入命令,要求说明类别、内容、来源、置信度 | 加 userId、traceId、ttl、confirmPolicy |
MemoryRecord | id / createdAt | 已落盘记忆证据,方便 trace/tool/MCP 返回引用 | 加 version、updatedAt、supersedes |
MemoryService | write() / list() | JSONL 追加写入和轻量查询 | 替换为数据库、索引、撤销和冲突合并 |
AgentControllerTests | chatEndpointReturnsSourcesAndReadableTrace() | 验证 HTTP -> Agent -> Search -> Trace | 后续加错误码、超时、权限测试 |
适合扩展的方向
| 阶段 | 新增模块 | 验收门槛 |
|---|---|---|
| 1 | ChatController + AgentService | 已完成:结构化响应、拒答、sources、trace |
| 2 | TraceService + DebugReportService | 已完成:每次请求有 traceId、阶段、耗时、JSONL 持久化、debug report |
| 3 | NoteSearchService | 已完成:检索结果带来源、分数、引用片段 |
| 4 | EvalService | 已完成:固定用例检查拒答、来源、答案关键词 |
| 5 | NoteReadService | 已完成:安全读取可检索 Markdown,阻止路径逃逸 |
| 6 | ToolService | 已完成:工具 schema、读写分级边界、结构化错误 |
| 7 | McpService | 已完成:最小 MCP JSON-RPC 包装,复用当前 tool registry |
| 8 | MemoryService | 已完成:JSONL 追加写入,记录 source/type/confidence/createdAt |
失败模式
| 失败 | 表现 | 处理 |
|---|---|---|
| 只写 Controller | 业务逻辑散在接口层 | 把 Agent 流程放进 service |
| 无 trace | 回答错了无法复盘 | 每次请求保存 trace |
| 无 schema | 工具参数不可控 | 工具输入先建 DTO / JSON Schema |
| 读取越界 | ../outside.md 之类路径逃逸 | service 拦截,HTTP 返回 400,tool 返回结构化 error |
| 无测试 | demo 一改就坏 | 至少保留 context load 和 service 单测 |
本地运行
代码块收起展开
cd "F:\2. ObsidianNotes\技术栈\AI Agent\demo"
.\mvnw.cmd test
.\mvnw.cmd spring-boot:run如果本机默认 JAVA_HOME 不可用,先临时指定可用 JDK,再运行 Maven。
代码块收起展开
$env:JAVA_HOME='D:\JAVA_TechTool\JDKs\oracle-24.0.1'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
.\mvnw.cmd test启动服务后,用 smoke 脚本验证三条接口链路:
代码块收起展开
.\mvnw.cmd spring-boot:run
.\smoke-test.ps1
.\eval-test.ps1
.\tool-test.ps1
.\mcp-test.ps1
.\memory-test.ps1
.\trace-test.ps1
.\debug-test.ps1如果服务跑在其他端口:
代码块收起展开
.\smoke-test.ps1 -BaseUrl "http://localhost:18080"
.\eval-test.ps1 -BaseUrl "http://localhost:18080"
.\tool-test.ps1 -BaseUrl "http://localhost:18080"
.\mcp-test.ps1 -BaseUrl "http://localhost:18080"
.\memory-test.ps1 -BaseUrl "http://localhost:18080"
.\trace-test.ps1 -BaseUrl "http://localhost:18080"
.\debug-test.ps1 -BaseUrl "http://localhost:18080"smoke 脚本必须同时证明三件事:
| 检查 | 证明 |
|---|---|
/API/notes/search 有结果 | Markdown 检索能读到本地笔记 |
/API/chat 有 sources 和 traceId | 回答不是无来源编造 |
/API/traces/{traceId} 有 steps | 请求能复盘阶段 |
eval 脚本再证明固定质量门槛没有退化:
| 检查 | 证明 |
|---|---|
expectRefused | 该拒答时拒答,该回答时不拒答 |
minSources | 回答必须有足够来源 |
requiredAnswerTerms | 关键概念没有从答案中消失 |
eval-test.ps1 会把 JSON body 转成 UTF-8 bytes 再发送,因为 eval-cases.json 里可能包含中文断言词;直接用 PowerShell 5 的字符串 body 容易把中文发成 ?。
tool 脚本证明工具契约和安全边界:
| 检查 | 证明 |
|---|---|
/API/tools 有 read_note schema | 工具能力可以被 Agent 发现 |
read_note 能读取 README.md | 工具执行能返回真实资源 |
read_note 拒绝 ../outside.md | 工具不会绕过 notes root |
mcp 脚本证明 transport 层没有绕开工具边界:
| 检查 | 证明 |
|---|---|
initialize 返回 2025-11-25 | demo 对齐当前稳定 MCP 协议版本的最小握手 |
tools/list 返回 read-only annotations | 外部 Agent 可以区分只读工具和写入工具 |
tools/call read_note 成功/失败都结构化 | MCP 层复用 ToolService 的安全边界 |
memory 脚本证明记忆写入边界:
| 检查 | 证明 |
|---|---|
| /API/memories 写入并查询 | memory 不是只存在内存里的临时变量 |
| write_memory/list_memories 工具调用 | 外部 Agent 能通过 tool contract 使用记忆 |
| data/ 被 git 忽略 | demo 运行数据不会污染笔记仓库 |
trace 脚本证明复盘入口一致:
| 检查 | 证明 |
|---|---|
/API/chat 产生 traceId | 每次回答都有复盘入口 |
/API/traces/{traceId} 读回 trace | HTTP 层能复盘一次请求 |
get_trace tool / MCP call | Tool/MCP 层复用同一份 trace store |
debug 脚本证明 trace 能转成诊断:
| 检查 | 证明 |
|---|---|
/API/chat 产生拒答 trace | demo 有可复现失败样例 |
/API/traces/{traceId}/debug 返回 issue=no_sources | trace 能定位到检索证据不足 |
debug_trace tool / MCP call | 外部 Agent 能拿到同一份诊断报告 |
边界
这个 demo 暂时不做真实 LLM、向量库、数据库、完整生产 MCP 会话/resources/prompts 和任意文件写入工具。当前目标是先证明一条可测试、可复盘、可评测、可工具化、可 MCP 包装的后端链路:
代码块收起展开
HTTP request -> AgentService -> NoteSearchService -> grounded answer/refusal -> TraceService
EvalCase -> EvalService -> AgentService -> pass/fail reasons
ToolCallRequest -> ToolService -> search/read/trace/debug/eval -> ToolCallResponse
McpRequest -> McpService -> ToolService -> McpResponse
MemoryWriteRequest -> MemoryService -> data/memories.jsonl
TraceRecord -> TraceService -> data/traces.jsonl
TraceRecord -> DebugReportService -> DebugReport