合并受阻
变更摘要
本 PR 为垂直领域发行版引入三个部署侧开关:一是新增 persona::resolve_persona,让配置的外部人设整体替换内置编码人设(未配置时返回内置人设,行为逐字节不变);二是让既有但从未被读取的 system_prompt 字段真正生效,并补充文件形式 system_prompt_file,通过 resolve_system_prompt_override 统一解析(inline 优先、相对路径按 config 目录解析、空值/读不到时回退并产出告警);三是新增 [tools] allow / deny 挂载名单,由 atomcode_coding::toolfilter::ToolFilter 实现精确名、前缀、group:* 族三种匹配,deny 在 allow 之后判定。两个开关在 CodingAgentConfig / CodingRuntimeConfig 中透传,并接到 TUI 与 daemon 两条运行路径及工具挂载点。
主要改动
- 新增人设整体替换入口: 在
crates/atomcode-coding/src/persona.rs新增resolve_persona(override_text, built_in),外部人设存在且非空时返回去空白后的文本并跳过内置人设闭包构造,否则调用闭包返回内置人设。 - 新增系统提示文件配置与解析:
ProviderConfig、ModelProfileConfig、ResolvedModelConfig新增system_prompt_file字段,并新增resolve_system_prompt_override及resolved_system_prompt方法,实现 inline 优先、文件按 config 目录解析、空/不可读时回退并回传告警。 - 新增工具挂载过滤器: 新文件
crates/atomcode-coding/src/toolfilter.rs定义ToolFilter,提供new、is_noop、keeps、retain/retained,支持精确名、尾随*前缀与group:coding/codeintel/atomgit/skills/subagent/mcp族匹配。 - 新增
[tools]白黑名单配置:crates/atomcode-config/src/config/mod.rs的ToolsConfig新增allow/deny两个默认空列表字段(skip_serializing_if为空),并在CodingRuntimeConfig构造处据此构建ToolFilter。 - 过滤器与覆盖人设落到挂载与装配点:
config.rs新增persona_override/tool_filter字段并在apply_provider_config(/model切换)重新解析人设;assemble.rs、parts.rs在基础名单、MCP 发布及register_extra_tool处应用tool_filter并用resolve_persona拼装;daemon/live_api.rs的chat_runtime_config同步接线。


变更摘要
本 PR 为垂直领域发行版引入三个部署侧开关,均以「未配置时行为逐字节不变」为约束。核心是让系统提示可被整体替换而非追加:persona.rs 新增 resolve_persona,配置了外部人设时整体替换内置编码人设,未配置时返回内置人设(内置人设以惰性闭包传入,配了外部人设时根本不构造)。同时让既有但从未被读取的 system_prompt 真正生效,并补充文件形式 system_prompt_file,经由 resolve_system_prompt_override / resolved_system_prompt 解析(inline 优先、相对路径按 config 目录解析、空值或读不到文件回退内置并返回警告)。此外新增 [tools] allow / deny 挂载名单过滤,由新文件 toolfilter.rs 的 ToolFilter 实现,作用于基础工具名单与各 MCP 发布点及运行时注入工具,未配置时挂载集合与现状一致。
主要改动
- 整体替换编码人设:
crates/atomcode-coding/src/persona.rs新增resolve_persona(override_text, built_in),override_text去空白后非空即返回该文本、否则调用内置闭包;assemble.rs与parts.rs的两处人设拼装改为经此函数,实现外部人设整体替换而非追加。 system_prompt生效并新增system_prompt_file:crates/atomcode-config/src/config/provider.rs在ProviderConfig、ModelProfileConfig、ResolvedModelConfig增加system_prompt_file字段,新增resolve_system_prompt_override及resolved_system_prompt;inline 优先、相对路径相对 config 目录解析、空文件或不可读时回退并返回警告字符串。[tools] allow/deny挂载过滤:crates/atomcode-config/src/config/mod.rs的ToolsConfig新增allow、deny;新文件crates/atomcode-coding/src/toolfilter.rs实现ToolFilter(keeps/retain/retained/is_noop),模式支持精确名、尾部*前缀与group:coding、group:codeintel、group:atomgit、group:skills、group:subagent、group:mcp族,deny在allow之后判定。- 过滤器落在多个挂载点:
crates/atomcode-coding/src/parts.rs在prepare的基础名单用tool_filter.retained过滤,并把tool_filter传入 MCP 初次就绪与单台服务器增量发布(先过滤再记录mcp_tool_names),register_extra_tool也先经tool_filter.keeps判定后才挂载;注册侧逻辑不变。 - 运行时接线与
/model重新解析:crates/atomcode-coding/src/config.rs的CodingAgentConfig/CodingRuntimeConfig新增persona_override与tool_filter字段并在构造与转换处赋值,apply_provider_config在切换模型时重新解析人设覆盖;crates/atomcode-daemon/src/live_api.rs的chat_runtime_config同步接入两条路径,reconcile_coding_persona增加对覆盖场景下前导系统消息的识别。


