Java AI 知识库:从向量检索到带引用的模型回答
把文档交给模型,并不等于做完知识库。文档更新后,旧向量还会不会命中?低权限用户能否通过提问读到管理员内容?模型给出的引用,是否真来自这次检索到的分块?这个项目把这些问题放进同一个 Java HTTP 服务中,沿请求阅读处理顺序和失败边界。
项目采用 Java 21、Spring Boot 4.0.0、H2,通过兼容 OpenAI 的 HTTP 接口调用嵌入模型和对话模型。H2 保存文档、版本、分块与向量,相似度在服务内计算。它适合阅读完整链路和本地小数据实验;大规模检索需要另外设计索引、分片与容量。
一条 RAG 请求经过什么
文档写入先分块并调用嵌入接口,再进入短事务提交。检索先把问题变成向量,只在当前用户可读的文档中比较相似度。生成接口接收到的是这一轮授权证据;模型返回以后,服务再检查文档是否仍然可读、版本是否仍然有效。
这里有两个重要顺序:外部模型调用放在数据库事务外,权限判断放在服务端。 前者避免等待模型时一直占着数据库锁;后者避免让提示词承担业务授权。
分块、向量与当前版本
分块上限为 800 个 Unicode 码点,步长 700,因而相邻块最多重叠 100 个码点。按码点切分可以避免把一个表情的 UTF-16 代理对拆开。它仍然是简单的长度切分;标题、段落和代码结构需要更细的文档解析器处理。
| 数据 | 用途 |
|---|---|
| kb_documents | 租户、当前版本、删除状态 |
| kb_versions | 每个版本的正文、标题和最低访问角色 |
| kb_chunks | 文档版本、分块号、文本、向量和嵌入模型标识 |
更新请求携带 expectedVersion。服务先检查目标版本,生成新分块向量,再在提交事务中锁定文档并复核版本。如果另一请求已经先提交,后来的请求得到 409,不能覆盖新版本。检索用文档当前版本连接分块,历史正文保留用于查看修改记录,不作为当前检索来源。
删除采用逻辑删除并清理该文档向量。删除后,即使曾经检索到该文档,生成结束前的状态复核也会阻止继续返回它。索引身份绑定供应商地址、嵌入模型名称和 EMBEDDING_INDEX_REVISION 修订号。切换提供商、模型或修订号后,旧向量不会混入新查询,需要更新文档重新索引。同一地址下的同名模型权重若改变,应主动递增修订号;服务无法自动感知供应商内部更新。
权限过滤发生在哪一层
登录取得不透明 Bearer token;每个请求根据会话从数据库读取租户和角色。客户端传入一个 tenant 或让模型“切换管理员模式”,都不能改变这份身份。
角色分为 READER、EDITOR、ADMIN。读者不能写文档,文档通过最低角色控制可见范围。其他租户的同一文档 ID 按不可见处理。查询、模型上下文构建和最终返回都需要遵守这套边界。
模型调用可能持续数秒。期间文档可能被更新、删除,用户权限也可能变化,因此服务会在返回前重新读当前权限和文档状态。这个检查缩短了陈旧授权窗口;它不意味着数据库更新与网络响应之间存在一个跨系统的可串行化事务。
为什么要核对精确引用
每个证据分块带有来源标记:
[文档ID@v版本号#分块号]服务建立本次允许的标记集合,模型回答至少要引用一个完整标记。符合标记格式但不属于这个集合的文档、版本或分块,会使回答被拒绝。最终 citations 只返回实际使用的授权来源,包含文档 ID、版本、标题、原文和分数。
引用合法性不等于语义正确性。 一个回答带着真实引用,仍可能曲解原文、扩大结论或在其他句子中添加无依据内容。这里实现的是可追踪来源和明确拒答;真实模型的语义支持度、提示注入抵抗和回答质量,需要用具体模型及题集另外评估。可以结合 RAG 评估方法继续分析。
模型适配与失败处理
模型适配器调用两个接口,密钥只从进程环境读取,不写入前端或错误响应:
| 调用 | 请求重点 | 失败处理 |
|---|---|---|
| POST /embeddings | model、input | 检查向量数组、数值和非零范数 |
| POST /chat/completions | model、messages | 检查回答内容,再核对授权引用 |
| 未配置供应商 | MODEL_BASE、CHAT_MODEL、EMBEDDING_MODEL | 返回 503,保留明确错误 |
| 上游限流或异常 | 429、其他非 2xx | 映射为 503 / 502,不回显供应商正文 |
| 超时或取消 | 请求超时、线程中断 | 映射为 504 / 取消结果 |
当前没有自动重试。给慢请求机械加重试,可能增加模型成本并拖垮下游;扩展时先确定整体时间预算、可重试条件和取消传播,参见接口超时预算。
本地启动与接口入口
解压后进入 ai-knowledge-service 目录。需要 JDK 21、Maven 3.9+,首次运行由 Maven 解析依赖。
# Windows
.\start.cmdLinux / macOS 使用 sh start.sh。默认监听 127.0.0.1:8086,H2 数据文件写在项目自己的 data 目录。演示控制台用于调用本地 API。
在启动进程前配置供应商;下面仅展示本地兼容接口的地址形式,模型名称需替换成实际已提供的名称:
$env:MODEL_BASE = 'http://127.0.0.1:11434/v1'
$env:CHAT_MODEL = '你的对话模型名称'
$env:EMBEDDING_MODEL = '你的嵌入模型名称'
# 需要认证时,通过进程环境设置 API_KEY
.\start.cmd.env.example 是参数说明,启动脚本不会自动加载 .env。没有供应商时健康检查仍可检查数据库,但文档嵌入、检索和问答会明确返回未配置,便于区分服务启动与模型就绪。
| 接口 | 作用 |
|---|---|
POST /api/auth/login | 登录,返回 token |
POST /api/documents | 创建文档并生成分块向量 |
PUT /api/documents/{id} | 带 expectedVersion 更新 |
DELETE /api/documents/{id} | 带 expectedVersion 删除 |
POST /api/search | 返回授权证据与来源 |
POST /api/ask | 生成回答并复核来源 |
POST /api/ask-stream | SSE 返回 request、answer、done |
DELETE /api/ask-stream/{requestId} | 取消当前用户的请求 |
示例账号、请求体和错误码见源码包内 API.md。默认演示 admin 密码为 demo-admin-local,用于本地阅读与测试。
SSE、取消与测试范围
SSE 先返回请求 ID,再返回完整回答事件,最后发送 done。它实现的是事件流,当前没有逐 token 转发供应商响应。前端用 fetch 读取 POST 流,使用显式取消接口中断对应任务;连接断开在服务器检测到后触发清理。
测试使用随机本机 HTTP fixture、显式 test profile 和隔离 H2,实际经过 HTTP 认证、模型协议、数据库持久化、检索与生成。fixture 中的向量和回答是确定的测试数据,用于检查请求字段、权限、版本、异常和取消行为。它们不代表真实模型语义能力。完整执行结果与源码哈希见项目测试报告。
源码阅读顺序
Auth.java:token、会话和当前权限。Model.java:真实 HTTP 适配器、超时与错误映射。Knowledge.java:分块、版本事务、相似度与引用复核。Api.java:REST、SSE 和取消任务。src/test:正常请求和独立边界用例。
