STARQUANT · ENGINEERING NOTE
企业知识 Agent 怎么搭:从文档入库到可追溯回答的 9 个关键环节
不绑定数据库、框架或模型厂商,拆解企业知识 Agent 的通用技术链路:文档身份、权限过滤、检索、引用、工具调用与证据记录。
很多团队做知识问答,第一步通常是:
把文档切成几段,生成向量,接一个聊天模型。
Demo 很快就能跑起来。
但一旦进入企业环境,真正的问题会变成:
•这段内容来自哪个文档、哪个版本?
•当前用户有没有权限看到它?
•模型引用的依据能不能被点回去?
•回答之后,能不能继续调用业务工具?
•出现错误时,能不能还原当时发生了什么?
所以一条可用的企业知识链路,不只是“向量检索+生成回答”。它至少要包含:
文档身份、权限过滤、混合检索、引用约束、工具白名单和证据账本。
下面用抽象数据表、伪 SQL 和 Python 风格伪代码写一套通用骨架。它不绑定某个数据库、向量库、解析器、模型或工作流框架;你可以把每一层替换成现有技术栈。代码是教学版,生产环境还需要补充密钥管理、脱敏、限流、备份和监控。
01
ENGINEERING NOTE
01 / 先画清链路
从文档到可继续执行的结果
最小链路可以拆成六步:
原始文档
↓
解析、清洗、切分
↓
写入文档身份与访问范围
↓
向量检索+关键词检索
↓
带引用回答/调用白名单工具
↓
验证结果并写入证据账本

一条从文档入库、检索、引用到验证的知识 Agent 链路
这条链路可以由任意开源组件组合而成:
解析层:读取文本、结构、页码和表格位置
存储层:保存文档身份、分块、向量和权限元数据
检索层:关键词、语义和结构化条件联合召回
编排层:维护任务状态,决定何时检索、何时调用工具
模型层:根据证据生成回答或提出动作
证据层:记录引用、工具调用、验证与最终状态
组件可以替换,但数据身份、权限、引用和证据这四件事不能省略。
这里有一个重要的设计原则:
检索结果不是最终答案,检索结果只是模型可以使用的证据。
因此,检索阶段要把文档身份和权限一起带出来;生成阶段要强制引用;执行阶段要记录工具调用;结束阶段要把依据、结果和状态保存下来。
02 · ENGINEERING NOTE
02 / 数据模型:向量旁边一定要有身份
不要只存 content 和 embedding
一个只包含文本和向量的表,适合做实验,不适合做企业知识库。
至少需要两张表:文档表和分块表。
下面是与具体数据库无关的伪 SQL:
TABLE documents (
id, tenant_id, doc_key, title, version, status,
source_uri, access_policy, checksum, created_at
)
TABLE document_chunks (
id, document_id, chunk_no, content,
embedding, locator, created_at
)