背景:我们在拿 AtomCode 做一个垂直领域发行版
我们在用 AtomCode 5.1.0 做一个证券投研方向的发行版:工作区里放的是持仓、
投资论点、因子定义和研报,不是代码;用户问的是"这只票基本面怎么样",不是"帮我改个 bug"。
底座能力(daemon HTTP/SSE、MCP、hook、skill)都很好用,唯一迈不过去的坎是系统提示。
问题:内置编码人设没有任何配置能替换
crates/atomcode-coding/src/persona.rs在进程内把系统提示拼出来。我们逐条试过现有的手段:AGENTS.md/.atomcode.md)## PRECEDENCE:背书## WORKFLOW:/## TOOLS:/## DOING TASKS:/## CODE REVIEW:/## GIT COMMITS:等段落,它们照样在提示里plan档每轮注入"给个方案然后停下来等批准",对非编码场景是负作用结果是模型每一轮都在两套互相矛盾的规则之间选边:系统提示让它 UNDERSTAND → SEARCH →
PLAN → EDIT → VERIFY,项目指令让它去调数据工具写研报。追加解决不了这个问题,
需要的是让那些规则「消失」。
另外:
system_prompt这个字段已经有了,但从来没被读到过ProviderConfig::system_prompt(crates/atomcode-config/src/config/provider.rs)和
ModelProfileConfig::system_prompt都在,序列化/反序列化都正常,但全树搜索下来没有任何代码把它送进 persona 的拼装路径。用户配了它,不会报错,也不会生效。
我们倾向认为这是遗留缺陷而不是有意为之。
第二个问题:工具也没法按部署裁
同一件事的另一半。
[permissions]能决定一次调用要不要弹窗,但工具照样出现在发给模型的
tools数组里 —— schema 的 token 照付,模型照样会去试,被拒之后再换一个工具试。一个证券投研部署用不到
list_symbols/trace_callers/blast_radius/atomgit_pr这些,可是没有任何配置能让它们不出现。内核里其实已经有正确的分层:
ToolRegistry::mount的注释写着「Unmounted tools never produce a ToolDef and are not resolvable during a turn →
zero effect on the agent」。缺的只是让部署方决定挂载名单的那个口子。
这个 PR 做了什么
三个部署侧开关,未配置时行为逐字节不变:
persona::resolve_persona(override, built_in)—— 配了外部人设就整体替换内置人设,没配就返回内置的。内置人设做成惰性闭包,配了外部人设时根本不去构造它。
system_prompt真正生效,并补一个文件形式system_prompt_file(整个文件的内容替换,相对路径按 config 目录解析,inline 优先)。
接到 TUI(
CodingRuntimeConfig)与 daemon(live_api::chat_runtime_config)两条路径上,/model切换(apply_provider_config)也重新解析 —— 否则换个模型会悄悄把内置编码人设换回来。[tools] allow/deny—— 挂载名单的白/黑名单(atomcode_coding::toolfilter)。模式写法三种:精确名
read_file、前缀mcp__screener__*、族group:coding/group:codeintel/group:atomgit/group:skills/group:subagent/group:mcp。deny在allow之后判,deny 赢。[models.my-domain-model] provider = "openai-compatible" model = "..." system_prompt_file = "personas/my-domain.md" # 不写这一行,行为与现在完全一致 [tools] deny = ["group:codeintel", "group:atomgit"] # 不写这一段,行为与现在完全一致过滤器落在四个挂载点上,少一个就是一句空话:基础工具名单(
prepare)、MCP 初次就绪发布、单台 MCP 连上时的增量发布、以及运行时注入的额外工具
(
register_extra_tool,/loop的schedule_wakeup走那条)。注册侧一个字不动 ——工具照样注册,只是不挂载,这正是上面那段内核注释描述的语义。
四个刻意的设计选择
deny是「不挂载」,不是「不许调」。 后者[permissions]已经做了。区别在于 token 与模型行为:没挂载的工具压根不进
tools数组,模型看不见也想不起来。tracing::warn!。用空系统提示启动一个 agent 比回退糟得多;但静默回退会让一个拼错的路径看起来像
"这个功能根本没生效"。
atomcode-config里没有 logger,按它既有的load_with_diagnostics惯例把诊断回传给调用方,由atomcode-coding/atomcode-daemon用各自的tracing打出来。system_prompt优先于system_prompt_file。 inline 是已有字段,保持它优先级最高,不改变任何既有配置的行为。
改动规模
19 个文件。其中 11 个只是给结构体字面量补一个
None字段(给 pub 结构体加字段就是这么回事),1 个是新文件
crates/atomcode-coding/src/toolfilter.rs。有逻辑的是:
persona.rs、assemble.rs、parts.rs、config/provider.rs、config/mod.rs、coding/config.rs、daemon/live_api.rs。新增 12 条单测,其中两条专门锁"未配置时行为不变":
#[test] fn no_override_keeps_the_built_in_persona_byte_for_byte() { let built_in = coding_persona("deepseek-v4-flash", false, false); for empty in [None, Some(""), Some(" \n ")] { assert_eq!(resolve_persona(empty, || coding_persona("deepseek-v4-flash", false, false)), built_in, "an unset / blank override must not change existing behavior"); } } #[test] fn unconfigured_keeps_every_tool() { let f = ToolFilter::default(); assert!(f.is_noop()); for name in ["read_file", "bash", "mcp__x__y", "list_symbols"] { assert!(f.keeps(name), "{name} must still mount when nothing is configured"); } }验证
按
.github/workflows/check.yml的四道门跑(Linux / rust stable 1.98.1 / Debian 12):cargo fmt --all -- --check(阻塞)cargo check --workspace --all-targets(阻塞)cargo clippy --workspace --all-targets(report-only)cargo test --workspace(report-only)那一条失败是
plugin::marketplace::tests::git_runs_rejects_present_but_failing_stub(「一个
--version能成功的二进制必须被当成 git」——它往 tempdir 里写一个 shell桩并执行它)。三条证据说明它与这个补丁无关:
它在
crates/atomcode-capabilities/里,而这个补丁一个字都没动那个 crate。这个测试套本身是 flaky 的。 两棵树各完整跑两遍
cargo test --workspace --no-fail-fast,四次的失败集合是这样:subagent::claude_code::tests::run_maps_is_error_to_agent_erroracp::engine::tests::shared_factory_builds_each_session_with_its_own_identity稳定的那 4 条两棵树完全相同(
atomcode-tuix的两条终端渲染、atomcode-daemon的两条内嵌 webui 资源,都是环境相关);除此之外每一次还会多冒出 1 条,四次是四条不同的、两棵树都有。
最开始那次
git_runs_rejects_present_but_failing_stub就是这一类—— 后面三次跑它都通过了。
补丁新增的单测在逐 crate 计数上对得上:
atomcode-codinglib 441 → 448(+7)、atomcode-configlib 322 → 327(+5),合计 12 条;其余 crate 的计数两棵树相同。另外:拿两个二进制跑同一个 stub 比过一次
「未配置时行为不变」这句话不该只靠读代码断言。用一个只记录请求的 stub provider,
同一道题、同一个工作区,分别用官方 5.1.0 二进制和打了补丁的二进制各跑一遍
headless:
system_prompt_file[tools] deny前两行的系统提示逐字节相同、工具清单相同。第四行摘掉的 13 个是
atomgit_*四个 + 代码智能八个 +task。一个给文档的提醒
整体替换会把内置人设末尾的
## ENVIRONMENT: Today's date: …一起替掉。用替换人设的部署需要自己注入日期(我们是用
UserPromptSubmithook 注入的,顺带修掉了一个小问题:内置那一行用的是进程所在时区的日期,我们的容器跑 UTC,
显示的日期比用户所在时区晚一天)。如果这个 PR 被接受,文档里值得提一句。
我们没有做的事
AGENTS.md的架构约束)。工具过滤放在
atomcode-coding(挂载名单的所有者),kernel 的ToolRegistry/MountedTools一行没改。group:*的族定义硬编成一张表:coding/codeintel/skills直接问各自的
*_tool_names(),所以上游往某个族里加工具时这里自动跟上。只有
atomgit例外 —— 那个模块在一个可选 Cargo feature 后面,按名字前缀atomgit_匹配,免得同一份配置在不同构建下含义不一样。提交自
agentpit-io/atomcode:feat/domain-persona(基点v5.1.0/72b538e8c)。补丁脚本与完整实测记录:https://gitcode.com/agentpit-io/huntercode-atomcode(
docs/fork-patch/、docs/开发文档/I2-性能优化报告.md)。