政务行业 AI 落地——政策文件查询的工程经验
政务行业 AI 落地——政策文件查询的工程经验
适读人群:做政务/党政/事业单位 AI 项目的工程师
阅读时长:约 21 分钟
文章价值:政务 AI 的特殊工程挑战 + 本地化部署方案 + 核心代码实现
那份措辞让我沉默了三秒
第一次和政务客户开需求会,客户方的一句话让我愣了一下:
「所有数据不能出内网,包括文档内容、查询记录、用户行为,一条字节都不能。」
我当时的第一反应是:没问题,我们可以私有化部署。然后他们补充了一句:「而且网络是物理隔离的,你们的工程师来现场调试,手机要存在门口。」
……好,这是认真的。
我在这之前做的大多数项目,调用的都是云端 LLM API,出问题了远程排查,方便得很。政务这个行业让我意识到,「网络连接」不是理所当然的事。
这篇文章是我在政务 AI 项目上的真实经验,主要讲政策文件查询系统的工程实现——这是政务 AI 里需求量最大、最有代表性的应用场景。
政务场景的三个特殊约束
1. 数据不能出内网
这不是客户的偏好,是法规要求。涉及政务的文件、公民数据、行政记录,有明确的数据安全等级要求,不能上传到任何外部服务器,包括云端 LLM API。
这意味着:整个 AI 系统必须在内网环境运行。 LLM 模型要本地部署,向量数据库要本地部署,检索服务要本地部署。不能有任何数据走外网。
对工程师来说,这带来了几个挑战:
- 没有 ChatGPT 这种级别的大模型可用,只能用能够私有化部署的开源模型
- 硬件资源由甲方提供,通常不是最新的 GPU 服务器,需要做性能优化
- 模型更新要走安全审查,不能随便换
2. 准确性要求极高,容错率几乎为零
政务场景的 AI 错误,不是「用户体验差」,是「可能造成行政错误」。
典型场景:某个企业向 AI 咨询某项补贴的申请条件,AI 给出了错误的答复,企业按照错误答复准备了材料,结果申请被驳回,延误了业务。这在现实中是真实发生过的类型。
所以政务 AI 有一个铁律:宁可说「我不确定,建议人工咨询」,也不能给出错误的肯定答复。
系统的设计原则:低置信度时主动说不确定,并提供联系人工的入口。这看起来是「不够智能」,实际上是正确的工程选择。
3. 可追溯性是刚需
政务 AI 的每一次重要查询,都要可追溯。包括:用户查了什么、系统给出了什么答复、答复引用了哪些文件的哪些条款、答复时刻对应的文件版本是什么。
这个要求不是为了方便开发调试,是为了满足政务审计要求。如果日后某个答复被质疑,要能复现当时的完整决策过程。
政策文件的特殊处理挑战
政策文件不是普通的文本文档,它有自己的结构和语义特性,处理起来有专门的难点。
条款引用的层级结构
政策文件充满了条款引用,比如:「根据《XX条例》第十五条第三款的规定……」,「依照前款规定……」,「除本条规定情形外……」。
这种引用关系如果处理不当,RAG 检索出来的文本片段可能缺乏上下文,导致理解错误。
比如「前款规定」,如果只检索出包含这三个字的段落,不同时检索出「前款」的内容,AI 根本不知道「前款规定」是什么。
我们的处理方案:建立条款引用图谱。在文档解析阶段,识别所有条款引用关系,建立有向图,检索时自动扩展引用链,确保上下文完整。
文件有效期管理
政策文件有生命周期:发布时间、生效时间、废止时间、被修订情况。同一个政策,可能有多个版本,旧版本被新版本替代,但仍然存档(历史查询需要)。
这个有效期管理很重要。如果一个政策已经废止,AI 还在引用它来回答问题,就是错误的。
我们在元数据层面管理这个:每个文档有 effective_date、expiry_date、status(ACTIVE/SUPERSEDED/ARCHIVED)、superseded_by 字段。检索时强制过滤只返回有效文件,但保留历史查询的能力(查历史政策时用日期参数过滤)。
歧义和模糊表述
政策文件有时候本身就有模糊表述,不同的理解会导致不同的结论。这种情况下,AI 不应该「猜」一个答案,而应该如实说明「此处存在解释空间,建议咨询法务/主管部门」。
我专门在 Prompt 里加了这条指令:遇到政策条文存在解释歧义时,列出可能的解读,不做单一定论,明确建议用户咨询权威解释。
本地化部署方案
政务 AI 的本地化部署是技术难点,我把我们的方案整理如下。
模型选择
内网部署,能用的模型选择有限。我们在几个项目上用过的方案:
Qwen2.5 系列:阿里开源,中文能力很强,政策文档理解效果好,有多种尺寸版本(7B/14B/72B),可以根据硬件资源选择。
GLM-4 系列:清华开源,同样中文能力强,部分场景比 Qwen 更适合。
对于内网部署,我的建议是:不要只看参数量,要在实际政务文档上做 benchmark。 14B 的模型在政务文本理解上可能比某个 70B 的模型效果更好,因为模型的训练数据和微调方向不同。
推理优化
政务 AI 系统通常不会有高并发,但响应时间要求不高(用户习惯了政务系统慢一点),可以用量化版本的模型降低显存需求,同时用 vLLM 或 Ollama 做推理服务。
典型配置:一张 A100 80G 显卡,可以跑 Qwen2.5-72B-Q4 量化版,单并发推理延迟 8-15 秒,满足大多数政务 AI 应用需求。
向量数据库选型
内网部署下,我们用的是 Milvus 独立部署,稳定性好,支持大规模文档检索。也有用 pgvector 的,适合文档量不大(10 万条以内)的场景,运维更简单。
系统架构
这个架构有几个关键点:
全链路内网:从用户终端到 LLM 推理,没有任何外网请求。
查询日志强制存档:每次查询必须写入审计日志,这不是可选的,是系统流程的强制节点。
低置信度路由:置信度低的查询不是直接给出答复就完了,要显式提示用户「本答复仅供参考,建议人工确认」,并提供联系方式。
代码实现:本地化政务 RAG 系统核心设计
@Service
public class GovernmentPolicyQueryService {
private final ChatClient chatClient;
private final VectorStore policyVectorStore;
private final PolicyClauseGraphService clauseGraphService;
private final QueryAuditLogger auditLogger;
private final PolicyFileRepository policyRepository;
// 置信度阈值:低于此值时触发不确定提示
private static final int CONFIDENCE_THRESHOLD = 75;
public GovernmentPolicyQueryService(
ChatClient.Builder chatClientBuilder,
VectorStore policyVectorStore,
PolicyClauseGraphService clauseGraphService,
QueryAuditLogger auditLogger,
PolicyFileRepository policyRepository) {
// 注意:ChatClient 配置指向本地 LLM 服务
this.chatClient = chatClientBuilder.build();
this.policyVectorStore = policyVectorStore;
this.clauseGraphService = clauseGraphService;
this.auditLogger = auditLogger;
this.policyRepository = policyRepository;
}
/**
* 政策文件查询入口
* @param queryRequest 查询请求(包含用户ID、查询内容、所属部门等)
*/
public PolicyQueryResponse query(PolicyQueryRequest queryRequest) {
String queryId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
try {
// 1. 检索相关政策文件
PolicyRetrievalResult retrievalResult = retrievePolicies(queryRequest);
// 2. 扩展条款引用链(处理"前款规定"等引用)
PolicyRetrievalResult expandedResult =
clauseGraphService.expandClauseReferences(retrievalResult);
// 3. 构建查询上下文,调用本地 LLM
PolicyQueryAnswer answer = generateAnswer(queryRequest, expandedResult);
// 4. 构建响应
PolicyQueryResponse response = buildResponse(queryId, answer, expandedResult);
// 5. 强制审计日志(必须在返回前完成)
auditLogger.log(PolicyQueryAuditRecord.builder()
.queryId(queryId)
.userId(queryRequest.getUserId())
.department(queryRequest.getDepartment())
.queryContent(queryRequest.getQueryContent())
.responseContent(response.getAnswer())
.referencedPolicies(extractReferencedPolicies(expandedResult))
.confidenceLevel(answer.getConfidenceLevel())
.processingTimeMs(System.currentTimeMillis() - startTime)
.modelVersion(getCurrentModelVersion())
.timestamp(LocalDateTime.now())
.build());
return response;
} catch (Exception e) {
log.error("Policy query failed, queryId: {}", queryId, e);
// 查询失败时也要记录日志
auditLogger.logFailure(queryId, queryRequest, e.getMessage());
// 返回安全的降级响应
return PolicyQueryResponse.systemError(queryId, "查询处理失败,请联系系统管理员或直接咨询相关部门。");
}
}
private PolicyRetrievalResult retrievePolicies(PolicyQueryRequest request) {
// 确定查询的有效时间(默认当前时间,历史查询用指定时间)
LocalDate queryDate = request.getQueryDate() != null
? request.getQueryDate()
: LocalDate.now();
// 向量检索:只检索有效的政策文件
SearchRequest searchRequest = SearchRequest.query(request.getQueryContent())
.withTopK(10)
.withSimilarityThreshold(0.72)
.withFilterExpression(buildEffectiveDateFilter(queryDate, request.getDomain()));
List<Document> vectorResults = policyVectorStore.similaritySearch(searchRequest);
// 全文检索补充(处理精确术语查询)
List<Document> keywordResults = searchByKeywords(request.getQueryContent(), queryDate);
// 合并去重,按相关度排序
List<Document> mergedResults = mergeAndRank(vectorResults, keywordResults);
// 加载完整文档元数据(包含条款引用关系)
List<PolicyDocumentContext> contexts = enrichWithMetadata(mergedResults);
return new PolicyRetrievalResult(contexts, queryDate);
}
private String buildEffectiveDateFilter(LocalDate queryDate, String domain) {
// 过滤条件:文件已生效 且 未废止(或废止时间在查询日期之后)
StringBuilder filter = new StringBuilder();
filter.append("status == 'ACTIVE'");
filter.append(" AND effective_date <= '").append(queryDate).append("'");
filter.append(" AND (expiry_date IS NULL OR expiry_date > '").append(queryDate).append("')");
if (domain != null && !domain.isEmpty()) {
filter.append(" AND domain == '").append(domain).append("'");
}
return filter.toString();
}
private PolicyQueryAnswer generateAnswer(
PolicyQueryRequest request,
PolicyRetrievalResult retrievalResult) {
if (retrievalResult.isEmpty()) {
// 没有检索到相关文件:返回无结果响应
return PolicyQueryAnswer.noResult(
"未找到相关政策文件,建议直接咨询" + request.getDomain() + "主管部门。"
);
}
String systemPrompt = buildGovernmentSystemPrompt();
String userPrompt = buildQueryPrompt(request, retrievalResult);
String rawResponse = chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content();
return parseAnswer(rawResponse);
}
private String buildGovernmentSystemPrompt() {
return """
你是一个政府政策文件咨询助手,专门帮助政务工作人员查询和理解政策文件。
工作原则:
1. 只基于提供的政策文件内容回答,不添加任何不在文件中的内容
2. 每个结论必须注明来源文件名称和具体条款编号
3. 如果问题涉及多个文件,分别说明每个文件的相关规定
4. 如果文件规定存在歧义或需要专业解释,明确说明,不做单一解读
5. 如果检索到的文件不足以完整回答问题,说明哪部分无法回答,建议咨询哪个部门
6. 政策文件的引用格式:《文件名称》第X条第X款
严格禁止:
- 凭记忆或推测补充政策内容
- 对政策规定做超出文字本身的解释
- 对尚未明确的政策走向做预测
置信度评估(在输出中包含):
- 90-100:文件明确规定,无歧义
- 70-89:文件有相关规定,但需要一定推断
- 50-69:文件有间接相关内容,答案不确定
- 50以下:无法从现有文件给出可靠答复
""";
}
private String buildQueryPrompt(
PolicyQueryRequest request,
PolicyRetrievalResult retrievalResult) {
StringBuilder sb = new StringBuilder();
sb.append("## 用户查询\n");
sb.append(request.getQueryContent()).append("\n");
sb.append(String.format("(查询时间:%s,查询部门:%s)\n\n",
retrievalResult.getQueryDate(), request.getDepartment()));
sb.append("## 相关政策文件(按相关度排序)\n\n");
for (int i = 0; i < retrievalResult.getContexts().size(); i++) {
PolicyDocumentContext ctx = retrievalResult.getContexts().get(i);
sb.append(String.format("### 文件%d:%s\n", i + 1, ctx.getDocumentTitle()));
sb.append(String.format("- 文件编号:%s\n", ctx.getDocumentNumber()));
sb.append(String.format("- 发布机关:%s\n", ctx.getIssuingAuthority()));
sb.append(String.format("- 生效日期:%s\n", ctx.getEffectiveDate()));
sb.append(String.format("- 相关章节:%s\n\n", ctx.getRelevantSection()));
sb.append("**相关内容**:\n");
sb.append(ctx.getRelevantContent()).append("\n");
if (!ctx.getReferencedClauses().isEmpty()) {
sb.append("\n**被引用的相关条款**:\n");
ctx.getReferencedClauses()
.forEach(clause -> sb.append("- ").append(clause).append("\n"));
}
sb.append("\n---\n\n");
}
sb.append("## 输出格式要求\n");
sb.append("请用 JSON 格式输出:\n");
sb.append("""
{
"directAnswer": "直接回答用户问题(200字以内)",
"detailedAnalysis": "详细分析,引用具体条款",
"keyReferences": [
{"documentTitle": "文件名", "clauseNumber": "第X条第X款", "content": "具体条款内容"}
],
"ambiguousPoints": "如有歧义点,在此说明",
"recommendedAction": "建议用户的下一步行动",
"consultationAdvice": "如需进一步确认,建议咨询的部门或方式",
"confidenceLevel": 85
}
""");
return sb.toString();
}
private PolicyQueryAnswer parseAnswer(String rawResponse) {
try {
String json = extractJsonFromResponse(rawResponse);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> parsed = mapper.readValue(json, Map.class);
int confidence = (Integer) parsed.getOrDefault("confidenceLevel", 50);
boolean needsHumanConsultation = confidence < CONFIDENCE_THRESHOLD;
return PolicyQueryAnswer.builder()
.directAnswer((String) parsed.get("directAnswer"))
.detailedAnalysis((String) parsed.get("detailedAnalysis"))
.keyReferences(parseKeyReferences(parsed))
.ambiguousPoints((String) parsed.getOrDefault("ambiguousPoints", ""))
.recommendedAction((String) parsed.get("recommendedAction"))
.consultationAdvice((String) parsed.get("consultationAdvice"))
.confidenceLevel(confidence)
.needsHumanConsultation(needsHumanConsultation)
.build();
} catch (Exception e) {
log.error("Failed to parse policy answer", e);
return PolicyQueryAnswer.parseError(
"答复生成遇到问题,请重新查询或联系系统管理员。"
);
}
}
private PolicyQueryResponse buildResponse(
String queryId,
PolicyQueryAnswer answer,
PolicyRetrievalResult retrievalResult) {
PolicyQueryResponse.Builder builder = PolicyQueryResponse.builder()
.queryId(queryId)
.directAnswer(answer.getDirectAnswer())
.detailedAnalysis(answer.getDetailedAnalysis())
.keyReferences(answer.getKeyReferences())
.confidenceLevel(answer.getConfidenceLevel());
// 低置信度时添加提醒
if (answer.isNeedsHumanConsultation()) {
builder.disclaimer(
"⚠️ 本答复仅供参考,置信度较低。建议您进一步咨询相关主管部门以获取权威解答。"
);
builder.consultationAdvice(answer.getConsultationAdvice());
}
// 添加来源信息(用于可追溯性)
builder.sourceDocuments(
retrievalResult.getContexts().stream()
.map(ctx -> PolicySourceInfo.of(
ctx.getDocumentTitle(),
ctx.getDocumentNumber(),
ctx.getEffectiveDate()
))
.collect(Collectors.toList())
);
return builder.build();
}
}这段代码有几个政务场景的特殊设计:
1. 有效期过滤是检索的强制条件
buildEffectiveDateFilter 确保每次检索都只返回在查询时间点有效的文件。历史查询(查某个时间点的政策状态)通过 queryDate 参数支持。
2. 审计日志是流程的强制节点
auditLogger.log 不是异步的,必须在返回前完成。如果审计日志写入失败,整个查询也应该失败(或至少记录失败原因)。政务场景不允许「查询成功但没有留下记录」的情况。
3. 低置信度的处理
不是低置信度就不返回,而是在答复中明确标注「本答复仅供参考」。既给用户提供了信息,又做了风险提示,不会造成用户把低质量答复当成权威依据。
4. 条款引用图谱扩展
clauseGraphService.expandClauseReferences 自动处理交叉引用,这是政务文件的特殊需求,普通 RAG 系统不会做这一步。
政策知识库的构建和维护
政务 RAG 系统的知识库构建,和普通业务系统有几个不同之处。
文档分块策略
政策文件的分块不能按固定字符数切割,要按条款结构切割。一个「第X条」就是一个语义完整的单元,切断它会破坏语义。
我们的分块策略:
- 识别条款编号(第X条、第X款、第X项)
- 每个最小条款单元作为一个 chunk
- 条款的标题、背景说明("第X章 总则"等)作为 metadata,不参与检索但作为上下文
- 超长的条款(超过 600 字)用句子边界二次切割,保留重叠
元数据的重要性
每个 chunk 至少要携带:
- 来源文件名、文件编号
- 发布机关、发布日期、生效日期、废止日期(如有)
- 条款编号
- 所属章节
- 文件状态(ACTIVE/SUPERSEDED/ARCHIVED)
- 被哪个文件替代(如果是废止状态)
元数据不完整,有效期过滤就无法工作,整个系统的准确性就没有保障。
文件更新的处理
政策文件更新是常态。处理原则:
- 新文件入库时,查找是否有被替代的旧文件,更新旧文件状态为 SUPERSEDED
- 部分修订(只修订某些条款)的情况:更新相应 chunk,不影响未修订的 chunk
- 废止文件:状态更新为 ARCHIVED,不删除(历史查询需要)
这套元数据管理,需要和政务部门的内容管理系统(如果有的话)做集成,或者建立人工审核的更新流程。自动化做不到 100%,政策发布没有标准格式,最终还是需要有人把关。
在物理隔离网络里调试系统的那段经历
前面说到客户的网络物理隔离,我来分享一下在那个环境里调试系统的感受。
第一次去现场,带了一台安装好所有依赖的开发机(先在测试环境配好,然后打镜像,带进去)。进了机房,发现他们给的服务器系统比我预期的旧,Docker 版本不兼容,直接报错。
我在那个机房里,没有网络,没有手机,靠着从外面打印出来的错误信息和一本厚厚的手册,花了三个小时解决了依赖问题。那是我职业生涯里最有「解决问题」感的三个小时,也是最憋屈的三个小时。
这段经历给我的教训是:政务 AI 项目的运维预案,要比普通项目详细三倍。 所有可能出问题的环节,都要有离线解决方案。依赖包全量离线准备,模型文件本地存储,文档全部打印备份,调试工具全部预装。
不能依赖「遇到问题再查」这种工作方式,在物理隔离环境里,你没有「再查」的机会。
总结:政务 AI 的工程底线
政务 AI 给我最深的印象是:这个行业对「正确」有极高要求,但对「聪明」的要求反而不高。
用户不需要 AI 做特别创造性的回答,他们需要的是「把正确的政策条款,在正确的时间,以清晰的方式,展示给需要它的人」。这件事听起来简单,工程上一点都不简单。
几个政务 AI 工程底线:
1. 数据主权是第一优先级。 任何设计,先确保数据不出内网。
2. 准确性 > 覆盖率。 宁可说「不知道」,不能说错。
3. 可追溯性不可妥协。 每一次查询都要有完整记录,不是为了性能,是为了合规。
4. 低置信度的处理是系统能力的一部分。 知道自己不确定,比装作确定,更有价值。
5. 文件有效期管理必须做到位。 废止的文件继续被引用,是比回答不出来更严重的问题。
如果你在做政务 AI 项目,欢迎交流,这个行业的坑比大多数人想象的深。
