施工企业做制度文档问答,难点不在“能不能问答”,而在“能不能管住权限、分清版本、保证答的是现行有效内容”。本文按模块拆解一套可落地的实现思路。
一、制度文档问答的技术难点
施工企业的制度文档有几个鲜明特点:文档数量多、版本更新频繁、权限边界严格、格式不统一。
由此带来三个核心问题:
- 权限问题:不同分公司、不同岗位能看的制度不同,问答系统不能越权返回;
- 版本问题:同一制度可能有多个历史版本,必须只召回现行有效版本;
- 准确性问题:制度条款表述严谨,切片一旦丢失上下文,容易答偏。
这三个问题决定了系统不能只是"文档切片 + 向量检索"这么简单,需要在切片、权限、版本、召回几个环节分别设计。
整体架构可以概括为下面这条链路:
二、文档切片与清洗
制度文档通常是结构化的,按“章—条—款”组织,切片应当尊重这种结构,而不是按固定长度硬切。
推荐的做法:
- 以“条”为最小切片单位,保留所属章、制度名称作为上下文元数据;
- 每个切片附带结构化字段:
doc_id、chapter、article_no、version、effective_date、status、dept_scope; - 清洗时统一格式,去除页眉页脚、修订痕迹、失效批注。
一个切片的元数据示例:
{"doc_id":"REG-0021","title":"项目成本管理办法","article_no":"第13条","version":"2026-03","effective_date":"2026-03-01","status":"active","dept_scope":["all"]}按条款切片的好处是,每条回答都能精确定位到“哪个制度、哪一条、哪个版本”,天然带出处。
切片流程可以进一步细化为:
三种常见切片方案各有取舍,横向对比如下:
| 对比维度 | 固定长度切片 | 按章切片 | 按条切片 |
|---|---|---|---|
| 上下文完整性 | 较差,容易在条款中间截断,语义被割裂 | 较好,整章内容完整,但单块偏大、噪声多 | 较好,以“条”为完整语义单元,配合章名元数据可补足上下文 |
| 出处精度 | 低,只能定位到文档级,难以精确到条款 | 中,可定位到章,但章内条款仍需二次定位 | 高,可精确定位到“哪个制度、哪一条、哪个版本” |
| 实现成本 | 低,按字符数或 token 数切分即可,无需解析文档结构 | 中,需识别章标题层级,对格式不规范的文档要做兼容 | 较高,需解析“章—条—款”结构并抽取条款号、版本等元数据 |
| 适用场景 | 结构松散、无固定层级的通用文档,或快速验证原型 | 以章为单位阅读、问题粒度较粗的场景 | 制度、法规、合同等强结构化且要求可溯源的问答场景 |
综合来看,制度文档问答更适合以“按条切片”为主,必要时用“按章切片”兜底,固定长度切片只作为兜底或冷启动方案。
三、权限模型设计
权限要在检索阶段就过滤,而不是检索完再筛,否则既越权又浪费算力。
常见的做法是把权限信息写进元数据,在召回时作为过滤条件。
| 权限维度 | 过滤字段 | 匹配方式 |
|---|---|---|
| 组织范围 | org_scope | 命中用户所属分公司或 all |
| 岗位范围 | role_scope | 命中用户岗位或 all |
| 密级 | security_level | 用户密级 ≥ 文档密级 |
| 文件状态 | status | 仅 active |
检索时,先根据当前用户的组织、岗位、密级构造过滤条件,再在符合权限的切片集合里做语义召回。这样能保证答案只会从用户本就有权看的内容里产生。
权限过滤与召回的关系如下:
把上面的过滤逻辑落到代码里,核心就是根据当前用户身份拼出一个向量库可用的 filter 条件。下面用 Python 伪代码演示(以 Chroma / Milvus 这类支持元数据过滤的向量库为例,写法做了简化):
defbuild_permission_filter(user):""" 根据当前用户身份构造向量库检索的 filter 条件。 user 结构示例: { "org": "华东分公司", # 用户所属组织 "role": "项目经理", # 用户岗位 "security_level": 2, # 用户密级,数值越大权限越高 } """conditions=[]# 1. 组织范围:切片允许的组织里包含用户所属分公司,或标记为 all(全公司通用)# 作用:防止跨分公司越权,比如华东的员工看不到华南的专项制度conditions.append({"org_scope":{"$in":[user["org"],"all"]}})# 2. 岗位范围:切片允许的岗位里包含用户岗位,或标记为 all(不限岗位)# 作用:区分管理层与执行层的可见范围,如薪酬制度仅对 HR 与管理者开放conditions.append({"role_scope":{"$in":[user["role"],"all"]}})# 3. 密级:文档密级不得高于用户密级(用户密级 >= 文档密级)# 作用:控制涉密制度的可见性,低密级用户无法召回高密级内容conditions.append({"security_level":{"$lte":user["security_level"]}})# 4. 文件状态:只召回现行有效版本# 作用:避免把已归档或草稿版本当作有效制度返回,保证答的是现行内容conditions.append({"status":{"$eq":"active"}})# 多个条件之间是 AND 关系,必须同时满足才进入召回候选集return{"$and":conditions}# 使用示例:先构造过滤条件,再在满足权限的切片集合里做语义召回user={"org":"华东分公司","role":"项目经理","security_level":2}where=build_permission_filter(user)results=vector_store.query(query_embeddings=[query_vector],# 用户问题的向量where=where,# 权限与状态过滤条件top_k=10# 候选切片数量)这样写的好处是:权限判断完全下沉到检索层,召回结果天然只包含用户有权查看且现行有效的条款,后续重排和生成阶段无需再做二次过滤。
如果系统承载在协同办公平台上,还可以直接继承平台的组织架构和权限体系,避免单独维护一套账号权限,权限变更也能自动同步。
四、多版本管理
制度会修订,旧版本不能一删了之,有时还要追溯“当时按哪版执行”,但日常问答必须只答现行版本。
实现上可以这样设计:
- 同一制度用稳定的
doc_id关联所有版本,每个版本有独立的version和effective_date; - 用
status标识active(现行)、archived(归档)、draft(草稿); - 向量库默认只对
active切片建立可检索索引,归档版本默认不参与召回; - 需要追溯历史时,再走专门的历史版本查询入口。
版本生效时,把旧版本批量置为archived、新版本置为active,是一次原子化的状态切换,避免出现新旧版本同时被召回的情况。
版本状态切换可以表示为:
五、召回与重排
在前面几个模块的基础上,召回链路可以这样组织:
- 用用户信息构造权限与状态过滤条件;
- 在过滤后的切片集合里做向量召回,取候选若干条;
- 结合关键词匹配(制度名称、条款号)做补充,避免专有词召回不准;
- 重排时优先保留条款完整、出处明确、版本最新的切片;
- 生成答案时附上制度名称、条款号和版本,作为出处。
召回与重排的完整链路:
对于无依据或检索置信度低的问题,系统应明确提示“请人工核实”,而不是强行拼凑答案。这一条在制度问答里尤其重要,因为错误的制度解释可能直接误导执行。
本文为技术科普