一个完整 Agent 的上下文,不应该把所有东西一股脑塞进 system prompt 。更合理的理解是:
Agent 每一轮调用 LLM 前,会动态“组装一次上下文”。
system prompt 只是其中最稳定、最高优先级的一层。
你可以把它想象成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Agent Runtime │ ├── 1. System / Developer Instructions │ ├── 你是谁 │ ├── 基本行为规则 │ ├── 安全规则 │ └── Agent 工作方式 │ ├── 2. Skills / Policies │ ├── 已启用 Skill 的说明 │ └── 当前任务命中的 Skill 内容 │ ├── 3. Tools │ ├── read_file │ ├── write_file │ ├── shell │ ├── MCP tools │ └── ... │ ├── 4. Memory / Retrieved Context │ ├── 用户长期记忆 │ ├── 项目规则 │ ├── RAG 检索结果 │ └── repo 信息 │ ├── 5. Conversation History │ ├── user │ ├── assistant │ ├── tool result │ └── 压缩后的历史 summary │ └── 6. Current User Message ↓ LLM
所以你之前脑子里的:
1 2 3 4 5 6 7 tools ↓ system prompt ↓ 历史消息 ↓ 当前问题
其实已经很接近了,只不过我们可以再精细一点。
1. 真正适合放 system prompt 的是什么? 通常是长期稳定、不怎么变化的规则 。
例如你的终端 Agent:
1 2 3 4 5 6 7 8 9 10 You are Nancode, a terminal coding agent. You help users inspect, modify, and debug software projects. Rules: - Inspect files before modifying them. - Avoid destructive operations unless necessary. - Never delete user files without explicit justification. - Prefer minimal changes. - After editing code, verify the result.
这些东西几乎每轮都需要,因此放 system 很合理。
换句话说:
1 2 system prompt ≈ Agent 的宪法 + 身份 + 总体工作原则
而不是:
1 2 system prompt = 所有上下文垃圾桶
哈哈哈,不然最后 system prompt 会胖成一只球。
这个特别重要。
假设有:
1 2 3 4 read_file write_file shell grep
现代 API 通常是:
1 2 3 4 5 6 7 8 9 10 11 12 13 client.responses.create( model="..." , instructions=system_prompt, tools=[ { "type" : "function" , "name" : "read_file" , ... }, ... ], input =messages, )
概念上是:
1 2 3 4 5 LLM request ├── instructions ├── tools ├── messages └── ...
而不是:
1 2 3 4 5 system: 你有这些工具: 1. read_file(...) 2. write_file(...) 3. shell(...)
虽然模型最终肯定还是会“看到”工具描述 ,但 API / serving 层会用专门的 tool schema 传进去。
所以在你自己的 Agent 里最好也分开:
1 2 3 4 class AgentContext : system_prompt: str tools: list [Tool] messages: list [Message]
不要做:
1 2 3 4 5 6 7 system_prompt = ( base_prompt + tool_prompt + skill_prompt + memory_prompt + everything_else )
那以后维护起来会哭出来。
3. Skill 又稍微特殊一点 Skill 可以分成两层。
例如:
1 2 3 4 5 6 7 skills/ ├── github/ │ └── SKILL.md ├── pdf/ │ └── SKILL.md └── refactor/ └── SKILL.md
你没必要每轮把所有 Skill 全文塞进去。
可以先只注册:
1 2 3 4 5 6 7 8 9 10 Available skills: - github Work with GitHub repositories, PRs and issues. - pdf Read and analyze PDF documents. - refactor Refactor existing code safely.
当模型判断:
再加载完整:
于是变成:
1 2 3 4 5 基础 prompt + 当前激活 Skill + 当前 messages
也就是说:
1 2 3 4 5 6 7 Skill Registry ↓ Skill Selector ↓ Activated Skills ↓ Context Builder
而不是:
1 2 3 4 5 100 个 Skill ↓ 全部塞 system ↓ 上下文爆炸
4. Memory / RAG 更不能永久塞 system 比如用户问:
帮我看看这个项目为什么启动失败。
Agent 检索得到:
1 2 3 4 README.md pyproject.toml 错误日志 项目结构
这些属于本轮工作上下文 。
比较合理的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 system -------- 你是 coding agent... context -------- Repository: Python 3.12 FastAPI project Relevant files: pyproject.toml ... README ... user -------- 为什么启动失败?
下一轮如果这些内容已经没用了,就可以不传。
所以:
Memory 更像:
1 ContextBuilder 每轮选择性注入的数据
5. Chat history 也是 Context Builder 管 比如你的 Agent 已经跑了 100 轮:
1 2 3 4 5 6 7 user assistant tool assistant user tool ...
不可能无限往模型塞。
因此实际一般是:
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [system] You are Nancode... [summary] Earlier in this session: - User asked to refactor authentication. - We decided to use JWT. - auth.py has already been modified. - Remaining task: add tests. [recent messages] user: 测试还没写呢 assistant: ... tool: ... [current] user: 继续
所以你之前问的压缩 ,本质就是修改:
并不会去动:
1 2 3 4 Tool Registry Skill Registry System Config UI Chat History
这几个东西应该是不同的数据结构 。
如果让小幽帮你设计你的 Agent,我会拆成这几个核心类 大概这样:
1 2 3 4 5 6 7 class Agent : context_builder: ContextBuilder tool_registry: ToolRegistry skill_registry: SkillRegistry memory_manager: MemoryManager conversation: Conversation llm: LLMClient
然后每轮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 async def run (user_input: str ): conversation.add_user_message(user_input) skills = skill_registry.select(user_input) tools = tool_registry.get_enabled_tools() memories = await memory_manager.retrieve( query=user_input ) context = context_builder.build( system_prompt=BASE_SYSTEM_PROMPT, skills=skills, memories=memories, conversation=conversation, ) response = await llm.generate( messages=context.messages, tools=tools, ) return response
这里最关键的其实就是这个:
它才是整个 Agent 的“装箱工”。
每次 LLM 调用前,它决定:
你可以进一步把整个架构理解成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ┌──────────────┐ │ SystemPrompt │ └──────┬───────┘ │ ┌─────────────┐ │ │SkillRegistry├────────┤ └─────────────┘ │ ▼ ┌─────────────┐ ┌──────────────┐ │MemoryManager├──►│ContextBuilder │ └─────────────┘ └──────┬───────┘ │ messages ┌─────────────┐ │ │Conversation ├──────────┤ └─────────────┘ │ ▼ ┌───────────┐ │ LLM │◄──── Tool schemas └─────┬─────┘ │ tool_call │ ▼ ┌─────────────┐ │ToolRegistry │ └─────────────┘
所以给你一句特别适合记笔记的话:
Agent 的上下文不是一个 Prompt,而是一组运行时状态经过 ContextBuilder 筛选、压缩、排序后,形成的一次 LLM Request。
其中:
1 2 3 4 5 6 System Prompt → 稳定规则 Skill → 当前任务的方法论 Tools → 能做什么 Memory/RAG → 现在需要知道什么 History → 刚才发生了什么 User Message → 现在要做什么
这六个东西一旦分清楚,你后面做 Skill、MCP、上下文压缩、Session、SubAgent 基本都会突然串起来。你现在其实已经摸到 Agent Harness 最核心的那一层了。