— 文档身份、分块和访问元数据需要一起保存
这几个字段各自承担不同责任:
•tenant_id:隔离不同企业或项目空间;
•doc_key:同一份业务文档的稳定身份;
•version:回答时可以明确使用哪一版;
•status:草稿、已发布、已归档不能混在一起检索;
•access_policy:用于做最基本的权限过滤;
•source_uri:让用户能够回到原始资料;
•locator:保存页码、段落、表格单元格或图纸区域。
checksum 用来判断文件内容是否真的变化。文件名不变,不代表内容没有更新。
元数据索引不要省
INDEX documents BY (tenant_id, status)
INDEX chunks BY (document_id, chunk_no)
INDEX chunks.embedding FOR similarity_search
向量索引只是召回手段,不负责权限判断。租户、版本、状态和访问策略,仍然要在查询条件中显式处理。数据量增长后,再根据延迟、召回率和更新频率选择精确或近似索引。
03 · ENGINEERING NOTE
03 / 入库:切分时把定位信息一起保存
chunk 不是随便按字数截断
切分的目标不是让每段长度相等,而是让每段内容可以独立回答一个小问题,同时保留它在原文中的位置。
一个教学版的切分函数可以这样写:
from dataclasses import dataclass
@dataclass
class Chunk:
text: str
chunk_no: int
locator: dict
def split_sections(text: str, max_chars: int = 900) -> list[Chunk]:
sections = [s.strip() for s in text.split("\n\n") if s.strip()]
chunks = []
buffer = ""
no = 0
for section in sections:
candidate = f"{buffer}\n\n{section}".strip()
if buffer and len(candidate) > max_chars:
chunks.append(Chunk(
text=buffer,
chunk_no=no,
locator={"paragraph_end": no},
))
no += 1
buffer = section
else:
buffer = candidate
if buffer:
chunks.append(Chunk(
text=buffer,
chunk_no=no,
locator={"paragraph_end": no},
))
return chunks
在实际使用中,locator 不应该只保存段落序号。PDF 要保存页码,Word 要保存标题路径,表格要保存行列,图形或工程文件则要保存图层、对象编号和区域。
这样回答中的引用才不是一个装饰性的 [C1],而是可以真正定位到原始资料。
入库写入的最小顺序
document_id = insert_document(
tenant_id=tenant_id,
doc_key=doc_key,
version=version,
status="published",
source_uri=source_uri,
access_policy=access_policy,
checksum=sha256(raw_file),
)
for chunk in split_sections(clean_text(raw_file)):
insert_chunk(
document_id=document_id,
chunk_no=chunk.chunk_no,
content=chunk.text,
locator=chunk.locator,
embedding=embed(chunk.text),
)
先写文档身份,再写分块。不要让孤立的向量先进入数据库,之后再想办法补来源。
04
ENGINEERING NOTE
04 / 检索:相似度之前先做权限和版本过滤
“最相似”不等于“最应该给用户看”
一个基础检索函数至少需要接收四个参数:
def retrieve(
query: str,
tenant_id: str,
subject_id: str,
doc_key: str | None = None,
top_k: int = 8,
):
query_vector = embed(query)
return similarity_search(
query_vector=query_vector,
filters={
"tenant_id": tenant_id,
"status": "published",
"subject_id": subject_id,
"doc_key": doc_key,
},
top_k=top_k,
)
这里有三个容易被忽略的点:
01权限条件在检索 SQL 中,而不是回答生成以后再补;
02只检索 published 版本,草稿和归档内容默认不参与回答;
03doc_key 可以把搜索范围缩小到某个业务对象,避免全库相似度竞争。
生产环境建议做混合检索
向量搜索擅长找语义相近的内容,关键词搜索擅长找编号、型号、命令和专有名词。工程资料中,二者经常需要一起用。

— 语义检索与关键词检索经过权限和版本过滤后合并为可引用结果
semantic_hits = semantic_search(query_vector, filters=filters)
keyword_hits = keyword_search(query_terms, filters=filters)
results = rerank(merge(semantic_hits, keyword_hits))
可以先分别取语义结果和关键词结果,再使用 RRF 或交叉编码器重排。第一版实现不必追求复杂,但要保留这两个入口,尤其是处理版本号、零件号、接口名和错误码时。
05 · ENGINEERING NOTE
05 / 生成:让模型只能根据证据回答
Prompt 里要写“不能做什么”
把检索结果拼进 Prompt 还不够。要明确引用格式、证据不足时的行为和禁止事项。
你是企业知识助手。
回答规则:
1. 只能使用 <context> 中的内容;
2. 每个关键结论后必须附引用,格式为 [C1]、[C2];
3. 引用必须对应给定的 chunk_id、文档版本和定位信息;
4. 证据不足时,回答“当前资料不足以确认”,并列出需要补充的资料;
5. 不要猜测版本、数字、责任人或审批状态;
6. 将“事实”“推断”“待确认项”分开输出。
输出结构:
- 结论
- 依据
- 待确认项
- 建议的下一步
上下文可以按下面的格式注入:
<context>
[C1] doc_key=<文档编号> version=<版本> locator=<位置>
<与问题相关的原文片段>
[C2] doc_key=<文档编号> version=<版本> locator=<位置>
<另一段与问题相关的原文片段>
</context>
这样,模型输出的不是一段脱离来源的“看起来合理的话”,而是一份带有依据的判断。
引用不应该只存在于文本里
返回给前端时,建议同时保留结构化 citations:
{
"answer": "根据当前资料,<结论>。[C2]",
"citations": [
{
"label": "C2",
"chunk_id": 1842,
"doc_key": "<文档编号>",
"version": "<版本>",
"locator": {"section": "<位置>"},
"source_uri": "<原始资料地址>"
}
],
"needs_confirmation": true
}
前端可以把引用做成可点击的来源,审计系统则可以直接保存结构化证据。
06 · ENGINEERING NOTE
06 / Agent:工具调用必须白名单化
让 Agent 做动作,不等于让它拥有全部权限
知识问答和 Agent 的分界点,通常发生在工具调用。
一个最小的工具描述应该包含名称、参数和权限要求:
{
"name": "create_task",
"description": "创建一条待处理任务",
"input_schema": {
"type": "object",
"required": ["object_id", "version", "title"],
"properties": {
"object_id": {"type": "string"},
"version": {"type": "string"},
"title": {"type": "string"},
"owner": {"type": "string"}
}
}
}
执行前至少检查:
assert tool.name in ALLOWED_TOOLS
assert current_user.has_scope(tool.required_scope)
assert args["version"] == current_object.current_version
assert evidence_is_sufficient(context)
对于修改正式数据、对外发送、发布版本这类动作,默认走人工确认,不要让模型用一句自然语言自行判断“应该可以”。

