fix(intent): address review findings for hierarchical intent funnel and security guards

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-08 17:53:39 +08:00
co-authored by Cursor
parent 8a8582056c
commit 473fe7ec76
2 changed files with 304 additions and 58 deletions
+88 -58
View File
@@ -73,19 +73,19 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
}, nil
}
// 2. 纯 UI 视窗控制指令拦截:归入 CASUAL_CHAT 并给出不可执行提示
// 2. 纯 UI 视窗与主题控制指令拦截:归入 CASUAL_CHAT 并给出不可执行提示
if isPureUICommand(trimmed) {
top := &CandidateIntent{
Intent: IntentCasualChat,
Branch: BranchAmbiguousFallback,
Confidence: 0.90,
Intent: IntentCasualChat,
Branch: BranchNewIntentPreempt,
Confidence: 0.95,
FilledSlots: make(map[string]string),
Reasoning: "纯UI视窗控制指令,系统暂不支持通过对话直接操作界面视窗布局,请通过界面按钮或快捷键操作",
ClarificationQuestion: "检测到您下达了界面视窗控制指令(如折叠/打开侧边栏)。该操作属于前端UI视窗布局控制,请直接点击界面右上角或侧边的视窗切换按钮。",
Reasoning: "纯UI视窗或主题控制指令,系统暂不支持通过对话直接操作界面视窗布局或主题,请通过界面按钮或快捷键操作",
ClarificationQuestion: "检测到您下达了界面视窗或主题控制指令(如折叠/打开侧边栏、切换主题/深色浅色)。该操作属于前端UI视窗布局与主题控制,请直接点击界面右上角或侧边的视窗/主题切换按钮。",
}
return &IntentRecognitionResult{
RawInput: trimmed,
Branch: BranchAmbiguousFallback,
Branch: BranchNewIntentPreempt,
TopIntent: top,
Candidates: []CandidateIntent{*top},
ContextSnapshotID: snapID,
@@ -183,8 +183,8 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
branch := BranchNewIntentPreempt
requiresClarifying := false
// 1. 若为 DOCGOV_REVIEW_REJECT,检查必填槽位 reject_reason
if top.Intent == IntentDocGovReviewReject {
// 1. 若为 DOCGOV_REVIEW_REJECT 或 DOCGOV_FINAL_REJECT,检查必填槽位 reject_reason
if top.Intent == IntentDocGovReviewReject || top.Intent == IntentDocGovFinalReject {
if top.FilledSlots[SlotRejectReason] == "" {
requiresClarifying = true
top.ClarificationQuestion = "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
@@ -218,11 +218,17 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
func isSecurityViolation(input string) bool {
lower := strings.ToLower(input)
patterns := []string{
"修改身份", "身份修改", "修改职位", "职位修改", "修改岗位",
"修改身份", "身份修改", "修改职位", "职位修改", "修改岗位", "岗位修改",
"修改姓名", "姓名修改", "修改名字", "名字修改",
"修改部门", "部门修改",
"修改资质", "资质修改",
"改成批准人", "改成审核人", "改成编制人",
"工号改成", "修改工号", "工号修改",
"修改设置", "修改配置", "系统设置", "修改网关", "网关配置",
"修改设置", "修改配置", "系统设置", "系统配置",
"修改网关", "网关配置", "网关密钥", "大模型网关", "模型网关",
"修改密码", "重置密码", "提升权限", "越权",
"修改向量库", "向量库连接", "向量库配置", "修改向量库连接",
"修改知识库连接", "知识库连接", "知识库配置",
}
for _, p := range patterns {
if strings.Contains(lower, p) {
@@ -231,8 +237,9 @@ func isSecurityViolation(input string) bool {
}
secRegexes := []string{
`把.*(?:身份|职位|角色|工号|权限).*(?:修改|改|换)`,
`(?:修改|改|换).*(?:身份|职位|角色|工号|网关)`,
`把.*(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质).*(?:修改|改|换|重置|设为|变为)`,
`(?:修改|改|换|重置|设为).*(?:我的)?(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质|网关|向量库|知识库连接|密钥|apikey|api_key)`,
`.*(?:向量库连接|修改向量库|知识库连接|大模型网关密钥|网关密钥).*`,
}
for _, r := range secRegexes {
if matched, _ := regexp.MatchString(r, input); matched {
@@ -242,7 +249,7 @@ func isSecurityViolation(input string) bool {
return false
}
// isPureUICommand 检查是否为纯前端UI视窗布局控制指令
// isPureUICommand 检查是否为纯前端UI视窗布局与主题控制指令
func isPureUICommand(input string) bool {
patterns := []string{
"打开工具栏", "关闭工具栏",
@@ -251,6 +258,9 @@ func isPureUICommand(input string) bool {
"折叠工作台", "展开工作台",
"切换全屏", "全屏显示", "退出全屏",
"打开控制台", "关闭控制台",
"切换主题", "换成深色", "换成浅色",
"打开抽屉", "展开抽屉", "关闭抽屉",
"深色模式", "浅色模式", "深色主题", "浅色主题",
}
for _, p := range patterns {
if strings.Contains(input, p) {
@@ -260,6 +270,17 @@ func isPureUICommand(input string) bool {
return false
}
// resolveTargetDoc 优先从输入中提取显式受控文档名称,提取不到时回退至上下文的 ActiveDocID
func resolveTargetDoc(input string, userCtx *ContextSnapshot) string {
if doc := extractDocName(input); doc != "" {
return doc
}
if userCtx != nil && userCtx.ActiveDocID != "" {
return userCtx.ActiveDocID
}
return ""
}
// matchDocGovAction 匹配法定受控文档治理流转动作与槽位提取
func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent {
// 1. 驳回 (Review Reject or Final Reject)
@@ -279,12 +300,7 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
slots := make(map[string]string)
// target_doc
targetDoc := ""
if userCtx != nil && userCtx.ActiveDocID != "" {
targetDoc = userCtx.ActiveDocID
} else {
targetDoc = extractDocName(input)
}
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
@@ -308,17 +324,17 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
// Stage 0 权限与状态前置剪枝校验
if userCtx != nil {
if !isFinal && userCtx.UserDocRole != "" && userCtx.UserDocRole != "reviewer" && userCtx.UserDocRole != "system" {
confidence = 0.55
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是审核人(UserDocRole: %s),权限不符,无法执行审核驳回", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备审核驳回权限(需审核人角色)。", userCtx.UserDocRole)
} else if isFinal && userCtx.UserDocRole != "" && userCtx.UserDocRole != "approver" && userCtx.UserDocRole != "system" {
confidence = 0.55
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是批准人(UserDocRole: %s),权限不符,无法执行终审驳回", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备终审批准驳回权限(需批准人角色)。", userCtx.UserDocRole)
}
if userCtx.DocLifecycleState == "drafting" {
confidence = 0.55
confidence = 0.90
reasoning = "前置剪枝:当前文档处于编制草稿阶段(drafting),尚未提交审核,无法执行驳回操作"
clarifyQ = "当前文档处于编制中(drafting),尚未送审,无需驳回。"
}
@@ -339,22 +355,34 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
strings.Contains(input, "同意发布") || (strings.Contains(input, "批准") && !strings.Contains(input, "不")) {
targetIntent := IntentDocGovFinalApprove
slots := make(map[string]string)
targetDoc := ""
if userCtx != nil && userCtx.ActiveDocID != "" {
targetDoc = userCtx.ActiveDocID
} else {
targetDoc = extractDocName(input)
}
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
confidence := 0.95
reasoning := "用户下达受控文档终审批准生效指令"
clarifyQ := ""
if userCtx != nil {
if userCtx.UserDocRole != "" && userCtx.UserDocRole != "approver" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是批准人(UserDocRole: %s),权限不符,无法执行终审批准", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备终审批准权限(需批准人角色)。", userCtx.UserDocRole)
} else if userCtx.DocLifecycleState != "" && userCtx.DocLifecycleState != "approving" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前文档处于【%s】阶段,尚未进入终审批准阶段(approving),无法执行终审批准", userCtx.DocLifecycleState)
clarifyQ = fmt.Sprintf("当前文档处于【%s】阶段,未进入终审阶段,无法执行终审批准。", userCtx.DocLifecycleState)
}
}
return &CandidateIntent{
Intent: targetIntent,
Confidence: 0.95,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "用户下达受控文档终审批准生效指令",
Intent: targetIntent,
Confidence: confidence,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: reasoning,
ClarificationQuestion: clarifyQ,
}
}
@@ -364,22 +392,34 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
if !strings.Contains(input, "下一阶段") && !strings.Contains(input, "推进") {
targetIntent := IntentDocGovReviewApprove
slots := make(map[string]string)
targetDoc := ""
if userCtx != nil && userCtx.ActiveDocID != "" {
targetDoc = userCtx.ActiveDocID
} else {
targetDoc = extractDocName(input)
}
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
confidence := 0.95
reasoning := "用户下达受控文档审核通过指令"
clarifyQ := ""
if userCtx != nil {
if userCtx.UserDocRole != "" && userCtx.UserDocRole != "reviewer" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是审核人(UserDocRole: %s),权限不符,无法执行审核通过", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备审核通过权限(需审核人角色)。", userCtx.UserDocRole)
} else if userCtx.DocLifecycleState != "" && userCtx.DocLifecycleState != "reviewing" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前文档处于【%s】阶段,尚未进入审核阶段(reviewing),无法执行审核通过", userCtx.DocLifecycleState)
clarifyQ = fmt.Sprintf("当前文档处于【%s】阶段,未进入审核阶段,无法执行审核通过。", userCtx.DocLifecycleState)
}
}
return &CandidateIntent{
Intent: targetIntent,
Confidence: 0.95,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "用户下达受控文档审核通过指令",
Intent: targetIntent,
Confidence: confidence,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: reasoning,
ClarificationQuestion: clarifyQ,
}
}
}
@@ -387,12 +427,7 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
// 4. 认领文档 (Claim)
if strings.Contains(input, "认领") {
slots := make(map[string]string)
targetDoc := ""
if userCtx != nil && userCtx.ActiveDocID != "" {
targetDoc = userCtx.ActiveDocID
} else {
targetDoc = extractDocName(input)
}
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
@@ -409,12 +444,7 @@ func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent
// 5. 提交送审 (Submit Review)
if strings.Contains(input, "送审") || strings.Contains(input, "提交审核") || strings.Contains(input, "发起审核") || strings.Contains(input, "提审") {
slots := make(map[string]string)
targetDoc := ""
if userCtx != nil && userCtx.ActiveDocID != "" {
targetDoc = userCtx.ActiveDocID
} else {
targetDoc = extractDocName(input)
}
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
@@ -567,7 +597,7 @@ func extractRejectReason(input string) string {
return strings.Trim(res, ",,。;; ")
}
reAfterReject := regexp.MustCompile(`驳回[,,\s]+(.+)`)
reAfterReject := regexp.MustCompile(`驳回[,,::\s]+(.+)`)
if match := reAfterReject.FindStringSubmatch(input); len(match) > 1 {
res := strings.TrimSpace(match[1])
res = regexp.MustCompile(`^(?:意见为|意见是|意见|理由为|理由是|理由|原因|批注为|批注|修改意见)[::\s]\s*`).ReplaceAllString(res, "")
+216
View File
@@ -433,6 +433,12 @@ func TestIntentRecognition_PureUICommand(t *testing.T) {
if res.TopIntent.Intent != intent.IntentCasualChat {
t.Errorf("expected intent CASUAL_CHAT for pure UI command, got %s", res.TopIntent.Intent)
}
if res.Branch != intent.BranchNewIntentPreempt {
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res.Branch)
}
if res.TopIntent.Confidence != 0.95 {
t.Errorf("expected confidence 0.95, got %v", res.TopIntent.Confidence)
}
if !strings.Contains(res.TopIntent.Reasoning, "UI") && !strings.Contains(res.TopIntent.ClarificationQuestion, "UI") {
t.Errorf("expected reasoning or clarification question to mention UI, got reasoning: %s", res.TopIntent.Reasoning)
}
@@ -492,3 +498,213 @@ func TestIntentRecognition_DocGov_OtherActions(t *testing.T) {
t.Errorf("expected intent DOCGOV_FINAL_APPROVE, got %s", resFinal.TopIntent.Intent)
}
}
func TestIntentRecognition_FinalReject_MissingReason(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{
SnapshotID: "ctx_final_rej_1",
CurrentStage: string(bizdev.StageV05_Feasibility),
ActiveDocID: "doc-002",
DocLifecycleState: "approving",
UserDocRole: "approver",
}
res, err := rec.Recognize(context.Background(), "把调研报告终审驳回", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if res.TopIntent.Intent != intent.IntentDocGovFinalReject {
t.Errorf("expected intent DOCGOV_FINAL_REJECT, got %s", res.TopIntent.Intent)
}
if !res.RequiresClarifying {
t.Errorf("expected RequiresClarifying to be true when reject_reason is missing in final reject")
}
expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
if res.TopIntent.ClarificationQuestion != expectedClarify {
t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion)
}
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" {
t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason])
}
if res.Branch != intent.BranchAmbiguousFallback {
t.Errorf("expected branch AMBIGUOUS_FALLBACK when slot is missing, got %s", res.Branch)
}
}
func TestIntentRecognition_SecurityForbidden_Extended(t *testing.T) {
rec := intent.NewRecognizer()
testInputs := []struct {
input string
desc string
}{
{"把姓名修改为李工", "姓名修改"},
{"修改我的部门为微创中心", "部门修改"},
{"把资质改成主任医师", "资质修改"},
{"修改向量库连接", "向量库连接修改"},
{"修改大模型网关密钥", "网关密钥修改"},
{"把知识库连接改为192.168.1.100:5432", "知识库连接修改"},
}
for _, tc := range testInputs {
t.Run(tc.desc, func(t *testing.T) {
res, err := rec.Recognize(context.Background(), tc.input, nil)
if err != nil {
t.Fatalf("recognize '%s' failed: %v", tc.input, err)
}
if res.TopIntent.Intent != intent.IntentSecurityForbidden {
t.Errorf("input '%s': expected intent SECURITY_FORBIDDEN, got %s", tc.input, res.TopIntent.Intent)
}
if res.TopIntent.Confidence != 1.0 {
t.Errorf("input '%s': expected confidence 1.0, got %v", tc.input, res.TopIntent.Confidence)
}
if res.Branch != intent.BranchNewIntentPreempt {
t.Errorf("input '%s': expected branch NEW_INTENT_PREEMPT, got %s", tc.input, res.Branch)
}
})
}
}
func TestIntentRecognition_ExplicitDocName_OverActiveDocID(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{
SnapshotID: "ctx_explicit_doc",
CurrentStage: string(bizdev.StageV05_Feasibility),
ActiveDocID: "doc-001-intake", // 上下文中当前文档是需求收集表
DocLifecycleState: "reviewing",
UserDocRole: "reviewer",
}
// 显式指定文档名称为“调研报告”,应优先提取“调研报告”而不是上下文的“doc-001-intake”
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
}
if res.TopIntent.FilledSlots[intent.SlotTargetDoc] != "调研报告" {
t.Errorf("expected target_doc '调研报告', got '%s'", res.TopIntent.FilledSlots[intent.SlotTargetDoc])
}
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", res.TopIntent.FilledSlots[intent.SlotRejectReason])
}
// 显式指定文档 ID
resDocID, err := rec.Recognize(context.Background(), "把 doc-003 审核通过", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-003" {
t.Errorf("expected target_doc 'doc-003', got '%s'", resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc])
}
// 未显式指定文档名称,应回退使用 ActiveDocID
resFallback, err := rec.Recognize(context.Background(), "审核通过,同意进入下一环节", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-001-intake" {
t.Errorf("expected fallback target_doc 'doc-001-intake', got '%s'", resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc])
}
}
func TestIntentRecognition_RejectReason_ColonSeparators(t *testing.T) {
rec := intent.NewRecognizer()
ctx := &intent.ContextSnapshot{
ActiveDocID: "doc-002",
DocLifecycleState: "reviewing",
UserDocRole: "reviewer",
}
// 全角冒号
resCN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if resCN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resCN.TopIntent.FilledSlots[intent.SlotRejectReason])
}
// 半角冒号
resEN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
if resEN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" {
t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resEN.TopIntent.FilledSlots[intent.SlotRejectReason])
}
}
func TestIntentRecognition_PureUICommands_Extended(t *testing.T) {
rec := intent.NewRecognizer()
uiCommands := []string{
"切换主题",
"换成深色",
"换成浅色",
"打开抽屉",
"展开抽屉",
"关闭抽屉",
"请帮我折叠侧边栏",
"展开侧边栏",
"打开工具栏",
"切换全屏",
}
for _, cmd := range uiCommands {
t.Run(cmd, func(t *testing.T) {
res, err := rec.Recognize(context.Background(), cmd, nil)
if err != nil {
t.Fatalf("recognize '%s' failed: %v", cmd, err)
}
if res.TopIntent.Intent != intent.IntentCasualChat {
t.Errorf("cmd '%s': expected intent CASUAL_CHAT, got %s", cmd, res.TopIntent.Intent)
}
if res.Branch != intent.BranchNewIntentPreempt {
t.Errorf("cmd '%s': expected branch NEW_INTENT_PREEMPT, got %s", cmd, res.Branch)
}
if res.TopIntent.Confidence != 0.95 {
t.Errorf("cmd '%s': expected confidence 0.95, got %v", cmd, res.TopIntent.Confidence)
}
if res.RequiresClarifying {
t.Errorf("cmd '%s': expected RequiresClarifying to be false", cmd)
}
})
}
}
func TestIntentRecognition_PermissionMismatch_NotHijackedBySupplement(t *testing.T) {
rec := intent.NewRecognizer()
// 用户是非审核人角色,但输入包含“修改为”(如果未拦截会被 SUPPLEMENT_INFO 0.88 抢占)
ctx := &intent.ContextSnapshot{
SnapshotID: "ctx_unauth_supp",
ActiveDocID: "doc-002",
DocLifecycleState: "reviewing",
UserDocRole: "author", // 非审核人
}
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,修改为第三阶段指标", ctx)
if err != nil {
t.Fatalf("recognize failed: %v", err)
}
// 必须保持 DOCGOV_REVIEW_REJECT,不能被 SUPPLEMENT_INFO 抢占
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
t.Errorf("expected intent DOCGOV_REVIEW_REJECT to stay top, got %s", res.TopIntent.Intent)
}
if res.TopIntent.Confidence < 0.90 {
t.Errorf("expected confidence >= 0.90, got %v", res.TopIntent.Confidence)
}
if !res.RequiresClarifying {
t.Errorf("expected RequiresClarifying to be true")
}
if !strings.Contains(res.TopIntent.Reasoning, "权限不符") {
t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning)
}
if res.Branch != intent.BranchAmbiguousFallback {
t.Errorf("expected branch AMBIGUOUS_FALLBACK, got %s", res.Branch)
}
}