lzf_0626
|
8283f6ab69
|
feat(customer-service): 把闲聊提示词补回生效版本,并修掉发布脚本的两个坑
**背景**:提示词配过(挂在 release 174),但 174 已被取代,而 load_active_prompt 是先定位
active 版本、再按 release_id 查的 —— 于是读不到,Agent 回落到代码默认值。功能看着正常
(_chitchat_prompt 有逐字段兜底),所以一直没人发现,也没有任何告警。
发布脚本原先有两个坑,这次一并修掉:
1. **version 写死为 1**。prompt_template_version 的唯一键是 (prompt_code, version),
客服那条已经占了 v1,照搬旧行会主键冲突。改为取现有最大值 +1(本次自动分到 v2)。
2. **继承只读 platform_config_item**。改用 ConfigReleaseService.effective_snapshot(),
它覆盖全部三张受管表;并且做**字段名映射**(库的 config_key → API 的 item_key、
value_json 归一化)—— 快照行是库的形状,直接 POST 会 422。
实测:
- 发布走路径二(路径一如预期被状态机拒:409 RUN_NOT_CANCELLABLE「只能修改草稿发布版本」)
- 新版本 201 继承 9 条配置项、一条没丢;提示词 v2 随之生效
- load_active_prompt 现在返回 release_id=201 / version=2(此前为 None)
- 闲聊链路:status=succeeded、intent=chitchat,回答「您好,我是南方科技智能客服,
想了解基金、理财还是账户服务?」—— 简洁、自然引导到业务,符合提示词要求
提示词正文与代码默认值**刻意保持一致**:发布前后行为不变,变的只是"能不能改"
(改话术从此要经审核并留痕)。Agent 侧仍保留代码默认值作为兜底。
ruff / mypy(136 文件) / 612 unit+contract / 29 integration 全绿。
|
2026-09-11 13:26:43 +08:00 |
|
lzf_0626
|
cdbd85b27c
|
test(platform): 端到端验证配置项丢失告警,并把它收成可复用的回归工具
上一轮加了"激活时点名将被丢掉的配置项"之后,我只用 caplog 验证了方法本身,
**没有跑过真实激活**——这一步把缺口补上。
tools/verify_config_drop_warning.py 走完整的"创建 → 加配置项 → 提交复核 → 审核 → 激活"
状态机,发三个版本:
- A:把当前生效配置项原样复制 → 一条不少,无告警(不产生噪音)
- B:去掉 agent_tools/risk:general → 告警 1 条并**精确点名**该配置项
- C:把完整的那份再发一次 → 恢复原状,无告警
跑完校验生效配置项与起点一致,避免把环境留在"少一条"的状态。
实测结果:A 告警 0 条 / B 告警 1 条且点名 agent_tools/risk:general / C 告警 0 条;
起点与终点均为 9 条配置项,环境已复原。
|
2026-09-11 13:05:33 +08:00 |
|
lzf_0626
|
554fbbcaab
|
fix(identity): data_scope 不能写死 self——它让所有"看全量"的路径失效
实测发现:9002(risk_operator) 与 9003(admin) 都持有 all 级权限
(permission_scopes 里 audit:read='all'、risk:alert:read='all' 等),
但 context.data_scope **永远是 'self'**——identity_repository.py 第 47 行把它写死了,
而上面第 34-39 行刚算出每个权限的 scope 并取了最高(rank 表都写好了)。
后果:凡按 context.data_scope == "all" 判断能否看全量的路径全部走不通
(risk_query_service.py:198、risk_analysis_service.py:126、
risk_evidence_archive_service.py:218、risk_action_service.py:212),
风控专员拿着全量权限却查不到任何预警——这是上一轮三个工具"都返回空"的真正原因。
改为取该身份所有权限里的最高范围。没有 all 权限的角色行为不变
(实测 9001 customer 的 data_scope 仍是 self),因此不放松任何既有边界。
另加 tools/seed_risk_alert_demo_data.py:造 3 条演示预警覆盖三个只读工具的读路径
(高危待处理 / 中危调查中 / 低危已闭环),字段取值照 risk_scan_service.py:312-333 的
_build_alert 抄、状态用 OPEN_STATUSES,时间按库内约定存 UTC。注意该表 id 非自增,
所以脚本手工生成 id。
实测(9002 身份):
- 查看当前风险概览 → 未闭环 2 条、高危 2 条、待处理 1 条,含高优先级清单与证据摘要
- 查询高风险预警 → 2 条明细,命中 RW-002/003/007/012/015,附只读复核草案
- 查询 ALDEMO0001 的证据 → 完整快照事实 + 客户维度,并主动指出证据缺口
客服回归:9001 身份行为不变。
|
2026-09-11 12:54:51 +08:00 |
|
lzf_0626
|
39c7ab51f4
|
feat(risk): 发布风控配置并补齐缺失的 RBAC 权限
修 docs/25 里的 P0:风控的意图配置与工具白名单一条都没发布,而白名单是失败关闭的,
导致任何工具调用都被拒。按组员交付的《20-Agent工具白名单与意图配置》补齐。
1. tools/publish_risk_agent_config.py:导入 4 条 risk 意图配置(id=61-64,active)与
4 条 agent_tools 白名单(risk_overview / risk_search / risk_evidence / general)。
发布版本 id=188 active,并且**继承了现有 5 条配置项**——config_release 是整版本替换
语义,不继承会把客服的 4 条白名单和示例 Agent 的 fund_query_demo:fund_quote 静默清空。
2. tools/grant_risk_alert_read_permission.py:补齐 risk:alert:read 权限。
实测发现这条权限在 sys_permission 里**根本不存在**,连 risk_operator 角色也没有,
所以任何身份调用风控工具都会拿到"缺少工具权限"。交付文档第 116 行正把这一项列为
接入前置条件。权限匹配实际用 permission_code 全串(identity_repository.py:25-37),
resource/action 只是元数据(照 fund:quote:read 的拆法);data_scope 取 all,因为
risk_query_service.py:194、risk_analysis_service.py:126 等按 context.data_scope == "all"
决定是否放行全量数据。只授权 risk_operator,不动 admin(交付文档只要求前者)。
验证(以 9002 risk_operator 身份实测):
- "查看当前风险概览" → status=succeeded、意图 risk_overview、工具真实返回数据
- "查询高风险预警" → status=succeeded、意图 risk_search
修复前两者均为 failed + ForbiddenAgentError: 缺少工具权限。
|
2026-09-11 12:18:34 +08:00 |
|
lzf_0626
|
478b64e4d7
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop_1
# Conflicts:
# app/service/agent/bootstrap.py
|
2026-09-11 10:43:08 +08:00 |
|
lzf_0626
|
33fbb0eb01
|
feat(customer-service): 接入适当性裁决,回答"我这个等级能不能买它"
客户实测反馈:「c1客户能买它吗」答的是 C1 的通用规则,没回答"能不能买季季盈90天"。
根因是这个问题需要**组合两个事实**——产品的风险等级(R2)与客户档案等级能不能匹配——
而检索只能给出"最像的那段原文",给不出结论。基座早就有了 check_suitability 裁决工具,
接口留好了但客服没接线(这个项目里第三次遇到同一类情况)。
改动三处:
1. 新增 suitability_check 意图,意图码三处对齐(AgentDefinition.supported_intents、
agent_intent_config 的 active 行、发布版 agent_tools 白名单)。
2. 新出口 _answer_suitability:产品风险等级**从知识库查出来**(不猜、也不采信问句里
出现的"R2"字样),产品名只取上一轮回答里的主语(来自知识块字段,可信),客户等级
交给 check_suitability 按档案解析——**不采信客户自称**。任何一步拿不到确定值就转人工:
这个出口会给出"能不能买"的结论,宁可答不了也不能答错。
3. 发布脚本加意图注册与工具白名单,并做成幂等(重跑不会因为"已经审过了"而 409)。
实测:意图正确路由到新出口,裁决链路走通。9001 因为在 fin_risk_assessment 里没有测评
记录,系统给出"暂时无法购买 + 您目前没有在有效期内的风险测评结果"——这正是适当性管理
要求的行为,不是故障:不能卖给一个没有有效测评结果的客户。措辞也据此改过,不写
"您的等级为未记录"这种客户看不懂的句子。
顺带修了前端一处误导标记:它用"是否含客服热线"判断"已引导人工",而正常的适当性回答
里也会建议拨打客服热线,于是"已经给出结论"被误报成"已引导人工"。
|
2026-09-10 22:42:17 +08:00 |
|
lzf_0626
|
7b3a72860c
|
feat(knowledge): 为 C1-C5 各补一条「能买什么产品」的问答
客户实测反馈:「C1 客户能买什么」被引导到人工客服,而知识库里其实有答案。
实测分数:这条问句 top1 只有 0.5633、与次优差 0.0236(低于 0.07 门槛)→ 转人工;
而「C1 保守型客户可以买哪些风险等级的产品」是 0.7794,过了 0.75 硬门槛、能答。
根因是客户与知识库的用词鸿沟:客户说「C1 客户」,知识块标题写的是「C1 保守型」。
短问法少了"保守型"这个锚点就差 0.19 分——而客户不知道 C1 就等于保守型,这正是他要问的。
按 A 方案(数据问题用数据解决)为 C1-C5 各补一条 FAQ,答案全部取自
《个人投资者适当性管理指南》原文,不自行编写:
- 第十二条投资者与产品匹配矩阵(各级别可购买的产品风险等级)
- 第十四条硬匹配规则的跨级禁止要求
- 第十五条豁免规则(C3 买 R4、C4 买 R5 的签署揭示书与持仓上限)
知识块 631 → 636。同时修正 load 脚本自检里过时的期望:这句话现在命中 FAQ 而非
POL-AST(两者是同一份内容,只是 FAQ 的问句措辞更接近客户口语)。
验证:C1-C5 六个等级的「能买什么」问法全部直接回答、无一转人工;
ruff / mypy / 468 unit+contract 全绿。
|
2026-09-10 22:34:20 +08:00 |
|
lzf_0626
|
4c2b147793
|
feat(knowledge): 产品知识拆到表格行级,并按问句选粒度
问题(客户实测反馈):同一会话里问「季季盈90天起投多少」和「那它风险高吗」,两次回答
**一模一样**——都是整个产品小节的表格。客户问的是风险,收到的是整张说明书,看起来像
客服没听懂问题。
根因是切分粒度:原来"一个叶子标题 = 一块",产品手册里就是整个产品小节(表格 + 说明)
成一块。这既让两个不同的问题命中同一块,也让整节几百字的向量成了"整节的混合语义",
与"起投多少"这种具体小问题相似度天然偏低(实测该问句向量 top1 仅 0.6291,够不到 0.75
硬门槛,只能靠与次优的差值勉强通过)。
改动三处:
1. 切分:Markdown 表格的每一行额外生成一个**自解释**的小块("南方季季盈90天:起投金额
1万元"),挂在父块 doc_id 下(PROD-007-04),父块照旧保留。知识块 160 → 631。
效果:该问句的命中分从 0.6291 升到 0.869,命中的正是"起投金额"那一行。
2. 检索:命中行级子块时把它的整节父块一并带回(分数按 0.9 折算),供调用方按问句选粒度。
整节块保底占最后一个名额,且不参与 top1/top2 判定——实测它挤到第 2 位会把 gap 从
0.090 压到 0.076,几乎跌破 0.07 的转人工门槛。
3. 客服:命中的是行级子块时,看问句与子块标签是否真的对得上——「起投多少」对「起投金额」
对得上,用那一行;「介绍一下」对不上,换成整节。
过程中两次判据写错并已修正(都固化进了测试):用"含连字符"认子块时,整节块自己的编号
PROD-901 被误判成子块;用"不含两位数字后缀"认整节块时,FAQ 块全被误判成整节块排到后面,
把正确答案挤出 top1、害得「基金赎回几天到账」转人工。
验证:起投/管理费等字段问法给出聚焦的单行答案;"介绍一下"给出整节;FAQ 与政策问法不受
影响(换话题、指代追问等此前修好的场景复测通过);
ruff / mypy(113 文件) / 468 unit+contract / 29 integration 全绿。
|
2026-09-10 22:29:23 +08:00 |
|
lzf_0626
|
07a922fa36
|
feat(tools): 新增本地客服控制台(可聊天的调试前端)
为什么做成独立进程而不是给底座加接口:底座目前没有登录接口(按计划推迟)。
任何让浏览器直接拿到令牌的做法——无论是一个 dev token 端点还是把私钥下发前端——
都等于把"任意身份"开放给任何能访问服务的人。控制台把令牌签发与调用全部留在
服务端进程内(私钥不出进程),底座代码零改动、也没有新增任何后门路由。
- GET /:返回聊天页;POST /api/chat:受理 agent-run 并驱动本进程执行
- 页面展示识别出的意图与是否引导人工,便于观察路由结果
- 同一 session_id 连续对话,用于验证短期记忆(多轮指代)生效
|
2026-09-10 22:06:43 +08:00 |
|
lzf_0626
|
89f889b350
|
feat: 图投影对账(数据层)并补运维工具,第 2 步收尾
与既有 `ProjectionReconciliationService` 的分工(两者互补,不是重复实现):
· 那个服务做**事件层**对账:哪些投影事件还没投递、需要重放;
· 本次新增的是**数据层**对账:投递完成之后,图里的内容与权威画像是否一致
(投影漏投、记忆失效后未清理、图库故障都会造成漂移)。
实现 `ProfileGraphProjectionService.reconcile_customer`:
· 从 user_facts 与持仓算出"应有的边",从图中读出"实际的边",求差集得到 missing
(画像有、图里没有)与 orphaned(图里有、画像已无);
· 图是投影、MySQL 是唯一真相,因此差异一律**以画像为准**:missing 补写、orphaned 删除,
而不是反过来去改画像;
· `repair=True` 时修复,并**修复后重新核对**再回报——不凭"操作没报错"就宣布修好了;
· 删除边前对关系名做白名单校验:关系名读自图中既有边,属外部数据,
必须过白名单才允许拼进 Cypher。
新增 tools/reconcile_graph.py:支持单客户与 `--all`、可选 `--repair`,退出码可直接用于巡检。
顺带修掉一处日志噪音:读边时原用 `coalesce(t.tag_key, t.product_code, ...)`,会引用当前
图中尚不存在的属性名,Neo4j 每次执行都抛 UnknownPropertyKeyWarning,把日志刷成噪音。
改用 `properties(t)` 后在应用侧按键取值,功能不变、日志干净。
实测(人为制造漂移再修复):
· 初始对账 一致=True、应有 2 条 / 实际 2 条;
· 删掉图中的 HAS_GOAL 边后 一致=False,缺失被准确报出;
· repair=True → 已修复=True、一致=True、缺失为空;
· 复验 一致=True,图中恢复 HAS_GOAL 与 PREFERS 两条关系;
· tools/reconcile_graph.py 单客户与 --all 均 EXIT=0,输出无警告;
· ruff 通过、mypy 112 文件无错。
|
2026-09-10 21:55:03 +08:00 |
|
lzf_0626
|
962a0a116f
|
feat: 记忆→画像打通(事实提升 + 画像组装 + 版本快照)
补齐"记忆系统为画像服务"的断链,按 docs/23 的分层设计实现后三层。
1. 新增 app/model/profile.py:user_facts 与 profile_snapshots 的 ORM 映射。此前这两张表
只有结构、没有 Model,实际没有任何代码在用。两处表结构特例在 docstring 里显式标注,
避免后续有人按直觉写入踩坑:
· user_facts.id 无 auto_increment,主键必须由应用提供(本实现用微秒时间戳,单调递增);
· profile_snapshots.current_customer_id 是生成列(IF(is_current=1, customer_id, NULL)),
故意不映射——映射了反而会在写入时与之冲突。
2. 新增 app/service/profile_assembly_service.py,三段职责:
· 事实提升(中期→长期):evidence_count ≥ 2 或 confidence ≥ 0.90 才从 memory_unit
提炼进 user_facts —— 这条门槛就是"客户随口一说不能变成画像结论"的落地方式;
· 画像组装(长期→画像):按白名单映射进 fin_customer_profile,未列入白名单的事实
(如 profile:family)只进 user_facts,保证画像的信噪比;
· 版本留痕:每次重建写一条 profile_snapshots,generation_basis 逐字段记录来源,
用于回答"当时凭什么这么判断"。
3. 新增 tools/rebuild_profile.py:手工触发入口(单客户或 --all)。画像暂无自动触发,
这是目前唯一的重建方式,也便于排查"画像为什么没更新"。
红线由代码保证而非约定:investor_type 只从 fin_risk_assessment 最新一条读取,实现中
不存在任何记忆路径能写它。实测——客户 9001 问卷为 C2、对话自述"稳健型",重建后
investor_type 仍为 C2,自述信息进入 risk_tags 并标注"自述:"前缀。三方不一致保持可见,
但等级判定只认问卷,客户无法靠对话改变自己的可购范围。
另一处由实测修正的设计:fin_customer_profile 的 trade_account/real_name/total_asset/
behavior_score 均为 NOT NULL,说明画像行由开户流程创建(也印证了"注册时填问卷"是开户
前置条件)。原先"首次重建时创建画像行"的做法是错的——会写出一条假的开户记录,而画像
恰恰是风控要读的数据。已改为只更新已存在的画像,未开户时返回 reason=profile_row_not_opened
并如实报告,而不是静默成功。
同时新增 docs/23-记忆分层与画像设计.md:短期/中期/长期/画像四层各自存在哪里、谁写、
提升门槛、是否进画像,以及三条路径(问卷/行为/对话)在画像层汇合的设计。
验证:ruff 通过、mypy 109 文件无错;tools/rebuild_profile.py 对客户 9001 连续两次重建
产生 version=1/2 两条快照且 is_current 正确轮转(旧版本置 0)。
|
2026-09-10 21:36:23 +08:00 |
|
zhangshy
|
a94d5c754d
|
feat: 迁移奶龙风控业务模块与演示文档
|
2026-09-10 21:03:44 +08:00 |
|
lzf_0626
|
20a3a2f249
|
feat: 客服闲聊提示词发布为可配置版本(提示词接入发布配置闭环)
业务方选定提示词走发布配置而不是写死在代码里:改话术要经过审核并留痕,符合金融场景
对口径变更的要求。Agent 侧读取上一批已实现(runtime_config_service.load_active_prompt),
本次补上发布侧,形成闭环。
脚本处理两条路径,实测第一条被拒、自动走了第二条:
1. 直接挂到当前生效版本 → 实测 409「只能修改草稿发布版本」,生效版本不可追加;
2. 新建发布版本,先**原样继承现有全部配置项**再追加提示词。原因:config_release 是
整版本替换语义,不继承就会把其他 Agent 的工具白名单清空(发布客服白名单时已踩过
一次这个坑,这次直接带上了继承逻辑)。
结果:新发布版本 174 生效,含 4 条 agent_tools 白名单 + 1 条 prompt_template_version
(prompt_code=customer_service_chitchat、task_type=chat、agent_type=customer_service)。
验证:load_active_prompt 能读到该提示词(release_id=174、version=1、checksum 已生成);
闲聊功能正常(意图 chitchat 置信 0.95,回答带免责声明)。
顺带记录一处错误码语义问题(本次不改):对「生效版本不可追加」这种资源状态冲突,
服务端返回的错误码是 RUN_NOT_CANCELLABLE,与场景不符。原因是 docs/05 §3.6 的码表里
没有表示「资源状态不允许该操作」的码,于是被复用了语义最近的运行类错误码。
建议后续在码表里补一个状态类错误码,而不是继续复用无关的码。
|
2026-09-10 20:41:09 +08:00 |
|
lzf_0626
|
1fa5fc7d03
|
refactor: 客服回答正文只保留固定免责声明
业务方要求客户侧只看到一句固定话术(不构成投资建议),因此从回答正文移除:
1. 中置信的「(以上信息可能不完整,具体以产品说明书与公司制度为准)」提示;
2. 「(依据:…)」出处行——原先它显示的是知识块标题,FAQ 的标题就是问题本身,
展示为「(依据:公司什么时候成立的?)」并没有可读价值。
可追溯性不受影响:本次命中哪个知识块仍由审计(agent.tool_executed 的工具调用记录)
与消息表留痕,source_references(tool 类型)也照常返回,只是不再面向客户展示。
取舍已在代码注释中标明:中置信回答此后不再向客户标注不确定性。
若将来要把出处展示给客户,应当走 source_references 的 knowledge 类型
(前提是让 ToolExecutor 把工具返回的 doc_id 登记为本次可引用来源),
而不是继续往正文里拼字符串。
同时移除因此不再使用的 INCOMPLETE_NOTICE 常量、_source_note 方法,
以及提问工具里那句"正文没有依据行"的提示。
验证:ruff 通过、mypy 107 文件无错;customer_service_check 9/9 通过;
ask_customer_service 实测回答正文为「答案 + 免责声明」两行。
|
2026-09-10 20:33:13 +08:00 |
|
lzf_0626
|
66595ce080
|
fix: 客服置信判定改为「绝对阈值 + 相对间隙」混合判定
问题(业务方实测发现):问「我们公司叫什么名字」被引导人工,但公司名称就在知识库里
——FAQ-0001「公司全称是什么?」与 COMP-001 都稳定命中 top1。所以根因不是检索不准,
而是判定规则不完整:客服方案 §2.3 要求的是「绝对阈值 AND(相对间隙 OR 分布优势)」,
实现里只做了绝对阈值 0.60。
实测校准(qwen3.7-text-embedding-flash,COSINE):
库内问法 top1 top1-top2
我们公司叫什么名字 0.592 0.100
你们公司名称是什么 0.579 0.090
公司全称是什么 0.671 0.098
你们公司总部在哪 0.764 0.245
南方科技的全称 0.855 —
库外 / 越界 top1 top1-top2
你们公司什么时候上市 0.500 0.046
推荐明天肯定涨的基金 0.488 0.023
我要投诉 0.492 0.025
今天天气怎么样 0.416 0.035
量子计算机退相干 0.416 0.044
两条结论:一是同为正确命中,口语问法的相似度天然偏低(0.592 vs 0.855),单用绝对阈值
必然误判;二是库内命中的 top1 领先幅度(≥0.09)显著大于库外(≤0.046),间隙是有效判别信号。
新规则:≥0.75 直接答(不再要求间隙);≥0.55 且间隙 ≥0.07 则回答并附「信息可能不完整」
提示;其余一律引导客户致电人工客服。两侧余量:库内最低 0.579、库外最高 0.500。
验证:ruff 通过、mypy 107 文件无错;ask_customer_service 对「我们公司叫什么名字」
正确返回南方科技有限公司;customer_service_check 由 8 项扩为 9 项,全部通过,
其中「推荐明天肯定涨的基金 / 我要投诉 / 量子计算机退相干」三条必须引导人工的用例
未被放宽后的阈值误答。
|
2026-09-10 20:29:12 +08:00 |
|
lzf_0626
|
a7fd02ec44
|
feat: 新增客服 Agent 手工提问工具 ask_customer_service.py
批量验收脚本(customer_service_check.py)只覆盖 8 个固定用例,手工试自己的问题时
需要自己签发 JWT、提交 run、驱动执行、解析结果,门槛太高。本工具把这条链路封成一条命令:
python tools/ask_customer_service.py "基金赎回到账要多久" # 单次提问
python tools/ask_customer_service.py # 交互模式,同会话连续追问
python tools/ask_customer_service.py --user 9003 "问题" # 换身份提问
输出包含:运行状态、识别意图与置信度、处置判定(直接回答 / 引导人工客服)、回答正文、
来源引用,并在"给了答案但正文没有依据"时给出人工核对提示。
注意:脚本自己驱动这一条 run 执行(WorkerRuntime().execute),运行前需停掉常驻 Worker,
否则常驻 Worker 会从共享队列抢走任务,脚本将等不到结果。
|
2026-09-10 20:24:33 +08:00 |
|
lzf_0626
|
13bab7c3d0
|
feat: 客服 Agent 端到端跑通(知识直返 + 答不了引导人工客服)
按业务方确定的取向实现:金融场景确定性优先,能溯源到公司资料的才答,答不了就
引导客户拨打客服热线,绝不用模型猜答案。端到端验收 8/8 通过。
新增:
- app/service/knowledge_search_service.py:知识检索。未复用记忆的 VectorMemoryAdapter
是因为它只返回 (memory_uuid, score),会丢掉知识块的标题与正文,而客服回答必须能把
原文与出处一起交付。检索失败一律返回 degraded 而不抛异常,由 Agent 走兜底。
- app/service/knowledge_tool.py + app/core/knowledge_contracts.py:只读工具 search_knowledge。
走 ToolExecutor 而不是让 Agent 直接持有检索服务,是为了让白名单、权限、审计、超时
都归基座统一管理;工具只读也符合 ToolRegistry 的硬约束。复用既有权限码
knowledge:reference:read(customer 角色已具备),不新增权限点。
- app/service/agent/implementations/customer_service.py:Agent 本体,刻意保持薄——
意图分发 + 四条出口(faq/产品/政策直返、闲聊走模型、其余与异常引导人工)。
直接返回知识原文而不经模型改写,答案的字面内容全部来自公司已发布资料。
- tools/publish_customer_service_config.py:发布意图工具白名单。
- tools/customer_service_check.py:端到端验收(8 个用例,含越界请求与知识库外问题)。
装配:
- bootstrap 新增 get_knowledge_search_service 工厂,注册 search_knowledge 工具与
customer_service Agent。
- runtime_config_service 新增 load_active_prompt:提示词绑定 release_id,按当前生效
版本读取,未发布时回落代码默认值。闲聊话术因此可审核、可回滚,不必改代码发版。
过程中发现并处理的三个问题:
1. 自造 source_references 被基座合规闸门拒绝。governance.review_output 只接受
「本次召回的记忆」与「本次成功调用的工具」两类引用(用于防止伪造来源),
knowledge 类型会被判非法并使整个 run 失败。处理方式是**不放开那道校验**,
而把知识出处(文件标题与内部编号)写进正文,source_references 交给基座自动附加。
2. 发布配置是整版本替换语义:新版本会清空旧版本的全部配置项。若只发客服白名单,
示例 Agent 的 fund_query_demo:fund_quote 会被静默清空。故发布脚本先读取当前生效
版本的全部配置项并原样继承,再追加新增项。
3. 验收脚本自身两处自伤:打印 emoji 触发 GBK UnicodeEncodeError、以及读错结果字段
(RunQueryService 返回的答案键是 content 不是 text)。
已知缺口(未修,已记录):
- CoreResult.transfer_required 未持久化:conversation_message 不存该标记,
API 读不到"本次是否引导了人工"。当前靠正文里的固定话术判断。
- 知识块引用(source_type=knowledge)尚未启用,需先让 ToolExecutor 把工具返回的
doc_id 登记为本次可引用来源。
验证:ruff 通过、mypy 107 文件无错、unit+contract 447 passed;
tools/customer_service_check.py 8/8 通过(含越界请求、投诉、知识库外问题三类
必须引导人工的场景,以及 7 个零容忍负面词零命中)。
|
2026-09-10 20:22:42 +08:00 |
|
lzf_0626
|
d2aff7c129
|
feat: 建客服知识库(Milvus 三集合)并修复模型端点筛选缺陷
一、知识库建设
- 新增 tools/build_knowledge_chunks.py:把 knowledge/ 下文档切成可检索知识块。
采用「叶子标题」策略(其后没有更深标题的标题即切分点),同时覆盖三种真实结构:
带子条款的按子条款切、无子条款的条款单独成块、无小节的章整章成块。
第一版按固定标题级别切是失败的——适当性指南的条款是 ### 而没有 ####,产品手册的
### 1.1 又不匹配「第X条」,两条规则互相打架,导致 4 个文件一块都没切出来。
- 新增 tools/load_knowledge_milvus.py:向量化并写入 Milvus,用 upsert 保证幂等。
schema 按方案 §4.2 统一字段,另加 chapter/section/source_file/doc_no/visibility 五个
检索与合规必需字段;索引 IVF_FLAT + COSINE + nlist=128;向量输入取「标题+正文」,
标题含条款号与章节名,是比正文更干净的检索信号。
- 知识内容按业务范围裁剪:反洗钱合规操作手册不入客服知识库(业务只做公募基金、
不涉及资金划付,且该手册标注内部机密、禁止向客户透露可疑交易信息),留给后续风控;
高净值客户服务规范只保留「客户分层标准」与「各层级专属权益」两章,
家族信托、资产配置流程、客户经理考核、隐私应急预案等内部管理章节不入库。
- 入库现状:fin_faq_collection 61 块、fin_product_collection 26 块、
fin_policy_collection 73 块,合计 160 块。检索自检 5/6——未命中的一条分数 0.660
落在中置信区间,按三档兜底策略本应提示信息可能不完整,属于预期行为。
二、embedding 端点
- 新增 tools/configure_embedding_endpoint.py:走管理 API(draft→approved→active)
配置并激活 qwen-embedding 端点,而不是直接写库。理由是状态机与审计都要留痕,
且 DatabaseModelGateway 只认 status='active',手工写错状态会报成与病因无关的
「模型端点未注册或未激活」。脚本先查 endpoint_code 是否已存在,幂等可重跑。
三、修复模型端点筛选缺陷(app/service/model_gateway.py)
- 原 DatabaseModelEndpointResolver 忽略 agent_type 与 task_type、直接返回全部 active
端点,而 ModelDispatchService 只按顺序尝试前 max_attempts(默认 2)个。两者叠加使
「能否选到支持该任务的端点」取决于端点表顺序:实测每次 embedding 都先拿文本生成
端点失败一次再落到向量端点(0.61s,修复后 0.42s)。
- 新增 TASK_CAPABILITY 显式映射后按能力筛选。用映射而不是同名筛选是必需的:
memory_extraction 并不是任何端点的能力名(deepseek 声明的是 text_generation 等),
按同名筛会得到空集、把记忆抽取打成失败关闭——这是本次修复最容易引入的回归。
- 保守兜底:未映射的 task_type、以及没有任何端点声明该能力时,都退回全部端点,
让配置缺口表现为调用失败,而不是让上层收到「解析为空」这种与病因无关的报错。
- 验证结果:embedding→[qwen-embedding]、intent_classification→[deepseek-flash]、
memory_extraction→[deepseek-flash]、未映射 task_type→全部;ruff 通过、
mypy 103 文件无错、unit+contract 447 passed。
四、需求文档提取物
- 新增 _flows/:三份流程文档(智能客服 Agent 专项设计方案、投资顾问流程、基金运营流程)
的纯文本提取,供开发期对照。原始 .docx/.html 保留在业务方目录侧。
说明:本次仅本地提交,未推送远程仓库。knowledge/ 内含公司内部制度与产品资料,
是否入远程库待确认。
|
2026-09-10 20:15:09 +08:00 |
|
lzf_0626
|
2c5ef35d18
|
chore: 删除退役的旧 JWT 密钥,同步过时引用并记录登录接口决策
1. 删除 config/jwt/jwt-private.pem 与 jwt-public.pem(旧密钥,配置已不再指向)。
删除后复跑全量门禁以确认没有残留依赖:ruff 通过、unit+contract 447 passed、
acceptance_check --production 7 PASS、demo_agent_e2e 9/9 PASS。
2. 修掉 4 处引用旧密钥路径的地方(它们会在删除密钥后直接失败或误导接入方):
- tests/unit/core/test_security.py 的三处硬编码路径改为单一常量 DEV_KEY_DIR,
否则删除旧密钥后该测试会因读不到文件而失败;
- tools/demo_agent_e2e.py 的 docstring 前置条件;
- docs/09 的配置示例;docs/19 的手工自签说明(改为指向配置项与
tools/generate_jwt_keys.py)。
3. docs/21 记录旧密钥已删除,并注明删除后已复跑验证无残留引用。
4. docs/19 未解决项新增第 5 条:无登录接口属于**有意识的推迟**(等业务 Agent 开发阶段
结束后再补),写明补的时候只需动签发侧、验签侧与身份解析侧都不需要改,
并附上当前私钥边界的实测结论(无法伪造不存在的用户、无法使用已禁用账号)。
|
2026-09-10 18:18:00 +08:00 |
|
lzf_0626
|
c32d3dbd06
|
chore: 开发专用 JWT 密钥、密钥生成脚本与轮换文档
背景:此前全环境共用一把 JWT 密钥(config/jwt/jwt-private.pem)。它相当于
"能冒充 9001/9002/9003 的万能钥匙"(实测边界:签名有效 + 用户存在且启用才通过,
伪造新用户与使用禁用账号都会被拒)。为避免同一把密钥将来又变成生产密钥,
本次引入开发专用密钥,并把签发侧收敛到配置。
改动:
1. 新增 tools/generate_jwt_keys.py:可复现地生成 RS256 密钥对(PKCS#8 / SPKI),
打印公钥 SHA-256 指纹便于核对服务端加载的是否同一把;密钥已存在时默认拒绝
覆盖,避免误操作导致所有已签发令牌立即失效。
2. 生成开发专用密钥到 config/jwt/dev/(该目录整体已被 .gitignore 忽略,不入库)。
3. 三个工具脚本不再硬编码私钥路径,改为读配置:acceptance_check 与 demo_agent_e2e
走 get_settings().jwt_private_key_path,smoke_check 因刻意不依赖 app 包而读
JWT_PRIVATE_KEY_PATH 环境变量。今后轮换密钥只需改 .env 一处。
4. .env、.env.example 与 Settings 默认值统一指向 config/jwt/dev/。
5. 新增 docs/21-JWT密钥管理与轮换.md:密钥分工(服务端只读公钥,
JWT_PRIVATE_KEY_PATH 在 app/ 中无任何读取点,故生产机可只挂公钥)、
克隆后必须自行生成、多人共用一个服务时必须共用同一把私钥、
轮换的影响面与生产部署要点、安全红线。
6. 记录一处易被忽略的问题:生产环境的 JWT_ISSUER / JWT_AUDIENCE 也应与开发不同,
否则开发环境签发的令牌在生产上依然有效——这比换密钥更容易漏。
说明:本次提交不含任何密钥文件(.env 与 config/jwt/ 均在 .gitignore 中)。
旧密钥 config/jwt/jwt-private.pem 已退役但保留未删,配置不再引用它,
用它签发的令牌会被拒绝。
验证:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、integration 29 passed、
acceptance_check --production 7 PASS、demo_agent_e2e 9/9 PASS——均使用新密钥完成
签发与验签。
|
2026-09-10 18:14:03 +08:00 |
|
lzf_0626
|
6516ccb385
|
feat: 第二版——接口契约对齐 docs/05,修复静默故障与数据库基线
相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。
一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
approve→reviews(需 body decision)、activate→activations、
rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
{data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
不再返回 FastAPI 默认的 {"detail": ...}。
二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。
三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
.env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。
四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。
五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。
验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
|
2026-09-10 15:55:54 +08:00 |
|
Codex
|
b1497fd2c6
|
chore: initialize project repository
|
2026-09-09 21:55:37 +08:00 |
|