BISHENG 知识空间 AI 问答检索权限过滤(F029):双层 view_file 过滤架构与落地实现
【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng
导读
本文围绕 BISHENG v2.6.0 特性F029-knowledge-qa-permission-filter(知识空间 AI 问答 - 检索权限过滤)展开,讲解其如何解决"列表 UI 看不到的文件,却能被 AI 问答检索到"这一越权读取风险。文中以 spec.md 与 tasks.md 为骨架,结合仓库真实源码,完整呈现双层过滤架构(AD-01/02/03/08)、KnowledgeFileVisibilityService的实现细节、可配置参数、四大问答入口(整空间 / 文件夹 / 文件预览 / 首页与工作台多 KB)与角标溯源接口的改造,以及 Test-First 测试策略与 E2E 回归清单。读完本文,你将掌握 BISHENG 如何在不新增 OpenFGA 关系、不新增数据库表的前提下,用"索引层粗滤 + 结果层精滤"保证 AI 问答可见性与列表 UI 可见性严格一致。
1. 背景:AI 问答为什么成为越权读取的入口
知识空间内,用户可能对某个空间有view_space权限,但只对其中部分文件有view_file权限。改造前,知识空间 AI 问答的检索链路(KnowledgeSpaceChatService、工作台WorkStationService.queryChunksFromDB)只做空间级鉴权,检索召回出的 chunk 没有按文件级权限过滤,导致:
- 普通用户在空间页发起问答,回答引用的文件名、来源、预览链接可能暴露无权文件;
- 历史会话的角标溯源(citation resolve)接口在用户失去
view_file后仍返回结构化字段,构成新的越权读取路径; - 首页 / 工作台多 KB 检索以
kb_id_whitelist+check_auth=False跳过 per-KB 鉴权,可被构造请求绕过下拉框。
该特性被定义为P0 优先级,必须在 v2.6.0 关闭这条越权路径。
1.1 范围边界(本期纳入 / 排除)
本期纳入:知识空间 AI 助手的 4 个问答入口(整空间 / 文件夹 / 文件预览 / 首页 & 工作台多 KB 检索)+ 新版角标溯源接口(POST /api/v1/citations/resolve批量、GET /api/v1/citations/{citation_id}单条)。
本期明确排除:
- 工作流
KNOWLEDGE_RETRIEVER节点(workflow/nodes/knowledge_retriever/)——涉及流程节点执行上下文中的"运行用户身份"问题,留待后续 feature; - 对外 RPC
/api/v2/filelib/retrieve——当前以"默认 operator"身份运行而非真实终端用户,需先定义"代用户检索"协议; - OpenFGA 模型变更——不引入新的
view_fileReBAC 关系,复用现有can_read与view_filepermission_id; - chunk 元数据 ACL 索引化("超大规模"方案)——单空间单用户可见文件 > 10 万时所需的索引层 ACL 字段方案不在本期,本期通过"双层过滤 + 检索次数封顶"在 10 万规模内提供可接受性能。
2. 核心设计:双层过滤架构(AD-01 / AD-02 / AD-03 / AD-08)
2.1 语义对齐基线(AD-01):为什么不能只用单一权限
架构决策 AD-01 在三个选项中选择了双层过滤(选 C):
| 方案 | 问题 |
|---|---|
A:仅用 ReBACcan_read | 会导致"列表看不到但问答能查到"(越权) |
B:仅用 fine-grainedview_file | 10 万规模冷启动需要对每个文件单独 OpenFGA tuple 读取,单空间一次性解析耗时不可接受 |
| C:双层(选) | can_read在 Milvus/ES 端把候选缩到至少有 ReBAC 读权限的文件(一次list_objects+ Redis 缓存),再在召回结果的 unique file_id(一般规模 ≤ 30 个)上跑 fine-grainedview_file解析,保证最终送进 LLM 的 chunks必然满足列表 UI 可见性 |
这一设计的目标不变量(INV-7,登记在 release-contract.md 表 2):
知识空间内容的"AI 问答可检索可见性"必须是"列表 UI 可见性"的子集;即对任意
(user, space, file),若用户在列表 UI 中不可见该file(view_file ∉ effective_permissions),则任何 AI 问答入口都不得让该file的 chunk / 文件名 / 来源出现在模型上下文、回答引用、角标溯源/api/v1/citations/resolve响应的结构化字段中。
2.2 索引层过滤策略(AD-02):自适应 IN / NOT-IN / 不过滤
以可见集合大小K与空间内主版本文件总数N为决策依据:
K ≤ 5000→IN(可见集合);N − K ≤ 5000→NOT IN(排除集合)(排除集合 = 非主版本 ∪ 不可见);- 二者均 > 5000 → 不下推索引过滤,仅靠"扩大候选 + 结果层精滤"。
设计理由:ESterms默认上限 65536;Milvus 长表达式解析在 ≤ 5000 内表现稳定;自适应让 95% 业务走快路径,极端规模有兜底。阈值 5000 写为可配置常量KnowledgeQAFilterConf.index_filter_threshold。
2.3 结果层扩展策略(AD-03):最多 2 次检索
- 首轮按
top_k × initial_multiplier(默认 3)召回; - 若过滤后 <
top_k且首轮命中文件数 > 0,进行至多 1 次扩张到top_k × expansion_multiplier; - 仍不足则返回已有结果(可少于
top_k),不存在"凑齐 top_k 的无限扩张"。
该策略保证最坏情况下也只是 2 次 Milvus + 2 次精滤批量调用,单次问答额外延迟 ≤ 400ms,可观测可控。
2.4 结果层精滤并发与缓存(AD-08)
复用列表 UI 已验证的_build_child_permission_context(含tuple_cache)+ 同一 semaphore 8 并发上限(与_CHILD_PERMISSION_CHECK_CONCURRENCY默认一致);空间维度的权限上下文在结果集少(≤ 30 个 file_id)时近乎一次性构建。
3. 核心服务源码解析:KnowledgeFileVisibilityService
新增服务文件位于 knowledge_file_visibility_service.py,集中"可见文件集合"的解析,避免逻辑散落在 chat service 各处。它由 FastAPI 依赖工厂(knowledge/api/dependencies.py 中的get_knowledge_file_visibility_service)构造,构造函数注入request: Request, login_user: UserPayload。
3.1IndexFilter:索引层过滤产物
@dataclass class IndexFilter: """Index-layer filter to be injected into Milvus / ES search_kwargs.""" strategy: str # in | notin | none | empty milvus_expr: str | None = None es_filter: list | None = None accessible_size: int = 0 excluded_ids: list[int] = field(default_factory=list) @property def is_empty(self) -> bool: return self.strategy == "empty"四种策略语义(源码 docstring 明确定义):
in——document_id in [visible ids],适用于小可见集;notin——document_id not in [excluded ids],适用于几乎全可见;none—— 不过滤,要么是 admin 调用者,要么两侧规模都过大(由结果层精滤兜底);empty—— 用户在该空间可见文件数为 0,调用方必须直接跳过检索。
3.2is_space_visible(space_id) -> bool(AC-11)
non-throwing 版本,复用KnowledgeSpaceService._require_permission_id('knowledge_space', space_id, 'view_space')的判定逻辑,捕获SpacePermissionDeniedError(错误码 18040,复用不新增)返回False。admin 用户由底层PermissionService短路返回True。该方法是首页 / 工作台多 KB 检索"无view_space的 KB 静默跳过"(AC-11)的判定来源。
3.3build_index_prefilter(space_id, candidate_file_ids) -> IndexFilter
AD-02 策略决策的核心实现,关键逻辑如下(源码级):
- 调
PermissionService.list_accessible_ids(user_id, 'can_read', 'knowledge_file', login_user)拿到用户租户级可读文件集(admin 返回None); - 拉取该空间主版本 file_id 集合(排除非主版本,复用
version_repo.find_non_primary_file_ids_by_knowledge_ids),计算交集K = accessible_ids ∩ candidate (∩ space_files if candidate=None); - 按阈值选策略并返回
IndexFilter。
源码中一个值得注意的细节是业务范围的正确性优先:candidate_file_ids(文件夹 / tag 业务范围)没有结果层兜底(post_filter_visible_files只强制view_file),因此只要传入了 candidate,就必须下推到索引层,不能走 admin 或"两侧过大"的none短路——大 IN 列表优于泄漏到范围之外。
3.4post_filter_visible_files(space_id, file_ids) -> Set[int]
结果层精滤,保证"最终送 LLM 前必经":
- admin → 返回输入集合(短路);
- 空输入 → 在构建权限上下文前短路;
- 一次性
_build_child_permission_context(space_id)拿共享 binding / tuple_cache / membership 上下文; - semaphore(
fine_grained_concurrency默认 8)并发跑_get_child_item_effective_permission_ids,保留view_file ∈ effective的 file_id。
源码 docstring 特别强调:委托调用应用了 per-item lineage walk(file → 祖先文件夹 → space)、nearest_binding_wins=True语义(文件级 revoke 优先于空间成员默认)、membership 默认权限与 public-space viewer 默认——若缺少这些参数,早期版本会返回所有 binding 的并集,让被 revoke 的文件泄漏过滤,这正是用户报告的 bug。测试 test_knowledge_file_visibility_service.py 中的test_post_filter_visible_files_regression_revoke_overrides_membership即该回归场景的验证。
4. 配置参数:KnowledgeQAFilterConf
配置块定义于 core/config/settings.py,挂在Settings.knowledge_qa_filter(settings.py第 740 行)顶层为可选块,YAML 缺省时按默认值。
class KnowledgeQAFilterConf(BaseModel): """Knowledge space AI Q&A retrieval permission filter (F029).""" index_filter_threshold: int = Field( default=5000, ge=1, description="AD-02 threshold. ...当可见或排除文件数 ≤ 该值走 IN / NOT-IN,否则仅后过滤", ) retrieval_initial_multiplier: int = Field( default=3, ge=1, description="AD-03 first attempt. 首轮召回 top_k * 该倍数", ) retrieval_expansion_multiplier: int = Field( default=6, ge=1, description="AD-03 capped expansion. 不足 top_k 时单次重试召回 top_k * 该倍数,不再扩张", ) fine_grained_concurrency: int = Field( default=8, ge=1, le=64, description="AD-08 concurrency. 结果层 view_file 解析的 semaphore 并发上限", ) @model_validator(mode="after") def validate(self): if self.retrieval_expansion_multiplier < self.retrieval_initial_multiplier: raise ValueError("retrieval_expansion_multiplier must be >= retrieval_initial_multiplier") return self参数速查表:
| 参数 | 默认值 | 约束 | 作用 | 关联决策 |
|---|---|---|---|---|
index_filter_threshold | 5000 | >= 1 | 索引层 IN / NOT-IN 切换阈值 | AD-02,AC-23/24/25 |
retrieval_initial_multiplier | 3 | >= 1 | 首轮召回倍数 | AD-03 |
retrieval_expansion_multiplier | 6 | >= initial | 单次扩张召回倍数,扩张后封顶 | AD-03,AC-26 |
fine_grained_concurrency | 8 | 1..64 | 结果层精滤并发 | AD-08 |
实现偏差提示:spec.md 中
retrieval_expansion_multiplier写的是默认 10,而实际提交到源码的默认值是6。源码注释说明改为 6 是为了控制重试搜索成本(base_k=100 时 k=600),且 Milvus wrapper 会提高 ef 覆盖该 k 值。这也是 tasks.md「实际偏差记录」一节建议登记的内容。
5. 四个问答入口的改造
5.1 整空间 / 文件夹问答:KnowledgeSpaceChatService
knowledge_space_chat_service.py 的改造要点:
- 将
_build_folder_search_kwargs拆为_compute_candidate_file_ids(knowledge_id, folder_id, tags)(保留原 candidate 计算逻辑)+ 调build_index_prefilter; - 新增
_retrieve_and_filter(space, query, candidate, max_content) -> List[Document](对应 spec §7.4 五步流程); chat_folder替换原milvus_kwargs/es_kwargs构造为_retrieve_and_filter调用;space_rag将retriever_tool.ainvoke调用迁入_retrieve_and_filter,移除原 retriever_tool 直接拼接逻辑,保留 prompt + LLM 调用;chat_single_file在space_rag之前增加防御性post_filter_visible_files({file_id})一次(已通过门禁的请求应必中;不中则记 WARN 并返回空,避免越权);_render_rag_response从space_rag中抽出,供chat_folder复用不变的 prompt + 流式路径。
_retrieve_and_filter的关键实现(源码第 480-566 行):
- 先
build_index_prefilter,若index_filter.is_empty则直接返回空(并写一条strategy=empty的结构化日志); - 对
(initial_multiplier, expansion_multiplier)二元组循环,每轮以base_k × multiplier构造 Milvus/ES search_kwargs(base_k=100,ef=110),注入milvus_expr/es_filter; - 调用
KnowledgeRetrieverTool.ainvoke(query)后抽 unique file_id,走post_filter_visible_files得view_file子集,过滤 chunks; - 一旦某轮 survivors 非空立即 break(AD-03 封顶 = 2 次尝试);
- 每轮结束写一条结构化日志(AC-27 字段)。
5.2 首页 / 工作台多 KB 检索:WorkStationService.queryChunksFromDB
workstation_service.py 按 spec §7.2b 逐项落地:
- Stage 1(AC-11):进入循环前对
space_bucket的每个 kb_id 调is_space_visible(kb_id);不通过则continue+ INFO 日志skipped_kb_id=%s reason=no_view_space(源码第 1017-1031 行)。该 KB 静默跳过、不抛错、不阻塞其他 KB、不出现在kb_succeed列表; - Stage 2(索引层粗滤):构造
MultiRetriever的search_kwargs时注入build_index_prefilter(kb_id, None)的产物,与chat_folder路径一致。不做显式预检测"用户在该 KB 是否有可见文件"——若accessible_ids与 KB 文件集交集为空,Milvus / ES 查询自然返回 0 chunks(AC-12); - Stage 3(结果层精滤):
per_kb_tool.ainvoke返回后对kb_docs走post_filter_visible_files(kb_id, unique_file_ids);过滤后空则不进入finally_docs/ 不计入kb_succeed(源码第 1085 行); - 沿用 AD-03 检索循环上限(首轮 ×3 / 扩张 ×10),多 KB 间不互相补量;
org_bucket(legacyknowledge_library)路径完全不改(AC-14),check_auth=False不动(与本特性正交)。
5.3 角标溯源:CitationResolveService
citiy_resolve_service.py 的改造:
- 删除
_has_file_access静态方法(旧 RBACRoleAccessDao+AccessType.KNOWLEDGE空间级判定,是 arch-guard RULE-8 VIOLATION)及其对from bisheng.database.models.role_access import AccessType的导入; - 新增
_permitted_file_ids(items, login_user):login_user is None(匿名)返回None表示不过滤(AC-20 保持现状);否则按(knowledgeId, documentId)分组,对每个 knowledgeId 调post_filter_visible_files,扁平合并为允许集合; _apply_tier_filter:web 类型 citation 永远通过(AC-19);per_userRAG citation 在文件不满足view_file时整条剔除;sharedRAG citation(toggle-OFF 的知识空间来源)保留、其整文件 URL 在 enrich 阶段再门控;匿名时全保留;resolve_citations(批量):先过滤再走原_enrich_item并发流程;resolve_citation(单条):RAG 类型且无权 → 抛NotFoundError(AC-18,与"citation 不存在"语义一致);web 类型不受影响;_enrich_rag_item内旧的has_access = login_user is None or ...整段删除——精滤已在上层完成,进入这里的 item 必然通过view_file或匿名,URL/bbox 无条件填充。
5.4 不修改的端点(明确排除)
knowledge/api/endpoints/qa.py:/qa/chunk、/qa/keyword是历史角标溯源接口,本期不动,待后续统一下线;open_endpoints/api/endpoints/filelib.py与citation.py的 v2 RPC:默认 operator 身份,本期不动(AD-06);workflow/nodes/knowledge_retriever/:工作流节点检索路径(AD-06)。
6. API 契约与错误码
所有端点继续走UserPayload = Depends(UserPayload.get_login_user)(QA 端点新增依赖);响应包装沿用UnifiedResponseModel[T]。无外部契约变化,前端契约透明。
| Method | Path | 变更 |
|---|---|---|
| POST | /api/v1/knowledge/space/{space_id}/chat/file/{file_id} | 内部新增结果层精滤(防御性确认) |
| POST | /api/v1/knowledge/space/{space_id}/chat/folder | 内部接入双层过滤(folder_id=0 即整空间) |
| POST | 工作台search_kb工具(无独立 HTTP 端点) | queryChunksFromDB内部按view_file过滤;无view_space的 KB 静默跳过 |
| POST | /api/v1/citations/resolve | Service 内_has_file_access改造为文件级view_file精滤;无权 citation 整条剔除 |
| GET | /api/v1/citations/{citation_id} | 单条无权返回NotFoundError |
/api/v1/qa/chunk/api/v1/qa/keyword | 历史接口 | 不修改,待下线 |
错误码表(不新增错误码):
| HTTP Status | Code | 场景 | 关联 AC |
|---|---|---|---|
| 200(body) | 18040SpacePermissionDeniedError(复用) | 无view_space/view_folder/view_file | AC-01, AC-05, AC-08 |
| 200(body) | — | 有view_*但可见集合为空;resolve 时 items 全被过滤 | AC-03, AC-12, AC-17 |
| 200(body) | 现有NotFoundError码 | 单条 citation 无view_file,与"citation 不存在"语义一致 | AC-18 |
请求 / 响应示例(spec §6,/citations/resolve部分 citation 无权,AC-16 整条剔除):
请求:
POST /api/v1/citations/resolve { "citationIds": ["cit_A_visible", "cit_B_invisible", "cit_C_visible"] }响应(cit_B_invisible对应的文件用户无view_file,整条不返回):
{ "status_code": 200, "status_message": "SUCCESS", "data": { "items": [ { "citationId": "cit_A_visible", "type": "rag", "sourcePayload": { "knowledgeId": 5, "knowledgeName": "Q1 报告库", "documentId": 1234, "documentName": "report-Q1.pdf", "snippet": "...", "previewUrl": "...", "downloadUrl": "...", "items": [{ "itemId": "...", "bbox": "..." }] } }, { "citationId": "cit_C_visible", "type": "rag", "sourcePayload": { "...": "..." } } ] } }chat_folder流式响应source_documents[*].file_id必为当前用户view_file子集(AD-01 结果层精滤保证),可能为空数组(AC-03 触发,但模型仍按 prompt 给出回答)。
7. 验收标准(AC)全景
7.1 整空间 / 文件夹 / 文件预览问答
| ID | 场景 | 预期 |
|---|---|---|
| AC-01 | 无view_space调整空间问答 | 返回 18040;不进入检索;不写 RecallChunk;不消耗 LLM 配额 |
| AC-02 | 有view_space、部分文件有view_file | 检索仅命中可见的主版本文件;source_documents[*].file_id是当前view_file子集 |
| AC-03 | 有view_space但空间内无任何view_file文件 | 空文档集;模型按 prompt 回答(如"未找到相关内容");HTTP 200 不报错 |
| AC-04 | 问答后管理员收回部分view_file,立即追问 | 第二轮用最新权限(缓存 TTL ≤ 10s 或invalidate_user已触发),被收回文件不再进入上下文 |
| AC-05 | 无view_folder调文件夹问答 | 返回 18040;不进入检索 |
| AC-06 | 有view_folder | 检索范围 = 文件夹子树下可见主版本文件;无view_folder的子文件夹分支被剔除(与列表 UI 对齐) |
| AC-07 | view_folder+tags过滤 | 检索范围 = 子树可见集 ∩ tag 文件集;交集为空按 AC-03 行为 |
| AC-08 | 无view_file调单文件问答(URL 直构造) | 返回 18040;不进入检索 |
| AC-09 | 有view_file | 检索限定document_id == file_id;沿用version_filter;行为与原有逻辑等价 |
7.2 首页 / 工作台多 KB
| ID | 场景 | 预期 |
|---|---|---|
| AC-10 | 拉取 KB 下拉框(mine/managed/joined/department 4 端点) | 本特性不改 4 端点行为(现有服务层已按can_read+ membership 过滤),E2E 回归验证 |
| AC-11 | 某 KB 无view_space(构造请求绕过下拉框) | 静默跳过;不抛错不阻塞;不出现在kb_succeed;INFO 日志skipped_kb_id=X reason=no_view_space |
| AC-12 | KB 有view_space但无任何view_file文件 | Stage 2/3 自然产出 0 docs;不进入finally_docs/kb_succeed;日志输出 AC-27 字段 |
| AC-13 | KB 内部分可见 | 每 KB 走双层过滤;跨 KB 合并按max_total_docs=100截断不变 |
| AC-14 | 选择org_bucket(legacyknowledge_library) | 不修改org_bucket路径行为;变更历史登记差异 |
7.3 角标溯源
| ID | 场景 | 预期 |
|---|---|---|
| AC-15 | 所有 RAG 文件均满足view_file | items结构与现状一致;downloadUrl/previewUrl/bbox照常填充 |
| AC-16 | 部分 RAG citation 已无view_file | 整条剔除无权 citation(documentName / knowledgeId / snippet / chunk 文本均不返回);web 类型不受影响 |
| AC-17 | 全部无权 | items返回空数组[](HTTP 200) |
| AC-18 | 单条 RAG citation 无权 | 返回NotFoundError;不返回任何文件元数据 |
| AC-19 | web类型 citation | 本期不修改_enrich_web_item行为 |
| AC-20 | 匿名调用方(share link,login_user is None) | 保持现状,不引入新鉴权 |
7.4 实时性 / 性能 / 可观测
| ID | 场景 | 预期 |
|---|---|---|
| AC-21 | 管理员 authorize / revoke | 同步触发PermissionCache.invalidate_user(user_id),下一轮问答即用新权限 |
| AC-22 | 缓存未及时失效 | TTL 上限 10s,最迟 10s 内自动收敛 |
| AC-23 | ≤ 5000 可见文件、热缓存 | 权限过滤新增耗时 ≤ 80ms(不含 LLM / Milvus / ES) |
| AC-24 | ≤ 5000 可见文件、冷缓存(OpenFGA list_objects) | 新增耗时 ≤ 500ms |
| AC-25 | ≤ 100000 可见文件、热缓存 | 新增耗时 ≤ 200ms(走 NOT-IN 或后过滤路径) |
| AC-26 | 任意场景 | 检索循环次数 ≤ 2,不存在无限扩张 |
| AC-27 | 任意场景 | 结构化日志含strategy=in\|notin\|postfilter、accessible_ids_size、prefilter_candidate_size、retrieval_attempts、post_filter_dropped_count |
8. Test-First 测试策略与测试覆盖
tasks.md 采用后端 Test-First(务实版):先写测试(红)再写实现(绿)。新测试统一放在test/<module>/之下,复用 conftest.py 现有 fixtures(mock_openfga、mock_redis、async_db_session、tenant_context等);OpenFGA 通过mock_openfga桩;Milvus / ES 不在单元测试中真起,用 monkeypatch 替换 retriever 工具最外层ainvoke返回固定 docs。
8.1 三个新增测试文件
| 测试文件 | 任务 | 数量 | 覆盖 AC |
|---|---|---|---|
| test_knowledge_file_visibility_service.py | T002 | 17 tests | AC-11 / AC-23 / AC-24 / AC-25 部分 |
| test_knowledge_space_chat_service_visibility.py | T004 | 7 tests | AC-01 ~ AC-09 / AC-26 / AC-27 |
test_query_chunks_visibility.py(新建test/workstation/目录) | T006 | 8 tests(4 主场景) | AC-11 ~ AC-14 |
test_citation_resolve_visibility.py(新建test/citation/目录) | T008 | 9 tests(7 主场景) | AC-15 ~ AC-20 |
8.2 关键测试场景示例
test_build_index_prefilter_*系列:K=0 → strategy=empty;K=10 → strategy=in 且milvus_expr="document_id in [..]"、es_filter含terms;N−K ≤ 5000 → strategy=notin;两侧均 > 5000 → strategy=none;admin → strategy=none;传入 candidate 时 admin 仍受业务范围约束(test_build_index_prefilter_admin_still_scopes_to_candidate);大 candidate 永不下沉为 none(test_build_index_prefilter_large_candidate_never_falls_to_none)。test_post_filter_visible_files_*系列:仅保留view_file ∈ effective的 file_id;admin 返回全部;空输入短路;50 个并发 file_id 验证 semaphore 限流不死锁;回归用例验证"revoke 覆盖 membership 默认"。test_retrieve_and_filter_*系列:empty策略直接跳过 retriever 调用;首轮即满足 top_k;精滤砍掉部分;封顶 2 次尝试(test_retrieve_and_filter_capped_at_two_attempts);扩张第二轮成功;用caplog验证permission_filter结构化字段齐全(test_retrieve_and_filter_logs_structured_fields)。test_chat_single_file_logs_view_file_passed_debug:验证单文件问答通过门禁后的 DEBUG 日志。- Citation 测试:全可见原样返回(含 URL/bbox);部分不可见整条剔除(web 不受影响);全不可见 items 空数组;单条无权抛
NotFoundError;web 类型不走view_file;匿名跳过精滤;admin 短路。
9. 前端改动:仅 i18n 文案,无组件 / 路由 / store
后端变更对前端契约透明,唯一需要联调的是"无可见内容"提示文案:
- Platform:public/locales/{en-US,zh-Hans,ja}/knowledge.json 补
knowledge.qa.noVisibleContent; - Client:src/locales/{en,zh-Hans,ja}/translation.json 补同名语义 key。
三语文案:
| 语言 | 文案 |
|---|---|
| 中文 | 未在你有权访问的内容中找到相关信息 |
| 英文 | No relevant content found in resources accessible to you |
| 日文 | アクセス権限のあるリソースに該当する内容が見つかりませんでした |
不涉及:新增组件、新增路由、新增 store。整空间 / 文件夹 / 单文件问答响应 schema 不变;角标溯源面板items数组变短时按数组渲染无需特殊处理;单条无权返回NotFoundError时前端复用现有"该来源已不可访问"提示。
10. E2E 回归清单与性能验证
tasks.md T012 定义的全链路回归场景(e2e-report.md 模板待人工执行):
- KB 下拉框回归(AC-10):以
view_space不全的测试账号登录,确认GET /api/v1/knowledge/space/{mine,managed,joined,department}仅返回有view_space的空间; - 整空间问答(AC-02 / AC-06):部分文件可见账号触发问答,截图
source_documents仅含可见 file_id 的 chunk; - 首页 / 工作台问答(AC-11/12/13):选 3 个 KB(1 个无 view_space、1 个无可见 view_file、1 个有可见 file),仅最后一个 KB 出现在回答 citation 列表;
- 角标溯源(AC-15/16/17):历史会话引用 N 个文件,管理员收回一半文件的
view_file,重开会话展开 citation 面板,确认无权 citation 整条不出现; - 单条 citation(AC-18):直接
GET /api/v1/citations/{无权 citation_id}返回NotFoundError; - 匿名调用(AC-20):share link 公开访问触发 citation resolve,行为不变;
- 实时失效(AC-21/22,关键场景):账号 A 对 F1 有
view_file→ 问答确认 F1 出现 → 管理员调PermissionService.revoke→ 10s 内追问验证 F1 消失;grep 后端日志确认accessible_ids_size前后两轮变化,证明list_accessible_ids已重算; - 结构化日志(AC-27):grep 日志
permission_filter行,确认 5 个字段齐全; - 性能 sanity(AC-23/24):单空间 5000+ 可见文件账号整空间问答,warmup ≤ 80ms、冷启动 ≤ 500ms;10 万规模(AC-25)环境不可达则在报告标注"未验证 + 留待 staging";
- arch-guard 校验:运行
bash scripts/arch-guard.sh,确认CitationResolveService的 RULE-8 VIOLATION 已消除。
11. 实现偏差记录与后续演进
tasks.md 末尾保留「实际偏差记录」章节,实现完成后登记与 spec.md 的偏差。本文可确认的一处偏差是retrieval_expansion_multiplier默认值由 spec 的 10 调整为源码的 6(为控制重试搜索成本,且 Milvus wrapper 会提高 ef 覆盖)。建议后续执行 E2E 时在报告中同步记录该调整对 AC-26 的影响。
后续演进方向(本期明确不支持,详见 spec §3 边界情况):
- 工作流 KNOWLEDGE_RETRIEVER 节点的检索权限过滤(需要先定义"运行身份"语义);
- 对外 RPC
/api/v2/filelib/retrieve的代用户检索协议(on_behalf_of_user_id或新鉴权方式); - 单用户单空间 > 10 万可见文件的低延迟保证(chunk 元数据 ACL group 字段方案);
/api/v1/qa/chunk与/api/v1/qa/keyword历史接口的统一下线;- 匿名调用方 citations 接口的鉴权专项(share-link 场景)。
相关文档
- 特性规格:spec.md
- 任务拆分:tasks.md
- 版本契约与 INV-7:release-contract.md
- 权限体系参考:10-permission-rbac.md(描述 v2.5 之前旧 RBAC 模型,仅作历史背景)
【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考