5.6 系统提示定制
本节你会学到
- 指令由 15 个块拼成,每个块落在哪一条消息上
- 你真正能定制的 3 个块:AGENTAO.md、技能、自定义 Tool description
- 其余由运行时注入、不该被覆盖
Agent 的指令是每轮 chat() 动态重建的,不是一个静态字符串,而且分两条消息送达:一条稳定的 system 消息,以及一条按请求拼装、从不落盘的易变 user 尾消息。本节讲这些块的来源、哪些可以被你定制、哪些不建议碰。
系统提示的构成
指令由两条消息承载,不是一条。system 消息是稳定的那一半;易变的那一半挂在一条尾部 user 消息上,每个请求重建一次,从不写进对话记录。
┌──────────────────────────────────────────────┐
│ system 消息 —— 整个会话内保持稳定 │
│ │
│ 1. 项目说明 (AGENTAO.md) │ ← 你可以写
│ 2. 身份 / 基础能力描述 │ ← 固定
│ 3. 可靠性原则 │ ← 固定
│ 4. 任务分级 │ ← 固定
│ 5. 执行协议 │ ← 固定
│ 6. 完成标准 │ ← 固定
│ 7. 不可信输入边界 │ ← 固定
│ 8. 操作规范 │ ← 固定
│ 9. 可用子 Agent 列表 │ ← 固定
│ 10. 可用技能清单 │ ← 随启用集合变
│ 11. <memory-stable> 稳定记忆 │ ← 慢变
│ === 可缓存前缀到此结束 === │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ……对话历史…… │
│ 本轮的 user 消息(会落盘): │
│ <system-reminder> │ ← 每轮变
│ Current Date/Time: 2026-04-16 15:30:00 │
│ (Thursday) │
│ </system-reminder> │
│ <你传给 chat() 的文本> │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 易变尾消息 —— 一条 user 消息,仅属于本次请求 │
│ <system-reminder> │
│ 12. 激活技能全文 │ ← 激活即变
│ 13. 当前 Todo 列表 │ ← 动态,每请求
│ 14. <memory-context> 动态召回 │ ← 每轮变
│ 15. Plan 模式提示(条件) │ ← 条件
│ </system-reminder> │
└──────────────────────────────────────────────┘尾消息只属于这一次请求。 它为一次外发请求拼装,从不追加进 agent.messages,因此不会进入会话文件、replay 记录或压缩。新增易变内容时不要照抄日期提醒那套写法:那一条是落盘的,而一条落盘的尾消息会让每轮往对话里堆一份 todos 快照。
0.5.0 之前还有第十六个块:夹在操作准则和子 Agent 列表之间的一段 "Reasoning Requirement",只有构造参数 thinking_callback= 被设置时才渲染。没有别的东西能打开它,所以走 transport= 的宿主 —— 包括 agentao 自己的 CLI —— 从来没有过这一段,它随那个 kwarg 一起退役了。如果你想把它放进自己的 AGENTAO.md,原文在 docs/migration/0.4.x-to-0.5.0.zh.md §7。
块 12–15 是在 0.4.26 从 system 消息里搬出来的(docs/design/llm-api-adapters.zh.md §2.3 的阶段 0a)。它们正是"稳定"前缀其实并不稳定的原因:<memory-context> 按查询生成,每轮都变,而 messages[0] 是 provider 缓存前缀的头部,于是每轮都会废掉覆盖整段历史的缓存。一个值得知道的副作用:因为尾消息是按请求而不是按轮重建的,模型在某个工具迭代里写的 todo_write 下一个迭代就能看到。
技能清单(块 10)在 0.4.26 跟着一起搬了出去,0.4.27 又搬了回来。 它列出每一个已启用的技能,包括已激活的,所以激活一个技能不会改动 system 消息的任何字节;激活改变的是尾消息里的块 12,模型也正是从那里得知清单中哪些已经激活。清单只在启用集合变化时才变 —— 启用、禁用、安装、reload —— 而这些事件本来就会改写 activate_skill 工具的 skill_name 枚举,前缀在这些时刻原本就要重建。它排在 <memory-stable> 之前,因为它变得更少:一次 save_memory 只从块 11 起重建,清单仍然命中缓存 —— 这是就按 token 前缀匹配的缓存而言。启用下文的显式断点时,system 消息是一个整块缓存,一次 save_memory 会把它整块重写。
日期时间提醒
每一轮开始时,runner 把当前日期和时间放在用户文本前面(ChatLoopRunner.run):
<system-reminder>
Current Date/Time: 2026-04-16 15:30:00 (Thursday)
</system-reminder>
<你传给 chat() 的文本>格式是 %Y-%m-%d %H:%M:%S (%A)。取值是 datetime.now():宿主进程所在时区的本地时间,不带时区标记。如果你的用户在别的时区,在 AGENTAO.md 里写明。带图片的轮次,提醒放在文本部分里。
这条提醒刻意不放进两条指令消息中的任何一条:
- 不放进 system 消息:时间每秒都在变,放进前缀就会让每一轮的提示缓存失效。
- 不放进易变尾消息:尾消息从不落盘。日期时间随用户消息一起存下来,恢复会话或查看 replay 时,就能知道每一轮发生在什么时候。
tests/test_date_in_prompt.py 两头都锁定:提醒出现在用户消息里,并且既不在系统提示里,也不在易变尾部。
你能定制的 3 个注入点
1. AGENTAO.md — 项目级指令
放在 working_directory 根下,构造 Agent 时自动读取。没有 AGENTAO.md 时,会改读同一位置的 AGENTS.md(两者不会同时读)。不是你写的仓库可能自带 AGENTS.md;传 project_instructions="" 则两个文件都不读。
# 项目说明
## 技术栈
- Python 3.12 + FastAPI + Pydantic v2
- 前端:Next.js 14 App Router + shadcn/ui
## 代码规范
- 用 Ruff + black,行长 100
- async 函数不用 threading;计算密集用 `asyncio.to_thread`
- 新 endpoint 必须加 OpenAPI docstring
## 项目特殊约束
- 不要跨 tenant 查询(每个 endpoint 必须有 tenant_id 守卫)
- 数据库 schema 改动必须走 Alembic migration
- 不要直接用 `datetime.now()`,用 `app.utils.time.now()`(UTC + 租户时区感知)最佳实践:
- 内容应是硬约束和项目事实(不是操作手册)
- 长度控制在 500-1500 字——太长挤掉其他空间
- 进 git 让团队共享
- 用
##分段便于 LLM 吸收
文件怎么找、放在哪
- 只读一个文件,只看根目录。 Agentao 先找
<working_directory>/AGENTAO.md,再找<working_directory>/AGENTS.md(agents.md 约定)。只读第一个存在的文件,两者不会合并。子目录和上级目录都不查找。在 CLI 里,working directory 就是你启动agentao的目录。 - 构造时读一次。
/clear和/new不会重读。改了文件要生效,需要重启进程或新建一个Agentao。 - 按 UTF-8 读取。 开头的 YAML frontmatter 会被去掉(从别的工具搬过来的文件常带这一块)。空文件不注入任何内容。
- 不会悄悄换文件。
AGENTAO.md存在但读不了(权限、编码问题)时,Agentao 记一条 warning,然后在没有项目指令的情况下启动,不会退而去读AGENTS.md。 - 宿主覆盖。 传入的
project_instructions=只要是字符串,就原样使用,不读任何文件;传""等于关掉这个功能。 - 子 agent 看不到它。 子 agent 的项目指令块是它自己定义里的
system_instructions。
文件内容位于稳定系统提示的最前面,外框如下:
=== Project Instructions ===
<AGENTAO.md 的内容>
=== Agent Instructions ===
<Agentao 内建的各个段落:identity、reliability……>它是缓存前缀的第一块,所以改动它会让整段 prompt cache 失效。查找逻辑:agentao/prompts/helpers.py(load_project_instructions)。外框:agentao/prompts/builder.py。测试:tests/test_agentao_md.py。
指令没有生效时
agentao.log 会记录加载结果:
Loaded project instructions from /path/to/project/AGENTAO.md
Could not load AGENTAO.md: <error>如果去掉了 frontmatter,第一行末尾会多出 (ignored leading YAML frontmatter)。按下面的顺序排查:
- 日志里一行都没有。 没找到文件。确认它在 working directory 根下而不是子目录里,文件名正好是
AGENTAO.md——大小写敏感的文件系统上大小写也要对。 - 出现
Could not load …。 修好文件权限;如果是解码错误,把文件另存为 UTF-8。PowerShell 5.1 用>和Out-File写出的是 UTF-16LE。 - 加载了,但不是你想要的文件。 两个文件都在时,
AGENTS.md会被忽略。 - 加载了,但没效果。 打印
agent._build_system_prompt()(见验证系统提示),找=== Project Instructions ===。找不到就检查宿主是否传了project_instructions=;文件改过的话,重启。
2. 技能 — 按需加载的长文档
需要写 > 1500 字的规范?拆成技能(5.2 节)。用户/LLM 触发激活时才注入全文。
3. 记忆 <memory-stable> — 用户级持久化
适合"跨项目、用户本人稳定不变"的事实:
# LLM 在对话中自动写
save_memory("user-profile", "Senior Python dev, prefers tabs, UTC+8 Shanghai")这会在所有后续会话里都注入稳定块。
你不能(也不建议)定制的
| 块 | 原因 |
|---|---|
| Agent 基础能力 | 决定 Agent 怎么用工具、怎么思考 |
| 可靠性 / 操作规范 | Agentao 核心质量保证 |
| 可用子 Agent / 技能清单 | 来自注册状态,不是静态文本 |
如果你想彻底改造 Agent 行为(比如去掉某些能力),目前没有公开 API。推荐做法:通过 AGENTAO.md 和技能覆盖/增强,而不是试图替换。
验证系统提示
# 构造后立即看
agent = Agentao(working_directory=Path.cwd())
print(agent._build_system_prompt()) # 稳定的 system 消息
print(agent._build_volatile_tail()) # 仅属于请求的尾消息(为空时返回 "")⚠️ 两者都是私有 API,不保证稳定。仅用于调试。_build_system_prompt() 只返回稳定的那一半 —— 要找 todos、已激活技能的正文或动态召回,它们在尾消息里。
生产里可以打印字符长度做监控:
sp = agent._build_system_prompt()
logger.info("system_prompt_chars", extra={"len": len(sp)})系统提示过大会:
- 占压有效 context
- 提高每轮成本
- 降低 cache 命中率(如果 cache prefix 之后有太多动态内容)
Prompt Cache 的实战技巧
system 消息在一个会话的各轮之间逐字节相同,所以厂商的 prompt cache 能复用它 —— 更要紧的是,能复用它后面的历史。
什么进了稳定前缀
整条 system 消息(块 1–11),以及对话历史本身。provider 能复用的前缀,止于自上次请求以来第一条发生变化的消息之前。
什么破坏 cache
- 两轮之间改
AGENTAO.md—— 它是块 1,一改全废。 - 一次落进
<memory-stable>(块 11)的save_memory。 - 启用、禁用、安装或 reload 一个技能(块 10,连同工具块里
activate_skill的枚举)。激活一个技能不算。 - 切换模型或端点(agentao 自己的 token 锚点也会在那里失效)。
- 不包括 todos、已激活技能的正文、召回和 plan 提示:0.4.26 起它们在仅属于请求的尾消息里,位于历史之后,改动它们只代价一条未缓存的尾消息。
代价是:尾消息每个请求都要未缓存地完整重发 —— 是每次工具迭代,不是每轮。没有激活技能时它是空的,或只有几十 tokens。大头是已激活技能的正文:在本仓(盘上 14 个技能)实测,system 消息约 5.2k tokens、尾消息为空;激活一个技能(doc-coauthoring)后,只要它保持激活,每个请求的尾消息约 4.1k tokens。用完的技能请取消激活,并在你自己的部署里实测 —— 这些是本地 token 估算,不是账单数字。
显式断点(opt-in)
如果你的端点在普通 Chat Completions 线路上认 Anthropic 风格的 cache_control,设 LLM_PROMPT_CACHE=anthropic(可选 LLM_PROMPT_CACHE_TTL=1h)。agentao 随后会在每个 agent 回合的请求上最多打三个断点 —— system 消息、最后一个工具定义、稳定历史的末尾 —— 并把第四个槽留给端点自己的自动缓存。
默认关闭,而且刻意不从 base URL 或模型名推断:agentao 核实的是 OpenAI SDK 会原样转发这个键,不是你的网关会认它。先验证你的端点,再打开。见 docs/reference/configuration.zh.md §2。
调试 cache 命中率
如果用 OpenAI:响应里有 usage.prompt_tokens_details.cached_tokens。理想情况是从第二轮起,除了最新的几条消息和尾消息,其余都命中缓存。
为不同业务配置不同 AGENTAO.md
多租户 / 多业务线时,每个 working_directory 可以有不同的 AGENTAO.md:
/data/tenants/acme-corp/
├── AGENTAO.md ← acme 的规范
└── .agentao/
/data/tenants/globex/
├── AGENTAO.md ← globex 的规范
└── .agentao/这是最干净的租户级定制方式——不需要代码分支,只靠目录布局。
动态生成 AGENTAO.md
有些信息是每个会话动态的(例如用户当前的订阅等级、语言偏好、所在地区)。做法:构造 Agent 前把 AGENTAO.md 写到会话专属目录:
def prepare_workdir(tenant, user) -> Path:
workdir = Path(f"/tmp/session-{user.id}")
workdir.mkdir(exist_ok=True)
(workdir / "AGENTAO.md").write_text(f"""
# User Context
- Tenant: {tenant.name} ({tenant.plan})
- User: {user.name}, role: {user.role}, locale: {user.locale}
- Today: {datetime.now().isoformat()}
- Current feature: {user.current_feature}
## Allowed actions
{format_allowed_actions(tenant.plan)}
""")
return workdir
agent = Agentao(working_directory=prepare_workdir(tenant, user))这让每个会话看到的系统提示就是为它量身定制的。
我要是真的想把整个系统提示换掉呢?
没有公开 API。但你可以继承 Agentao 并 override 私有方法:
from agentao import Agentao
class MyAgentao(Agentao):
def _build_system_prompt(self) -> str:
parent = super()._build_system_prompt()
# 在最前面加一段你的总纲
return "# Your company's top-level charter\n\n...\n\n" + parent
agent = MyAgentao(working_directory=Path.cwd())⚠️ 这依赖私有方法名,版本升级要重新测试。尽量用 AGENTAO.md + 技能组合替代。
⚠️ 常见陷阱
上线前先确认这几条
- ❌ AGENTAO.md 过长 —— 长篇前置说明稀释模型对真正用户消息的注意力
- ❌ 不同会话共享同一 AGENTAO.md —— 一个租户的规则跑到了另一个租户身上
- ❌ 把敏感信息写进 AGENTAO.md —— 会被发到每次 LLM 调用,且可能被记日志
下面每一条都附完整修法。
❌ AGENTAO.md 过长
2000+ 字的 AGENTAO.md 会吃掉太多 context。把"操作指南类"内容拆成技能,AGENTAO.md 只保留"硬约束+关键事实"。
❌ 不同会话共享同一 AGENTAO.md
多租户场景如果所有 Agent 都指向同一个 working_directory,他们会共享 AGENTAO.md——但你可能有想按租户定制的需求。按会话独立 working_directory 是唯一干净解。
❌ 把敏感信息写进 AGENTAO.md
AGENTAO.md 是项目文件,可能进 git、被 LLM 记忆、出现在日志里。不要放 API key、真实凭据、客户 PII。
第 5 部分到此完成。 你现在有了让 Agent 理解你业务的完整工具链:工具、技能、MCP、权限、记忆、系统提示。下一部分讲怎么在生产环境下安全地部署这一切。
TL;DR
- 指令每轮重建——不要假设它是个静态字符串可以缓存。
- 它分两条消息送达:稳定的 system 消息(块 1–11)与仅属于请求的易变尾消息(块 12–15,一条从不进入对话记录的
user消息)。 - 你拥有其中 3 个块:
AGENTAO.md(项目硬规则)、技能正文(激活后的知识)、自定义 Tool 的 description(什么时候/怎么调)。 - 其余(日期、工作目录、可用工具/技能目录、记忆召回、todos 等)是运行时注入的,不该被覆盖。
AGENTAO.md写得短而绝对("永远不要做 X"、"永远用 Y 格式")——长篇前置说明会稀释注意力。