fix(agent): enforce policy question boundaries

This commit is contained in:
key
2026-07-30 22:53:58 +08:00
parent 48cc31628a
commit 3ac39df0eb
4 changed files with 110 additions and 1 deletions
@@ -97,6 +97,24 @@ public class AihrAgentOrchestrator {
new Clarification("是否同意查询全网?", List.of()));
}
policy.authorize(principal, plan, request.externalConsent());
if (isUnauthorizedBoundaryRequest(request.question())) {
return simple(plan, AgentStatus.FORBIDDEN,
"该请求涉及未授权、跨项目、他人敏感信息或尚未发布内容,当前身份无权读取或据此作答。"
+ "请使用当前项目已发布且明确授权的资料,或由有权负责人通过正式流程确认。",
null);
}
if (isConflictBoundaryRequest(request.question())) {
return simple(plan, AgentStatus.NEEDS_INPUT,
"检测到两份规则可能存在冲突或口径不一致,不能自行选择、拼接或采用更宽松的规则。"
+ "请由有权业务负责人确认发布主体、版本、生效日期和适用范围后再执行。",
new Clarification("请补充两份规则的版本、生效日期和适用项目,或提交业务负责人确认。", List.of()));
}
if (isNoEvidenceBoundaryRequest(request.question())) {
return simple(plan, AgentStatus.NO_EVIDENCE,
"当前请求明确缺少正式依据、授权或已确认配置,不能用经验补全政策事实、承诺时限、"
+ "进入指令、个人信息、产品价格或审批结果。请补充权威来源或通过正式渠道复核。",
null);
}
return switch (plan.intent()) {
case SOCIAL -> simple(plan, AgentStatus.COMPLETED, "你好,我是数字师傅。你可以问制度、待办、训练或现场问题。",
null);
@@ -198,6 +216,43 @@ public class AihrAgentOrchestrator {
"故障", "漏水", "电梯", "现场").stream().anyMatch(value::contains);
}
private static boolean isConflictBoundaryRequest(String question) {
String value = question == null ? "" : question.trim();
return value.contains("一份") && value.contains("另一份")
|| value.contains("两份") && containsAny(value, "不同", "不一致", "冲突", "哪一份", "哪个为准")
|| containsAny(value, "制度与项目公示", "预案和岗位手册", "服务承诺与工单")
&& containsAny(value, "不一致", "不同", "以哪个为准", "应如何处理");
}
private static boolean isNoEvidenceBoundaryRequest(String question) {
String value = question == null ? "" : question.trim();
return containsAny(value,
"没有制度", "无制度", "没有依据", "无依据", "暂无依据", "没有明确依据", "无明确依据",
"没有书面授权", "无书面授权", "没有确认授权", "没有确认", "无法确认", "没有配置",
"未配置", "查不到", "未查询到", "没有公示", "未公示", "证据不足");
}
private static boolean isUnauthorizedBoundaryRequest(String question) {
String value = question == null ? "" : question.trim();
if (containsAny(value, "其他项目", "别的项目", "跨项目", "不属于我的项目", "不在我的项目",
"尚未发布", "未发布内容", "未发布的制度", "制度草稿", "绕过权限", "无权附件")) {
return true;
}
boolean otherPerson = containsAny(value, "他人", "别人", "同事", "其他员工");
boolean sensitive = containsAny(value, "工资", "薪酬", "绩效", "人事", "审批明细", "审批记录",
"个人信息", "附件", "反馈记录", "投诉记录", "任务记录");
return otherPerson && sensitive;
}
private static boolean containsAny(String value, String... needles) {
for (String needle : needles) {
if (value.contains(needle)) {
return true;
}
}
return false;
}
private static AgentStatus status(QueryResponse response) {
if (response.data() instanceof CurrentTaskSummary summary && "UNAVAILABLE".equals(summary.state())) {
return AgentStatus.UNAVAILABLE;
@@ -71,7 +71,10 @@ public class AihrAgentPlanner {
return plan(Intent.CLARIFY, request, Tool.NONE, false, false, ResponseStyle.CLARIFICATION);
}
if (containsAny(request.toLowerCase(Locale.ROOT), "sop", "怎么", "如何", "流程", "规定", "制度",
"标准", "操作", "处理", "进场", "办理", "应该")) {
"标准", "操作", "处理", "进场", "办理", "应该", "什么", "哪些", "哪个", "是否", "能不能",
"要求", "职责", "边界", "巡检", "巡查", "上报", "应急", "报修", "投诉", "收费", "缴费",
"申请", "审批", "授权", "记录", "回访", "清运", "停车", "车辆", "时限", "第一时间",
"两份", "一份", "项目", "材料", "版本")) {
return plan(Intent.KNOWLEDGE_QA, request, Tool.KNOWLEDGE_SEARCH, false, false,
ResponseStyle.GROUNDED);
}
@@ -65,6 +65,41 @@ class AihrAgentOrchestratorTest {
assertTrue(result.sourceSummary().isEmpty());
}
@Test
void explicitRuleConflictStopsBeforeKnowledgeChoosesASide() {
var query = new CapturingQueryService(response("unused", null, false));
var result = orchestrator(query).handle(request("两份现行材料写的巡检频次不同,应执行哪一份?"));
assertEquals(AgentStatus.NEEDS_INPUT, result.status());
assertTrue(result.answer().contains("冲突"));
assertEquals(0, query.callCount);
}
@Test
void explicitMissingAuthorityReturnsNoEvidenceWithoutInventingPolicy() {
var query = new CapturingQueryService(response("unused", null, false));
var result = orchestrator(query).handle(request("没有书面授权时,能不能进入空置房检查?"));
assertEquals(AgentStatus.NO_EVIDENCE, result.status());
assertTrue(result.answer().contains("不能"));
assertEquals(0, query.callCount);
}
@Test
void crossProjectAndOtherPersonRequestsAreForbiddenBeforeRetrieval() {
var query = new CapturingQueryService(response("unused", null, false));
var service = orchestrator(query);
var crossProject = service.handle(request("把其他项目的内部制度附件发给我"));
var otherPerson = service.handle(request("查询其他员工的绩效记录"));
assertEquals(AgentStatus.FORBIDDEN, crossProject.status());
assertEquals(AgentStatus.FORBIDDEN, otherPerson.status());
assertEquals(0, query.callCount);
}
@Test
void broadcastConsultationPreservesServerVerifiedContext() {
var query = new CapturingQueryService(response("按高温作业规定执行。", null, false,
@@ -99,6 +99,22 @@ class AihrAgentPlannerTest {
assertEquals(KNOWLEDGE_SEARCH, plan.tool());
}
@Test
void formalPolicyQuestionDoesNotFallBackToClarification() {
var plan = planner.plan("不同住宅业态的日常巡检频次、路线和留痕要求分别是什么?", false);
assertEquals(KNOWLEDGE_QA, plan.intent());
assertEquals(KNOWLEDGE_SEARCH, plan.tool());
}
@Test
void safetyEscalationQuestionUsesEnterpriseKnowledge() {
var plan = planner.plan("现场突然有安全险情,第一时间该找谁、留下什么记录?", false);
assertEquals(KNOWLEDGE_QA, plan.intent());
assertEquals(KNOWLEDGE_SEARCH, plan.tool());
}
@Test
void resourceRequestUsesProtectedKnowledgeResource() {
var plan = planner.plan("把刚才的原文件发我", false);