上下文控制:instructions、按需 skills、subagents
30 秒导读: 一个 agent 跑得好不好,关键看「模型每一回 合到底看到了什么」。eve 给你三档杠杆:
instructions是永远在场的系统提示;skills/默认不进提示,模型按需用load_skill拉进来;subagents干脆把一整段活委派给一个有独立提示、工具、沙箱、状态的子 agent。本章讲这三档怎么实现。
本章只讲「eve 如何控制模型每回合看到什么」。沙箱的信任边界细节见 05-channels-connections-security; 默认 harness 的 agent 循环与 compaction 见 03-harness-tool-loop;文件系统如何被发现、编译成 manifest 见 01-filesystem-discovery。
1. 这是什么(零基础也能懂)
一句话定义: 上下文控制 = 决定「模型这一回合的提示里塞了什么、没塞什么」的那套规则。
一个 agent 的「智力」很大程度上是被它看到的上下文决定的。塞太多——又贵、又慢、又容易被无关内容带偏; 塞太少——它不知道该怎么干。所以问题永远是:哪些东西每回合都得在,哪些只在需要时才出现,哪些根本不该进这个 agent 的提示。
eve 把答案分成三档,按「成本」由低到高排:
| 杠杆 | 什么时候在提示里 | 适合放什么 |
|---|---|---|
instructions(指令) | 每一回合都在(always-on) | agent 的 永久身份、稳定契约 |
skills/(技能) | 默认不在,模型 load_skill 后才注入 | 可选的、长的操作手册 / playbook |
subagents(子 agent) | 完全不在本 agent 的提示里,另起一个隔离上下文 | 需要不同身份 / 不同工具面 / 想并行的整段活 |
一句话直觉/类比: 把它想成你桌上的三种东西——
instructions是贴在显示器上的便签:抬头就看见,永远在。skills是书架上的手册:平时收着,要用时才抽出来翻开。subagent是把活外包给另一个同事:他有自己的桌子、自己的工具、自己的便签,干完把结果交回来。
一个最小的目录长这样(eve 是「文件系统即接口」,见 01-filesystem-discovery):
agent/
├── agent.ts # 定义 agent(模型、limits 等)
├── instructions.md # always-on 系统提示
├── skills/
│ ├── forecast.md # flat skill(单文件)
│ └── research/SKILL.md # packaged skill(目录 + 附属文件)
└── subagents/
└── researcher/agent.ts # 声明式子 agent(必须 export description)
本节不出现底层代码。记住一句话就够:instructions 永远在、skills 按需进、subagent 整段外包。
2. 顶层全景(三档杠杆怎么落到一次模型调用)
这节讲:一次模型调用的系统提示到底是怎么拼出来的,三档杠杆各自在哪一步进场。
怎么读这张图: 从上到下是「拼一次模型提示」的顺序;左边是 always-on 的、右边是按需的。
┌─────────────────────────────────────────────┐
│ 一次模型调用的 system 提示 │
└─────────────────────────────────────────────┘
▲
┌──────────────────────────────┼──────────────────────────────┐
│ always-on(每回合都拼) │ 按需(只有触发了才进) │
▼ ▼ ▼
┌───────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ instructions │ │ Available skills 清单│ │ load_skill 的结果 │
│ 的 markdown │ │ (只列名+描述,不含正文)│ │ (某个 SKILL.md 正文)│
│ ① 永久身份 │ │ ② 路由提示 │ │ ③ 模型主动拉来的 │
└───────────────┘ └────────────────────┘ └────────────────────┘
│ ▲
│ 另一条路:把整段活交出去,不进本提示 │ 模型调用 load_skill 工具
▼ │
┌──────────────────────────────────────────────┐ │
│ subagent → 另起一个隔离子 session │ ← 子 agent 有自己 │
│ (独立 instructions / tools / sandbox / state)│ 的这整张图 │
└──────────────────────────────────────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪个文件(packages/eve/src/) |
|---|---|---|
| 提示拼装 | 把 instructions / workspace / connections / skills 清单拼成 base 提示 | runtime/prompt/compose.ts:30 composeRuntimeBasePrompt |
| skills 清单格式化 | 生成「Available skills」那一段(只列名+描述) | execution/skills/instructions.ts:21 formatAvailableSkillsSection |
load_skill 工具 | 从沙箱读出某个 SKILL.md 正文,作为工具结果返回 | runtime/framework-tools/skill.ts:68 SKILL_TOOL_DEFINITION |
| 每回合注入 | 把动态指令 + 技能公告塞进本回合 system 消息 | harness/tool-loop.ts:564 |
| subagent 注册 | 把每个子 agent 降成一个模型可见工具 | runtime/subagents/registry.ts:49 createRuntimeSubagentRegistry |
| subagent 派发 | 真正起一个子 session 跑这个委派 | execution/dispatch-runtime-actions-step.ts |
| 深度上限 | 算当前委派深度、决定还能不能再委派 | harness/subagent-depth.ts:21 resolveSubagentDelegationLimit |
主线走一遍(高层): 一个回合开始 → harness 拼系统提示:instructions 正文 + workspace 提示 + connections + skills 清单(只有名字和描述) →
模型判断:这活要不要某个 skill?要 → 调 load_skill → eve 从沙箱读出那份 SKILL.md 正文当工具结果交回 → 模型拿到正文继续干。
若这活该整段外包 → 模型调某个 subagent 工具 → eve 起一个隔离子 session,跑完把结果当工具结果交回。
3. 核心机制一:instructions —— 永远在场的系统提示
它要解决的小问题
agent 总有一份「不管这回合干啥都该遵守」的契约:它是谁、说话风格、硬规矩。这部分必须每回合都在。
思路:稳定 = 进 always-on,且尽量可被缓存
eve 的设计取舍是:把稳定的东西做成整个 session 不变的系统提示,这样上游的 prompt caching 能命中,省钱省延迟。 所以「永久身份」放 instructions,「会变的东西」(谁在调用、加载了哪个 skill)走别的通道,故意不去改 base 提示。
两种写法:markdown vs TypeScript
最简单的写法是 instructions.md,纯 markdown,就是一段稳定指令。
当你需要从「带类型的辅助函数 / 库代码 / 构建期环境值」拼出这段提示时,改写成模块 instructions.ts:
// 示意,非源码:agent/instructions.ts
import { defineInstructions } from "eve/instructions";
import { buildInstructionsPrompt } from "./lib/prompts.js";
export default defineInstructions({
markdown: buildInstructionsPrompt(), // 在构建期算出最终 markdown
});
关键细节:模块版只在构建期跑一次。 defineInstructions 的契约写得很明确:模块化的静态指令在构建期执行一次,
编译器把产出的 markdown 收进 compiled manifest,运行时每个 session 直接用同一份,不会再跑这个模块
(public/definitions/instructions.ts:19 InstructionsDefinition、:34 defineInstructions)。
defineInstructions 还会给返回值打一个 brand 标记(INSTRUCTIONS_BRAND),让动态指令生命周期能校验「这个返回值确实是经过该 helper 的」
(public/definitions/instructions.ts:37)。
真实实现:instructions 怎么拼进 base 提示
base 提示由 composeRuntimeBasePrompt 拼:
// runtime/prompt/compose.ts:30 composeRuntimeBasePrompt(摘录)
return [
...createInstructionsPromptBlocks(input.instructions), // ① instructions 正文
...createWorkspacePromptBlocks(input.workspaceSpec), // ② workspace 浅提示
...(input.toolsAvailable ? [PARALLEL_ACTION_INSTRUCTION] : []),
...createConnectionsPromptBlocks(input.connections), // ③ connections
...createSkillsPromptBlocks(input.skills), // ④ skills 清单(只有名+描述)
];
注意函数名:composeRuntimeBasePrompt——「without flattening skills into always-on instructions」
(runtime/prompt/compose.ts:27 注释)。这一句是整章的题眼:skills 不会被压平进 always-on 指令,base 提示里只放 skills 的清单,不放正文。
instructions 那一块自己很朴素:trim 后非空才进,加一行标题 Instructions (<name>)
(runtime/prompt/compose.ts:40 createInstructionsPromptBlocks)。