报销额度查询会对不存在的员工返回"成功"结果(expense.quota_query 无存在性校验)
现象
在团队 TL 对话中询问「员工张三 2026 年 9 月的报销额度」,财务成员追问工号,用户随手补充 666。系统随后回复:
员工张三(工号 666)2026年9月的报销额度为 20,000.00 CNY,当前已用 0.00 CNY,剩余额度 20,000.00 CNY,额度充足。
「张三」和工号 666 在花名册、知识库、任何人员主数据中都不存在。用户随后反问,TL 才承认查不到该员工。
复现会话:session_80ac377dd3c64f23(TL)→ 成员会话 session_861fcb66894243a2,团队任务 task_ebe76a65f8ef4ed1。
根因(按数据流逐层排查)
| 层 |
发生了什么 |
是否校验员工存在 |
| TL turn_planner |
创建 TaskFrame,slot_hints={employee_name: 张三},指派财务成员 |
否 |
SOP skill_expense_quota_query / node_collect_info |
只抽取 employee_id、month 两个槽位,没有按姓名查工号的能力,于是追问工号 |
否 |
用户补充 666 → /tasks/{id}/resume |
原文写入 review_json.comment,唤醒成员 rework |
否(也不应该) |
node_call_quota_query |
employee_id=666 直接调用 HTTP 工具 expense.quota_query |
否 |
外部 mock http://58.57.119.30:52008/api/mock/expense/quota_query |
对任意 employee_id 都返回 HTTP 200 + total_quota=20000 |
否 ← 根因 |
node_response_result |
指令是"根据工具返回的 total_quota/used/remaining 展示",只在"工具返回异常或数据缺失"时说明情况;工具既没异常也没缺失,于是如实转述 |
否 |
| TL 汇总 |
原文转发成员报告 |
否 |
直接对该 mock 端点探测:
employee_id=666 → total_quota=20000, remaining=20000, "额度充足,请安心使用"
employee_id=999999 → total_quota=20000, remaining=20000
employee_id=DOES_NOT_EXIST → total_quota=20000, remaining=20000, "额度充足,请安心使用"
employee_id="" → employee_id 被改写为 "UNKNOWN",仍然 total_quota=20000
同一 mock 主机上的兄弟端点行为并不一致:hr/balance_query 对不存在工号会返回「未找到工号为 … 的员工记录」,expense/submit 会直接 rejected: 员工工号不存在。只有 expense/quota_query 对任何输入都"查得到"。
模型没有虚构数字:harness_tool_completed 事件里的 result.data 与最终回复完全一致。「张三」这个名字来自用户输入,被 SOP 存进 employee_name 槽位后与工具结果拼在一起输出,从而形成"张三(工号 666)额度 20000"这个看起来像查到真人的答案。
为什么这是系统性问题,而不只是 mock 的问题
- 工具契约里没有"未命中"语义。
expense.quota_query 的 output_schema 只有 employee_id/month/total_quota/used/remaining/currency/message,没有 found/error_code 之类字段。即使换成真实后端,只要它对未知工号返回默认额度,链路照样会把它当事实播报。对比本仓库 backend/app/api/mock.py 里的 _order_miss/_product_miss,它们都显式带 found: false 和 miss_reason。
- SOP 的响应节点只防"异常",不防"看似成功的空结果"。
node_response_result 的指令把"工具成功"等价于"业务结果成立"。
- 用户提供的标识符被当作已验证事实。 姓名 → 工号的映射没有任何一环去查花名册或人员主数据;TL 在下一轮才用花名册否定了这条记录,说明系统里其实有可用的校验源,只是没接进 SOP。
- mock 地址硬编码为外部 IP,且仓库内没有对应实现。
backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.json 里 11 个工具全部指向 58.57.119.30:52008,本地 app/api/mock.py 没有 /expense/quota_query 路由。演示行为不受版本控制,无法在仓库内修复或加测试。
- TL 侧 Trace 看不到成员的工具调用。
/api/enterprise/traces/{session_id} 只返回 TL 会话的事件,tool_call_count=0,实际的 harness_tool_completed 落在成员会话里。排查时必须手动跳到 team_tasks.session_id 才能看到真正的工具入参出参。
建议修复
按优先级:
- 工具契约:
expense.quota_query(以及所有按工号查询的工具)的 output_schema 增加 found: boolean 与 miss_reason;mock 端对未知工号返回 found=false,与 hr/balance_query、expense/submit 对齐。
- SOP 响应节点:
node_response_result 指令显式要求「若 found=false 或返回内容为默认值 / 未识别工号,必须告知用户未找到该员工,不得展示额度数字」。
- 身份校验前置:
node_collect_info 拿到姓名或工号后,先查花名册 / 人员主数据确认存在,再进入工具调用;查不到就明确回复"未找到该员工",不追问工号。
- mock 收编进仓库:在
backend/app/api/mock.py 补 /expense/quota_query 等路由,seed 里的 URL 改为可配置的本地地址,并加回归测试:未知工号必须返回 found=false。
- Trace 可观测性:TL 会话的 trace 聚合下游成员会话的
harness_tool_completed 事件,或至少在 TL 侧给出成员会话链接,避免"看 Trace 却看不到工具调用"。
复现步骤
- 在团队 TL 会话中发送:
帮我查询一下员工张三 2026 年 9 月的报销额度和剩余额度。
- 财务成员追问工号时,回复任意不存在的工号,例如
666。
- 观察 TL 回复给出 20,000 CNY 额度,且把用户输入的姓名与工号一并当作已确认员工信息输出。
- 直接请求
POST http://58.57.119.30:52008/api/mock/expense/quota_query,{"employee_id":"DOES_NOT_EXIST","month":"2026-09"},得到同样的 20,000 额度。
相关位置
backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.json:tools[10](expense.quota_query)、skills[14](skill_expense_quota_query)
backend/app/api/mock.py:本地 mock,缺 expense/quota_query
backend/app/api/teams.py:resume_team_task,用户补充的工号在此进入任务
backend/app/api/traces.py:TL 侧 trace 不包含成员会话事件
报销额度查询会对不存在的员工返回"成功"结果(expense.quota_query 无存在性校验)
现象
在团队 TL 对话中询问「员工张三 2026 年 9 月的报销额度」,财务成员追问工号,用户随手补充
666。系统随后回复:「张三」和工号
666在花名册、知识库、任何人员主数据中都不存在。用户随后反问,TL 才承认查不到该员工。复现会话:
session_80ac377dd3c64f23(TL)→ 成员会话session_861fcb66894243a2,团队任务task_ebe76a65f8ef4ed1。根因(按数据流逐层排查)
slot_hints={employee_name: 张三},指派财务成员skill_expense_quota_query/node_collect_infoemployee_id、month两个槽位,没有按姓名查工号的能力,于是追问工号666→/tasks/{id}/resumereview_json.comment,唤醒成员 reworknode_call_quota_queryemployee_id=666直接调用 HTTP 工具expense.quota_queryhttp://58.57.119.30:52008/api/mock/expense/quota_queryemployee_id都返回 HTTP 200 +total_quota=20000node_response_result直接对该 mock 端点探测:
同一 mock 主机上的兄弟端点行为并不一致:
hr/balance_query对不存在工号会返回「未找到工号为 … 的员工记录」,expense/submit会直接rejected: 员工工号不存在。只有expense/quota_query对任何输入都"查得到"。模型没有虚构数字:
harness_tool_completed事件里的result.data与最终回复完全一致。「张三」这个名字来自用户输入,被 SOP 存进employee_name槽位后与工具结果拼在一起输出,从而形成"张三(工号 666)额度 20000"这个看起来像查到真人的答案。为什么这是系统性问题,而不只是 mock 的问题
expense.quota_query的output_schema只有employee_id/month/total_quota/used/remaining/currency/message,没有found/error_code之类字段。即使换成真实后端,只要它对未知工号返回默认额度,链路照样会把它当事实播报。对比本仓库backend/app/api/mock.py里的_order_miss/_product_miss,它们都显式带found: false和miss_reason。node_response_result的指令把"工具成功"等价于"业务结果成立"。backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.json里 11 个工具全部指向58.57.119.30:52008,本地app/api/mock.py没有/expense/quota_query路由。演示行为不受版本控制,无法在仓库内修复或加测试。/api/enterprise/traces/{session_id}只返回 TL 会话的事件,tool_call_count=0,实际的harness_tool_completed落在成员会话里。排查时必须手动跳到team_tasks.session_id才能看到真正的工具入参出参。建议修复
按优先级:
expense.quota_query(以及所有按工号查询的工具)的output_schema增加found: boolean与miss_reason;mock 端对未知工号返回found=false,与hr/balance_query、expense/submit对齐。node_response_result指令显式要求「若found=false或返回内容为默认值 / 未识别工号,必须告知用户未找到该员工,不得展示额度数字」。node_collect_info拿到姓名或工号后,先查花名册 / 人员主数据确认存在,再进入工具调用;查不到就明确回复"未找到该员工",不追问工号。backend/app/api/mock.py补/expense/quota_query等路由,seed 里的 URL 改为可配置的本地地址,并加回归测试:未知工号必须返回found=false。harness_tool_completed事件,或至少在 TL 侧给出成员会话链接,避免"看 Trace 却看不到工具调用"。复现步骤
帮我查询一下员工张三 2026 年 9 月的报销额度和剩余额度。666。POST http://58.57.119.30:52008/api/mock/expense/quota_query,{"employee_id":"DOES_NOT_EXIST","month":"2026-09"},得到同样的 20,000 额度。相关位置
backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.json:tools[10](expense.quota_query)、skills[14](skill_expense_quota_query)backend/app/api/mock.py:本地 mock,缺expense/quota_querybackend/app/api/teams.py:resume_team_task,用户补充的工号在此进入任务backend/app/api/traces.py:TL 侧 trace 不包含成员会话事件