system_prompt 治理 —— 前后对比预览(先审查,不改文件)#
目标:保留同质 fork(工具集一件不减)、不做角色感知渲染;只对 prompt/prompts.yml 的
system_prompt 做表述去重 + 精简,逐字同质不破。309 行 → ~230 行。
硬护栏(调研得出):tests/test_m0_base.py 断言 6 个 XML 标签必须在——
<role> <workflow> <tool_policy> <termination> <constraints> <user_long_term_preferences>。
治理只精简块内文字,不消灭标签骨架。改完 pytest tests/test_m0_base.py + 种子集 rubric 前后对照。
语义无损原则:🟡 标记的刀碰的是”针对性防线”(老 bad case 沉淀),只合并措辞、不弱化语义。
刀1 —— 两 protocol「执行方」段去重 🟡(改动最大,审查重点)#
改前#
<fork_protocol> 执行方(现 200-218,19 行)+ <target_protocol> 执行方(现 239-251,13 行),
两段大量重合:都讲”直接 item_search / 别再 planner / 找够用不找最优 / 按硬约束过滤 / 空召回不编造 /
别下钻别收尾 / 产出候选或未找到”。
改后#
抽公共「执行方通则」放 <fork_protocol> 执行方;<target_protocol> 执行方只留 3 点差异,引用通则。
作为执行方(你收到的就是一条这样的子目标时)——通则:
- demands 已含主流程拆好的结构化字段与品类常识,**直接据此 item_search**,不要再调 planner /
category_insight(系统会拦)。目标是「找够用」不是「找最优」:召回里有合适候选就立刻收敛返回,
不为「再找更好」反复换词重搜;仅当召回明显跑题才换一次词。单平台检索次数有硬上限。
- 收敛前据硬约束自己过一遍候选标题(任何语言的「塑料/plastic/プラスチック」都读得懂),
**标题明确违反**硬约束的剔掉;看不出违不违反的不要臆测排除(留给主流程 item_picker 兜底)。
- 空召回(total_recall=0)可松一档约束再搜**一次**;仍为 0 就如实返回「未找到」,**绝不编造商品**。
- **不要**再 fork,也**不要** price_compare / shipping_calc / item_picker / shopping_summary——
比价 / 精挑 / 收尾统一由主流程做。产出=收敛并按硬约束过滤后的候选集(或「未找到」的如实说明)。plaintext<target_protocol> 执行方改为:
作为执行方(收到单商品调查 demands 时)——遵守上面「执行方通则」,本场景三点差异:
- 定位时 item_search 必须**同时**传 target_name=商品名/型号原文 **和** expected_category=demands
里的品类(型号过滤掉型号不符的、品类过滤掉「型号对得上但其实是配件/耗材」的假阳性,缺一个都
可能误判「找到了」)。
- 库里没有 / 召回不相关:web_search 补公开信息(价格区间、评测口碑),标注来源「库内候选」还是
「网络检索」;两边都没有就返回「未找到可靠信息」,不编造。
- 你只看到自己这一件,**不比较不排序**(那是主流程合流后的事)。产出=这一件的调查结果 JSON。plaintext语义核对:双过滤定位✅ web_search 补+标来源✅ 两边没有不编造✅ 不比较✅ 不 fork 不下钻(通则覆盖)✅ 产出 JSON✅ 硬约束过滤✅ 空召回不编造✅。32 行 → ~15 行,省 ~11 行,无语义丢失。
刀3 —— item_search 双过滤说明去三留一 🟡(防线,审查重点)#
改前(三处讲同一件事)#
<tool_policy>item_search 条目 119-126:完整解释 target_name + expected_category 防配件假阳性<target_protocol>派发方 230:又提一次<target_protocol>执行方 240-244:又完整解释一次
改后#
<tool_policy>保留完整详解一处(权威版,不动)- 两 protocol 处改为一句引用:「定点定位务必同时传 target_name + expected_category,理由见
<tool_policy>item_search 条」——刀1 的 target 执行方差异点已含精简版,不再第三次展开 - 省 ~8 行。防线语义在 tool_policy 完整保留,不弱化。
刀6 —— remember_preference 参数详解精简 🟡(前置已确认,审查重点)#
前置(调研确认)#
app/tools/remember_preference.py:55-73 的 args docstring 已完整承载 canonical_key / polarity /
strength / keywords / keys_to_supersede 逐字段说明 + 三原则 Integration/Resolution 逻辑。
→ system_prompt 里 147-160 的字段详解属重复,可安全压缩。
改前(tool_policy 137-160,含三原则 + 7 字段逐条详解,约 24 行)#
改后(保留三原则判断 + 关键点,字段细节依赖工具 args;约 10 行)#
- remember_preference:把用户本轮明确表达的**跨会话仍成立的持久偏好**记进长期记忆。**非终结**,
识别到就调、不必等收尾。写入前按**记忆合并三原则**判断(三选一):
1. 保留(不写):只是对当前结果满意/附和(「这个不错」「就它了」)或本轮一次性约束
(「这次预算 300」)——一律不写,它们不是跨会话取向。
2. 新增(写、不传 keys_to_supersede):新的、与已有不冲突的持久取向。
3. 取代(写、带 keys_to_supersede):与已有偏好冲突/翻转(「改成/不要了/换成」)——写新的并列出
被取代的旧 key(从 `<user_long_term_preferences>` 查)。
参数格式(canonical_key = `polarity:category:英文标识`、dislike 必给 keywords 作黑名单、strength
仅对 dislike 有意义)详见工具入参说明;同一意思不管中英文都要生成相同 canonical_key。plaintext语义核对:三原则完整✅ canonical_key 归一✅ dislike 给 keywords✅ strength 语义✅ keys_to_supersede✅(细节转由工具 args 承载,不是删除)。省 ~8 行。 前置条件:确认工具 args description 覆盖等价信息后再删;否则只压措辞、保留字段点。
刀7 —— 空召回/不编造去重 🟡(P0 诚实红线,审查重点)#
改前(两处)#
<fork_protocol>执行方 207-208:子视角”空召回松一档再搜一次、仍空如实返回、不编造”<termination>空召回硬路径 285-294:主视角”各平台汇集全空→shopping_summary 如实告知、不拿 category_insight 编造/背书”
改后#
- 子视角:已并入刀1 的执行方通则(“空召回可松一档再搜一次,仍空如实返回,绝不编造”)
- 主视角:
<termination>权威版完整保留(这是 P0,是主 loop 收尾诚实的核心,一字不弱化) - 仅去掉子/主之间的措辞重叠,省 ~4 行。红线语义零弱化。
刀2 —— workflow × tool_policy 工具触发条件去重 🟢#
改前(同一触发条件两处讲)#
- price_compare:workflow 61「tasks 含 price_compare 时对候选调」 vs tool_policy 129「仅 tasks 含 price_compare 时调」
- shipping_calc:workflow 63-65 vs tool_policy 130-131
- category_insight 两场景:workflow 26-32 vs tool_policy 114-118
- item_search 跨平台:workflow 42-46 vs tool_policy 119
改后#
<tool_policy>留「工具能力 + 何时调」权威版(工具手册视角,不动)<workflow>留编排菜单逻辑(按 tasks 组合、跳过没要的、依赖顺序、fork 判断、收尾分界), 删掉与 tool_policy 逐字重复的”仅 tasks 含 X 时调”触发细则- 注意:workflow 条目里”对已汇集/定位的候选调”这类编排依赖语义要保留,只删纯触发条件重复
- 省 ~10-12 行(清单原估 15,诚实下调——两块视角不同,重叠只在触发条件)
刀4 —— 收尾分界线「要不要甩商品卡」去重 🟢#
- 改前:workflow 84-88 与 termination 270-271 逐点重复(有商品卡→shopping_summary,纯文字→chat_fallback)
- 改后:权威版留
<termination>;workflow 里压成一句”收尾规则(终结工具怎么选)见<termination>” - 省 ~4 行
刀5 —— 跨平台「一次 fork / 5 平台 / 别重复拓宽」去重 🟢#
- 改前:至少 5 处——workflow 42-46、workflow 关键纪律 96、tool_policy 119、fork_protocol 190-198、 constraints 304
- 改后:
<fork_protocol>派发方留权威版(含 5 平台清单、写法);<constraints>留一句红线 (“跨平台一律 parallel_dispatch 一次并行,禁止串行多次 item_search”);workflow / tool_policy 的 重复叮嘱删除或压成一句指向 fork_protocol - 省 ~6 行。 🟡 注意保留”汇集后不要对同品类再 fork 拓宽”这条防死循环语义(并进 fork_protocol)
刀8 —— 主动性纪律 vs ask_user 合并 🟢#
- 改前:workflow 主动性纪律 90-93 与 tool_policy ask_user 164-165 都讲”品类宽直接搜、别停下反问、 只硬信息缺失才澄清”
- 改后:合并到
<workflow>主动性纪律一处;tool_policy 的 ask_user 条只留工具能力一句 + 指向 - 省 ~3 行
刀9 —— 全局排比 / 括号补充 / “(见 xxx)” 精简 🟢#
- 逐句精简冗长括号注释与重复语气,不动任何语义
- 省 ~15-20 行
账目汇总#
| 刀 | 省行 | 安全度 |
|---|---|---|
| 1 两 protocol 执行方 | ~11 | 🟡 |
| 2 workflow×tool_policy | ~10-12 | 🟢 |
| 3 item_search 双过滤 | ~8 | 🟡 |
| 4 收尾分界线 | ~4 | 🟢 |
| 5 跨平台一次 fork | ~6 | 🟢 |
| 6 remember 参数 | ~8 | 🟡 |
| 7 空召回不编造 | ~4 | 🟡 |
| 8 主动性 vs ask_user | ~3 | 🟢 |
| 9 全局措辞 | ~15-20 | 🟢 |
| 合计 | ~69-76 行 | 309 → ~233-240 行;token ~6.3k → ~4.7k |
验证方式#
pytest tests/test_m0_base.py tests/test_main_agent.py(保 6 个 XML 标签 + 偏好注入)- 种子集 rubric 前后对照(
scripts/eval/run_rubric.py),确认无退化 - 🟡 四刀(1/3/6/7)逐条核对语义无损后再提交
执行实况(本节为落地后追记,上文预览稿写于 P_t / 记忆管家落地之前,行号已过时)#
上面的预览稿针对旧 309 行版本;实际执行时文件已因「会话级短期记忆 P_t + 记忆管家 curator」 涨到 399 行,多出
<user_recent_history>/<session_preferences>/<memory_note>注入块 与 curator prompt。故实际治理基于当前 399 行重新盘点,与预览稿有偏差,如实记录如下。
与预览稿的偏差#
- 刀6(remember_preference 参数精简)作废:
remember_preference已不在 system prompt 里 (偏好沉淀移交给会话结束后的记忆管家),此刀无对象。 - 新增重复 A(记忆归属):「沉淀是记忆管家的事、你不写长期库」原在
<role>/forget_preference注 /<memory_note>/<termination>讲了 4 遍 → 收敛到<memory_note>权威、其余压成引用。🟡 - 新增重复 B(定点隔离机制):「demands 无平台名→自动识别定点批次→web_search 门控互不拦」
原在
<workflow>+<target_protocol>各一遍 → 只留<target_protocol>。🟢 - 新增重复 C(evaluate 诚实前提):「未库内定位不拿品类基准背书」
<workflow>精简 + 引用<termination>空召回硬路径权威版。🟡
其余刀 1/2/3/4/5/7/8/9 仍成立并已执行。
实际账目#
| 项 | 结果 |
|---|---|
| system_prompt | 300 → 259 行(净 −41) |
| 全文 diff | −144 / +103(git diff --stat) |
| token | ~6.3k → ~5.0k |
| 护栏测试 | test_m0_base + test_main_agent 共 45 项全过 |
| 语义无损自查 | 16 条关键防线短语全部存活(≥1),P0 空召回硬路径一字未弱化 |
同质 fork 不破:工具集一件不减、无角色感知渲染、主/子共用同一份 prompt——只做块内 + 跨块去重。
工程加固轮(治理后追加:从垂直 Agent 工程实践审视)#
去重完成后,进一步从「垂直 Agent 工程实践」角度审视(非再精简——精简对延迟无益:实测 295s 里 96% 是 decode 解码,prefill 可忽略,继续切措辞是负 ROI)。识别并落地两处实缺:
① 补输出语言约定(高 ROI,已落地)#
全球电商多语言场景,但 prompt 原先一句都没约定回复语言,纯靠 LLM 默认跟随,中文用户 +
英文商品名 + 英文 web_search 结果混杂时回复语言易漂移。<constraints> 补一条:
「用用户提问所用的语言回复;商品原文名 / 型号 / 品牌保留原文不译,必要时括注中文。」
② P0 诚实红线显式标注(纯标注不改语义,已落地)#
<termination> 标了 P0,但 <constraints> 原为平铺红线、无优先级。加优先级总纲
「P0(诚实红线)> 硬约束 > 质量要求,冲突/信息不足时永远保 P0」,并把「不编造商品/价格/
运费/评分」提升为 【P0】、以指针引用 <termination> 空召回硬路径(不重复展开)。
③ cache 位置按 volatility 排序(分析后主动不做 → 后续以另一种方式落地,见文末追记)#
现状注入顺序 long_term(每用户) → recent_history(每用户) → session_preferences(每轮变),其后压
着 <memory_note>/<termination>/<constraints> ~70 行静态内容,隐式前缀缓存每轮在
session_preferences 处断开、这 70 行每轮重新 prefill。教科书做法是「最易变的挪到最末、静态块上提」。
当时不做的理由:(a) 与「硬约束放结尾」的位置敏感原则冲突;(b) 本项目 decode-bound,延迟收益≈0,
只省少量 input token;(c) 当前 cache_control 只打在对话历史(较旧区),system prompt 没有显式缓存
断点。
后续更新:没有按「挪到 system prompt 末尾」这个教科书方案做(会真撞上 (a) 的冲突),而是 发现
session_preferences(P_t)本质是「当轮状态」而非「系统级指令」,压根不该待在 system prompt 里——直接搬到当轮 human message 尾部,见文末「P_t 迁出 system prompt」一节,(a)(c) 两条 理由因此都不成立了;(b) 的判断仍然对(延迟收益≈0),这次做纯粹是为了让prior_turns的显式 cache_control 断点(M6 已实现)真正吃到跨轮命中,而不是每轮陪跑打空。
架构洞察(不改,存档备忘)#
同质 fork 令子 Agent 读整份 259 行(~150 行主流程编排它作为执行方用不上)是 token 税;但正因 完全同质、跨 fork 字节一致,DashScope 隐式前缀缓存可跨 5 个并行子 Agent 复用同一 system 前缀, 部分抵消。同质设计在缓存上是红利而非纯负担——「别给子 Agent 专用化裁剪」除架构一致性外还有 缓存复用的实利。
工程加固轮验证#
pytest tests/test_m0_base.py tests/test_main_agent.py 45 项全过;YAML 可解析、5 个必需标签齐、
回复语言 / 【P0】 均已入 system prompt。
P_t 迁出 system prompt(③ 的后续落地:不排序,搬家)#
背景#
上面 ③ 判断「排序」这个教科书方案会撞上「硬约束放结尾」的位置敏感原则,主动搁置。重新想这个
问题时发现:症结不是「session_preferences 排得不够靠后」,而是它根本不该出现在 system prompt
里——system prompt 是「本该跨轮稳定的指令」,P_t 是「这一轮才知道的会话状态」,两者混在一起,
P_t 每轮一变就把它前面本该稳定复用的整段(含长期偏好、<constraints> 等静态指令)一起拖下水。
system prompt 在一次请求里天然排在 messages 数组最前面——只要它自己不逐轮变,messages 里
prior_turns(回填的历史摘要)打的那个 cache_control 断点(M6 已实现)才有意义;只要 P_t 还嵌
在 system prompt 里,这个断点年年打、年年空。
改法#
prompt/prompts.yml的<session_preferences>块去掉{session_preferences}占位符,改成一段 纯静态说明(更名<session_constraints_policy>),告诉模型这类内容会包在<session_constraints>标签里跟着用户消息一起送达。get_system_prompt()去掉session_preferences形参——system prompt 现在只有「静态指令 + 长期 偏好 + 行为历史」,跟当轮/当次会话状态彻底解耦。main_agent.run_agent()新增_inject_session_constraints(query, pt):P_t 非空时包成<session_constraints>...</session_constraints>拼在本轮 query 前面,作为payload["messages"]里那条 human message 的内容;P_t 为空(会话刚开始)则不加标签,不产生占位噪音。
代价:P_t 现在离「当轮 query」只有一层标签之隔,模型看它的注意力位置变了——但从
app.memory.curator/item_picker 的机制性强制执行(硬 dislike 进 exclude、预算兜底)来看,P_t 本
来就不是「靠模型自己记住」的唯一保险,这层风险可控。
验证(真实 DashScope 调用,非单测断言结构对不对)#
同一 thread_id 连续跑两轮真实购物对话(deepseek-v4-flash),在 BaseChatModel.ainvoke 层读
LangChain 解析好的 usage_metadata.input_token_details.cache_read:
| input_tokens | cache_read | 命中率 | |
|---|---|---|---|
| 第一轮开局第一次调用 | 12428 | 12288 | 98.9% |
| 第二轮开局第一次调用(跨过一整轮,P_t 从空变非空) | 13065 | 12288 | 94.1% |
两轮 cache_read 绝对值相同(12288)——system prompt 静态前缀跨轮字节不变,prior_turns 断点终
于有稳定前缀可打。落盘核对 pt.json 确认第二轮开局时 P_t 确实非空(不是「碰巧没测出问题」)。
顺带对照跑了一版登录用户场景:第二轮 cache_read 掉到 5632(明显低于 12288),排查是
long_term_preferences(system prompt 中段,<constraints> 之前)跨轮可能因记忆管家写入新长期
偏好而变化,把它之后的静态段连累一起失效——这是另一个已知但尚未处理的 volatility 源,跟这次
P_t 的改动无关,暂不动(优先级低于本次修复,留作后续)。
结论#
③ 当初否决的是「排序」,不是「不做」;换成「搬家」之后,位置敏感原则不用碰、cache_control
显式断点也不用现在就打,(a)(c) 两条否决理由同时失效,改动量反而更小。