— Agent 动作经过白名单、权限和证据检查,并将结果写入证据账本
工具执行结果也要返回稳定的 task_id、status 和 result_uri,方便后续继续工作。
07
ENGINEERING NOTE
07 / 证据账本:回答结束以后还要记什么
没有账本,就无法复盘
每次请求至少记录以下字段:
TABLE evidence_ledger (
id, task_id, tenant_id, actor_id,
model_id, prompt_hash, citations,
tool_calls, verifier_result, status, created_at
)
prompt_hash 用于定位本次请求使用的 Prompt 版本,citations 保存引用,tool_calls 保存动作,verifier_result 保存检查结果。
当用户问“为什么得到这个结论”时,系统应该能直接打开这条账本,而不是让工程师去翻聊天记录和临时文件。
08 · ENGINEERING NOTE
08 / 上线前,先做四组测试
不要只测“回答像不像人”
检索测试
•给定问题是否能召回正确文档和版本;
•权限不足的文档是否永远不会出现;
•编号、型号、错误码能否被关键词检索召回。
引用测试
•每个关键结论是否都有引用;
•引用是否真的支持结论;
•文档更新后,旧版本是否不再被默认使用。
工具测试
•缺少必填参数时是否拒绝执行;
•超出权限时是否拒绝执行;
•需要人工确认的动作是否能正确暂停。
追溯测试
•能否从回答跳回原始资料;
•能否从工具结果找到任务和操作者;
•能否复原一次完整请求的 Prompt、证据和状态变化。
这些测试比“模型回答得像不像人”更接近企业真正关心的质量。
09 · ENGINEERING NOTE
09 / 如何把这条通用链路落地
先跑通一条小链路,再接入现有系统
通用技术做法解决的是链路问题,落地时还要接入现有身份体系、资料来源、审批流程和业务工具。建议按下面的顺序推进:
01先选一类文档和一个明确问题,完成入库、检索和引用;
02再加入租户、版本和权限过滤,验证不同用户得到的证据是否一致;
03接入一个只读工具,观察 Agent 是否能正确选择、调用和记录;
04最后再开放创建、修改或发布类动作,并增加人工确认节点。
先把一条链路做小、做完整,再根据真实反馈扩展数据源、工具和自动化范围。
∞
CLOSING NOTE
结语 / BUILD THE TRACE
企业知识应用的第一版,不需要一开始就做得很大。
先选一类文档、一种权限边界、一个可验证的任务结果,把入库、检索、引用、工具调用和证据记录完整跑通。
当这条最小链路稳定以后,再扩展更多数据源、更多工具和更多 Agent。
能被引用,才敢被使用;能被追溯,才能持续运行。
星旷技术 · STARQUANT
AI-NATIVE APPLICATIONS FOR REAL OPERATIONS
了解星旷:www.starquant.net
商务合作:contact@starquant.net
本文示例均为与具体数据库、框架和厂商无关的教学伪代码,重点是方法与约束,而不是某个产品的配置。
夜雨聆风