# pbl_kdb_ext —— PBL 知识库(KDB)只读桩扩展模块(M7) ## 1. 模块用途 `pbl_kdb_ext` 是 PBL(Project/Problem-Based Learning)Agent OS 的**知识库只读扩展层**,为 Designer / Critic Agent 与教师端提供「查得到、改不了」的知识条目检索与研究数据聚合能力。 设计定位(对应两个开放问题的当期落地口径): | 开放问题 | 当期口径 | 本模块实现 | |---------|---------|-----------| | **Q-OPEN-5** KDB 是否落地向量库 | 暂不引入向量库/Embedding | **不落地向量桩**:检索走结构化字段等值 + `LIKE` 关键词匹配,返回体带 `stub` 标记,后续可平滑替换为向量检索而不改契约 | | **Q-OPEN-6** research 数据是否可写回 | 只读、匿名 | **只读匿名聚合**:分组统计评估分数,最小分组 `k_min=5`,样本不足的分组直接抑制(`suppressed_groups` 计数),返回 `anonymous=true / written=false` | ### G6 范围纪律(硬约束,可 grep 核对) 本模块**零写入业务对象**,明确**不实现**以下任何能力: - ❌ `kdb.write` / `kdb.add_candidate` / `kdb.propose_pattern`(知识条目写入、候选入库、模式提炼) - ❌ `research.write`(研究数据写回) - ❌ `sim.*`(仿真/模拟相关全部接口) `pbl_kdb_item.is_write_locked` 恒为 `1`,模块内**不存在**任何 `UPDATE` / `INSERT` / `DELETE` `pbl_kdb_item` 的代码路径;唯一的写操作是**查询留痕日志** `pbl_kdb_query_log`(审计用途, append-only,不属于业务对象写入)。 ## 2. 数据表(2 张,定义在 `models/`) ### 2.1 `pbl_kdb_item` —— 知识条目表(只读,写锁定) | 字段 | 类型 | 说明 | |------|------|------| | `id` | str(32) | 主键 | | `tenant_id` | str(32) | 租户隔离,**所有查询强制打头** | | `code` | str(64) | 条目业务编码(唯一,配合 tenant_id) | | `item_type` | str(32) | 条目类型(pattern / case / rubric_ref 等,取自 appcodes) | | `title` | str(255) | 标题(检索字段) | | `summary_txt` | text | 摘要正文(`LIKE` 关键词匹配字段) | | `payload_json` | text | 结构化载荷(JSON,读取时反序列化为 dict) | | `source` | str(64) | 来源(可作过滤维度) | | `version_no` | int | 版本号 | | `status` | str(16) | 状态,检索仅命中 `published` | | `is_write_locked` | short | **恒为 1**:写锁定标记,本模块只读约束的数据层体现 | | `created_at` / `updated_at` | timestamp | 审计时间戳 | > 表数据由上游(蓝图编译 / 治理域)产生并置 `is_write_locked=1`;本模块只消费,不生产。 ### 2.2 `pbl_kdb_query_log` —— 检索留痕日志表(append-only) | 字段 | 类型 | 说明 | |------|------|------| | `id` | str(32) | 主键 | | `tenant_id` | str(32) | 租户隔离 | | `query_uid` | str(64) | 查询唯一标识(`Q` + sha256(参数+时间) 前 20 位) | | `caller_type` | str(16) | 调用方类型(agent / teacher,缺省 `agent`) | | `caller_id` | str(32) | 调用方 ID(取自 `actor_id()`) | | `query_json` | text | 查询参数快照(剔除 `caller_id`,避免重复存储) | | `hit_count` | int | 命中条数 | | `latency_ms` | int | 检索耗时(毫秒) | | `ok` | short | 是否成功(恒 1,异常路径直接抛错不落日志) | | `created_at` | timestamp | 记录时间 | 用途:检索行为可观测(命中率 / 延迟)与审计追溯。**只 INSERT,不 UPDATE / DELETE。** ### 2.3 跨模块只读依赖 `pbl_research_aggregate` 只读聚合 **`pbl_assessment_record`**(属 `pbl_assessment` 模块, M6 产出)的 `total_score` / `band` / `assessor_type` / `blueprint_id` 字段——**跨模块只读, 不写不建表**,符合交互/扩展层模块的数据所有权边界。 ## 3. 契约接口(3 个,全部只读) 三个契约均为 `wwwroot/api/*.dspy` 薄封装 → 调用 `pbl_kdb_ext/api.py` 中的同名 async 函数 (函数经 `load_pbl_kdb_ext()` 注册到 ServerEnv,dspy 内直接以全局名调用,无需 import)。 | # | 契约名 | HTTP 路径(自动路由) | 入参 | 返回要点 | |---|--------|---------------------|------|---------| | 1 | `pbl_kdb_search` | `/pbl_kdb_ext/api/pbl_kdb_search.dspy` | `item_type`、`source`、`keyword`、`limit`(≤50,默认 10)、`caller_type` | `{ok, data[], hit_count, latency_ms, writable:false, stub}`;固定过滤 `tenant_id` + `is_write_locked=1` + `status='published'`;写一条 `pbl_kdb_query_log` | | 2 | `pbl_kdb_get` | `/pbl_kdb_ext/api/pbl_kdb_get.dspy` | `id` 或 `code` | `{ok, data{...payload_json 已反序列化}, is_write_locked:true, stub}`;未命中抛 `PBL_KDB_NOT_FOUND` | | 3 | `pbl_research_aggregate` | `/pbl_kdb_ext/api/pbl_research_aggregate.dspy` | `group_by` ∈ {`band`, `assessor_type`, `blueprint_id`}(默认 `band`) | `{ok, dimension, groups[{group,n,avg_score,min_score,max_score}], suppressed_groups, k_min:5, anonymous:true, written:false, note}`;维度非法抛 `PBL_AGG_DIM_INVALID` | 错误码沿用 `pbl_common` 统一错误体系(`PblError`):`PBL_KDB_NOT_FOUND`、`PBL_AGG_DIM_INVALID`。 本模块**不另设** `errors.py`——错误类型与错误码常量由公共内核 `pbl_common.api` 统一提供, 模块内只做抛出,避免与公共内核重复定义(交付清单与磁盘文件一一对应)。 ## 4. 目录结构 ``` pbl_kdb_ext/ ├── pbl_kdb_ext/ # Python 包(包目录 = 模块名) │ ├── __init__.py # ② 导出三个契约函数(三处同步之②) │ ├── init.py # ③ load_pbl_kdb_ext():env.<契约> = <契约> │ └── api.py # ① 契约实现(只读检索 / 详情 / 匿名聚合) ├── wwwroot/ │ ├── index.ui # 模块入口页(三张卡片 → urlwidget 到内容区) │ └── api/ │ ├── pbl_kdb_search.dspy │ ├── pbl_kdb_get.dspy │ └── pbl_research_aggregate.dspy ├── models/ │ ├── pbl_kdb_item.json # 表定义(四段式 summary/fields/indexes/codes) │ └── pbl_kdb_query_log.json ├── json/ # CRUD 定义(只读浏览用,无 editable 写入 URL) │ ├── kdb_ext_pbl_kdb_item.json │ └── kdb_ext_pbl_kdb_query_log.json ├── scripts/load_path.py # RBAC 路径注册(显式枚举,无通配符) ├── sql/pbl_kdb_ext.sql # DDL(建表脚本) ├── skill/SKILL.md # Agent 必读模块技能文档 ├── pyproject.toml └── README.md # 本文件 ``` ## 5. 挂载方式(宿主应用集成) 模块**不是独立部署单元**——无自己的 `app.py` / 端口 / Dockerfile / 服务脚本,只能由宿主应用 (`apps/pbls`)挂载: ```python # apps/pbls/app/pbls.py 的 init() 中,ServerEnv() 与基础模块加载之后: from pbl_kdb_ext.init import load_pbl_kdb_ext load_pbl_kdb_ext() # 返回 'pbl_kdb_ext' ``` `load_pbl_kdb_ext()` 是**唯一集成点**,做且只做一件事:把三个契约函数注册到 `ServerEnv` (`env.pbl_kdb_search` / `env.pbl_kdb_get` / `env.pbl_research_aggregate`),使 `.dspy` / `.ui` 能以全局名直接调用。宿主决定何时、以何顺序挂载。 ### 5.1 三处同步铁律 新增/删除一个契约函数必须同步改三处,缺一即运行期报错: 1. `pbl_kdb_ext/api.py` —— 函数实现; 2. `pbl_kdb_ext/__init__.py` —— `from .api import ` 导出(缺 → ImportError / AttributeError); 3. `pbl_kdb_ext/init.py` —— `env. = `(缺 → dspy 内 `NameError: name '' is not defined`)。 核对命令:`grep -rn 'pbl_kdb_search' pbl_kdb_ext/ --include='*.py'` 应恰好命中这三处。 ### 5.2 RBAC 注册(`scripts/load_path.py`) 由 `apps/pbls/build.sh` 调用 `register()`,逐条注册(**显式枚举,禁止 `%` / `*` 通配符**): | 路径 | 角色 | |------|------| | `/pbl_kdb_ext` | `logined` | | `/pbl_kdb_ext/index.ui` | `logined` | | `/pbl_kdb_ext/api/pbl_kdb_search.dspy` | `logined` | | `/pbl_kdb_ext/api/pbl_kdb_get.dspy` | `logined` | | `/pbl_kdb_ext/api/pbl_research_aggregate.dspy` | `logined` | 入口页的**目录路径与文件路径都要注册**,否则入口页上线即 403。`set_role_perm.py` 不在位时 脚本打印 `PENDING ` 清单并以退出码 1 返回(不静默跳过、不抛异常),由部署角色 手工补注册。本模块全部接口为登录可读,无 `any`(匿名)路径,也无 teacher/admin 专属写路径 (因为没有写接口)。 ### 5.3 数据库名 模块内**不硬编码库名**,统一通过 `pbl_common` 的 DB 适配层(`sql_rows` / `sql_exec` 的 `'pbl'` 逻辑库标识)由宿主应用的 `get_module_dbname()` 解析到真实库。 ## 6. 只读 / 匿名聚合约束说明(验收要点) 1. **零写入**:模块内无 `kdb.write` / `research.write` / `sim.*` 任何实现; `grep -rn "def .*write\|def .*sim_" pbl_kdb_ext/` 结果为空。 2. **写锁定**:`pbl_kdb_search` 的 WHERE 固定包含 `is_write_locked = 1`;`pbl_kdb_get` 返回体固定 `is_write_locked: true`;无任何路径可修改该字段。 3. **租户隔离**:三个契约的 SQL 全部以 `tenant_id = ${t}$` 打头(`tenant_id()` 取自 `pbl_common` 请求上下文),缺失租户上下文即报错,不降级为全租户查询。 4. **匿名聚合(隐私线)**:`k_min = 5`;分组样本数 `< 5` 的分组**不出现在 `groups` 中**, 仅计入 `suppressed_groups`;返回体不含任何个体标识(无 student_id / user_id / 明细行)。 5. **聚合维度白名单**:`group_by` 仅允许 `band` / `assessor_type` / `blueprint_id`, 其它值抛 `PBL_AGG_DIM_INVALID`(同时杜绝 SQL 注入——维度名不来自自由文本拼接)。 6. **参数化 SQL**:全部使用 sqlor `${name}$` 占位符,无字符串拼接用户输入。 7. **桩标记透明**:检索/详情返回体带 `stub: 'kdb_stub: no vector store (Q-OPEN-5)'`, 调用方可据此判断结果来自结构化匹配而非语义向量检索。 8. **可观测**:每次检索落一条 `pbl_kdb_query_log`(hit_count / latency_ms / query_json), 用于命中率与延迟分析;日志表 append-only。 ## 7. 依赖 - **基础/公共**:`pbl_common`(租户上下文、DB 适配、`PblError` 错误码、`actor_id` / `now_str` / `json_dump` 工具)、`ahserver`(ServerEnv)、`sqlor`(数据访问)。 - **数据依赖(只读)**:`pbl_assessment` 的 `pbl_assessment_record`(聚合数据源)。 - **被依赖**:`pbl_agent_runtime`(Designer/Critic Agent 检索知识条目)、 `pbl_scense_ext` / 教师端页面(研究数据聚合展示)。 ## 8. 本地校验 ```bash cd modules/pbl_kdb_ext python3 -m py_compile pbl_kdb_ext/*.py scripts/load_path.py # .py 编译 python3 -c "import json;[json.load(open('models/'+f)) for f in ('pbl_kdb_item.json','pbl_kdb_query_log.json')]" python3 -c "import json;json.load(open('wwwroot/index.ui'));print('index.ui JSON OK')" grep -rn "^import\|^from" wwwroot/ --include='*.dspy' # dspy 审计:应为空 grep -rn "entire_url(" wwwroot/index.ui | grep -v "/pbl_kdb_ext/" # 绝对路径核对:应为空 ``` > `.dspy` 不能用 `py_compile` 校验(框架运行期注入 async 函数体,顶层 `return`/`await` 合法), > 只做 grep 审计。