一个完整 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 会胖成一只球。


2. Tool 通常并不是文本塞进 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
这次需要 github skill

再加载完整:

1
github/SKILL.md

于是变成:

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
--------
为什么启动失败?

下一轮如果这些内容已经没用了,就可以不传。

所以:

1
Memory ≠ System Prompt

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
最近消息
+
历史摘要
+
关键状态

例如:

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
Conversation Context

并不会去动:

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

这里最关键的其实就是这个:

1
ContextBuilder

它才是整个 Agent 的“装箱工”。

每次 LLM 调用前,它决定:

1
这轮到底给模型看什么?

你可以进一步把整个架构理解成:

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 最核心的那一层了。