上一篇我们分析了工具编排。今天深入第三个主题:上下文管理。Agent 的上下文包含消息历史、文件状态、Token 预算、配置等。如何高效管理这些状态?
ToolUseContext:共享状态容器
Context 是 Agent 执行的"记忆":
typescript
type ToolUseContext = {
// === 执行控制 ===
abortController: AbortController
// === 文件状态 ===
readFileState: FileStateCache
// === 消息历史 ===
messages: Message[]
// === UI 状态 ===
getAppState(): AppState
setAppState(updater: (prev: AppState) => AppState): void
// === 配置选项 ===
options: {
tools: Tools
commands: Command[]
mcpClients: MCPServerConnection[]
mainLoopModel: string
thinkingConfig: ThinkingConfig
isNonInteractiveSession: boolean
maxBudgetUsd?: number
customSystemPrompt?: string
agentDefinitions: AgentDefinitionsResult
}
// === Agent 信息 ===
agentId?: AgentId
agentType?: string
toolUseId?: string
// === 追踪信息 ===
queryTracking?: QueryChainTracking
// === 限制配置 ===
fileReadingLimits?: { maxTokens?: number; maxSizeBytes?: number }
globLimits?: { maxResults?: number }
// === Hook 触发器 ===
nestedMemoryAttachmentTriggers?: Set<string>
loadedNestedMemoryPaths?: Set<string>
dynamicSkillDirTriggers?: Set<string>
// === 特殊状态 ===
renderedSystemPrompt?: SystemPrompt
localDenialTracking?: DenialTrackingState
preserveToolUseResults?: boolean
}
Context 包含了执行所需的所有信息。
文件读取状态缓存
为什么需要缓存?
每次读取文件都发送内容给 API,浪费 Token。缓存避免重复发送:
typescript
type FileReadState = {
content: string
timestamp: number
offset?: number
limit?: number
isPartialView: boolean
}
type FileStateCache = Map<string, FileReadState>
// 读取时记录状态
const content = await fs.readFile(filePath)
readFileState.set(filePath, {
content,
timestamp: Date.now(),
offset: input.offset,
limit: input.limit,
isPartialView: input.offset !== undefined || input.limit !== undefined,
})
缓存命中检测
typescript
// 再次读取同一范围时
const existingState = readFileState.get(filePath)
if (existingState) {
// 范围匹配
const rangeMatch =
existingState.offset === input.offset &&
existingState.limit === input.limit
// 文件未修改
const mtimeMs = await getFileModificationTime(filePath)
const unchanged = mtimeMs === existingState.timestamp
if (rangeMatch && unchanged) {
// 返回 stub,不发送内容
return {
type: 'file_unchanged',
file: { filePath },
}
}
}
编辑前的状态检查
typescript
// Edit 工具要求先读取
const readState = readFileState.get(filePath)
if (!readState) {
return {
result: false,
message: 'File has not been read yet. Read it first.',
errorCode: 6,
}
}
// 文件可能被外部修改
if (lastWriteTime > readState.timestamp) {
// 检查内容是否真的变了
const currentContent = await fs.readFile(filePath)
if (currentContent !== readState.content) {
return {
result: false,
message: 'File has been modified since read',
errorCode: 7,
}
}
}
缓存既是优化,也是安全机制------确保编辑的文件是已知的。
子 Agent 的缓存继承
typescript
// Fork 模式:子 Agent 克隆缓存
const agentReadFileState = forkContextMessages !== undefined
? cloneFileStateCache(toolUseContext.readFileState)
: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
// 克隆函数
function cloneFileStateCache(source: FileStateCache): FileStateCache {
const clone = new Map()
for (const [key, value] of source.entries()) {
clone.set(key, { ...value })
}
return clone
}
子 Agent 继承父 Agent 已读取的文件状态,但独立管理更新。
Token 预算管理
Token 计算
typescript
// 粗略估算
function roughTokenCountEstimation(text: string): number {
// 英文 ~4 chars/token
// 中文 ~1.5 chars/token
return Math.ceil(text.length / 4)
}
// 精确计算(使用 tokenizer)
function exactTokenCount(text: string, model: string): number {
const tokenizer = getTokenizerForModel(model)
return tokenizer.encode(text).length
}
预算分配
typescript
// Token 预算来源
function getTokenBudget(model: string): number {
// Claude 模型限制
if (model.includes('claude-3')) {
return 200_000 - SYSTEM_PROMPT_TOKENS - SAFETY_BUFFER
}
// 默认
return 100_000 - SAFETY_BUFFER
}
// 预算分配
type TokenBudgetAllocation = {
systemPrompt: number // 系统提示固定预算
userContext: number // CLAUDE.md 等
conversationHistory: number // 消息历史
toolDescriptions: number // 工具提示
responseBudget: number // 响应预留
}
function allocateTokenBudget(total: number): TokenBudgetAllocation {
return {
systemPrompt: 20_000,
userContext: 30_000,
conversationHistory: total - 20_000 - 30_000 - 10_000 - 40_000,
toolDescriptions: 10_000,
responseBudget: 40_000,
}
}
Token 监控
typescript
type TokenTracker = {
currentCount: number
budget: number
warningThreshold: number // 80%
criticalThreshold: number // 90%
}
function checkTokenStatus(tracker: TokenTracker): TokenStatus {
const usageRatio = tracker.currentCount / tracker.budget
if (usageRatio >= tracker.criticalThreshold) {
return 'critical' // 触发压缩
}
if (usageRatio >= tracker.warningThreshold) {
return 'warning' // 显示警告
}
return 'ok'
}
自动压缩触发
typescript
// 每轮结束后检查
const tokenCount = estimateTokenCount(currentMessages)
const tokenBudget = context.tokenBudget
if (tokenCount >= tokenBudget * 0.9) {
// 触发压缩
const compactResult = await compactConversation(
currentMessages,
context,
cacheSafeParams,
false, // suppressFollowUpQuestions
undefined, // customInstructions
true, // isAutoCompact
)
currentMessages = compactResult.compactedMessages
yield createCompactBoundaryMessage(compactResult)
}
消息历史管理
消息类型
typescript
type Message =
| UserMessage // 用户输入
| AssistantMessage // AI 响应
| ToolResultMessage // 工具结果
| SystemMessage // 系统提示
| AttachmentMessage // 文件附件
| CompactBoundaryMessage // 压缩边界
消息过滤
typescript
// 发送给 API 时过滤
function filterMessagesForAPI(messages: Message[]): APIMessage[] {
return messages
.filter(m => m.type !== 'compact_boundary')
.filter(m => m.type !== 'attachment' || m.shouldInclude)
.map(normalizeMessageForAPI)
}
// 不完整工具调用过滤
function filterIncompleteToolCalls(messages: Message[]): Message[] {
// 移除没有 result 的 tool_use
const toolUseIds = new Set<string>()
const completeMessages: Message[] = []
for (const message of messages) {
if (message.type === 'assistant') {
for (const block of message.message.content) {
if (block.type === 'tool_use') {
toolUseIds.add(block.id)
}
}
}
if (message.type === 'tool_result') {
if (toolUseIds.has(message.toolUseId)) {
completeMessages.push(message)
}
}
}
return completeMessages
}
消息截断
typescript
// 消息太大时截断
function truncateMessageContent(message: Message, maxTokens: number): Message {
const content = getMessageContent(message)
const tokenCount = estimateTokenCount(content)
if (tokenCount > maxTokens) {
// 截断并添加提示
const truncatedContent = content.slice(0, maxTokens * 4)
return {
...message,
message: {
...message.message,
content: truncatedContent + '\n[truncated]',
},
}
}
return message
}
CLAUDE.md 加载
加载流程
typescript
async function loadClaudeMd(cwd: string): ClaudeMdResult {
const paths = [
join(cwd, 'CLAUDE.md'),
join(cwd, '.claude', 'CLAUDE.md'),
]
for (const path of paths) {
if (await fs.exists(path)) {
const content = await fs.readFile(path)
// 解析 frontmatter
const { frontmatter, body } = parseFrontmatter(content)
// 估算 token
const tokenEstimate = estimateTokenCount(body)
return {
path,
content: body,
frontmatter,
tokenEstimate,
}
}
}
return null
}
Token 预算控制
typescript
// CLAUDE.md 太大时警告
if (claudeMdResult.tokenEstimate > 20_000) {
addNotification({
type: 'warning',
message: 'CLAUDE.md is large (~${claudeMdResult.tokenEstimate} tokens). Consider splitting.',
})
}
// 超限时截断
if (claudeMdResult.tokenEstimate > MAX_CLAUDE_MD_TOKENS) {
const truncated = truncateClaudeMd(claudeMdResult.content, MAX_CLAUDE_MD_TOKENS)
claudeMdResult.content = truncated
claudeMdResult.tokenEstimate = MAX_CLAUDE_MD_TOKENS
}
嵌套加载
typescript
// CLAUDE.md 可以引用其他文件
function processClaudeMdReferences(content: string, cwd: string): string[] {
const references: string[] = []
// 检测 @file 语法
const matches = content.matchAll(/@([^\s]+)/g)
for (const match of matches) {
const refPath = join(cwd, match[1])
references.push(refPath)
}
return references
}
// 加载引用文件
async function loadNestedClaudeMdFiles(references: string[]): AttachmentMessage[] {
const attachments: AttachmentMessage[] = []
for (const refPath of references) {
if (await fs.exists(refPath)) {
const content = await fs.readFile(refPath)
attachments.push({
type: 'attachment',
attachment: {
type: 'file',
path: refPath,
content,
},
})
}
}
return attachments
}
系统提示构建
动态构建
typescript
async function buildSystemPrompt(context: ToolUseContext): SystemPrompt {
const parts: string[] = []
// 1. 基础系统提示
parts.push(getBaseSystemPrompt())
// 2. 工具描述
const toolDescriptions = await buildToolDescriptions(context.options.tools)
parts.push(toolDescriptions)
// 3. 用户上下文(CLAUDE.md)
if (context.userContext?.claudeMd) {
parts.push(context.userContext.claudeMd.content)
}
// 4. 记忆文件
const memoryFiles = await loadMemoryFiles(context)
for (const memory of memoryFiles) {
parts.push(memory.content)
}
// 5. Skills 提示
const skillPrompts = await buildSkillPrompts(context.options.skills)
parts.push(skillPrompts)
// 6. 自定义追加
if (context.options.appendSystemPrompt) {
parts.push(context.options.appendSystemPrompt)
}
return {
content: parts.join('\n\n'),
tokenEstimate: estimateTokenCount(parts.join('\n\n')),
}
}
Token 预算平衡
typescript
function balanceSystemPromptBudget(
allocation: TokenBudgetAllocation,
actual: { systemPrompt: number; userContext: number },
): string {
const overhead = actual.systemPrompt + actual.userContext - allocation.systemPrompt - allocation.userContext
if (overhead > 0) {
// 超限:压缩用户上下文
const compressedUserContext = compressUserContext(
actual.userContext,
allocation.userContext,
)
return rebuildSystemPrompt({
systemPrompt: actual.systemPrompt,
userContext: compressedUserContext,
})
}
return buildFullSystemPrompt()
}
上下文隔离
子 Agent 独立上下文
typescript
function createSubagentContext(parent: ToolUseContext, overrides: Partial<ToolUseContext>): ToolUseContext {
return {
...parent,
// 独立的消息历史
messages: overrides.messages ?? [],
// 克隆的文件状态
readFileState: cloneFileStateCache(parent.readFileState),
// 独立的 AbortController
abortController: new AbortController(),
// 独立的 Agent ID
agentId: overrides.agentId,
agentType: overrides.agentType,
// 可能有不同的权限模式
getAppState: overrides.getAppState ?? parent.getAppState,
// 独立的 MCP clients(可能不同)
options: {
...parent.options,
mcpClients: overrides.mcpClients ?? parent.options.mcpClients,
tools: overrides.tools ?? parent.options.tools,
},
}
}
上下文清理
typescript
// 子 Agent 结束时清理
async function cleanupSubagentContext(context: ToolUseContext): void {
// 清除文件状态缓存
context.readFileState.clear()
// 关闭 MCP connections
for (const client of context.options.mcpClients) {
if (client.type === 'connected' && client.isSubagentOwned) {
await client.cleanup()
}
}
// 清除 Hook 注册
clearSessionHooks(context.agentId)
// 清除追踪
unregisterPerfettoAgent(context.agentId)
}
状态持久化
会话保存
typescript
async function saveSessionState(sessionId: string, context: ToolUseContext): void {
const sessionDir = `.claude/sessions/${sessionId}`
// 消息历史
await fs.writeFile(
join(sessionDir, 'messages.json'),
JSON.stringify(context.messages),
)
// 文件状态摘要(不保存完整内容)
const fileStateSummary = {}
for (const [path, state] of context.readFileState.entries()) {
fileStateSummary[path] = {
timestamp: state.timestamp,
offset: state.offset,
limit: state.limit,
}
}
await fs.writeFile(
join(sessionDir, 'file-state.json'),
JSON.stringify(fileStateSummary),
)
// 元数据
await fs.writeFile(
join(sessionDir, 'metadata.json'),
JSON.stringify({
sessionId,
agentId: context.agentId,
tokenCount: estimateTokenCount(context.messages),
savedAt: Date.now(),
}),
)
}
会话恢复
typescript
async function restoreSessionState(sessionId: string): ToolUseContext {
const sessionDir = `.claude/sessions/${sessionId}`
// 消息历史
const messages = JSON.parse(await fs.readFile(join(sessionDir, 'messages.json')))
// 文件状态(重新验证)
const fileStateSummary = JSON.parse(await fs.readFile(join(sessionDir, 'file-state.json')))
const readFileState = new Map()
for (const [path, summary] of Object.entries(fileStateSummary)) {
// 验证文件是否还存在
if (await fs.exists(path)) {
const mtimeMs = await getFileModificationTime(path)
if (mtimeMs === summary.timestamp) {
readFileState.set(path, {
timestamp: summary.timestamp,
offset: summary.offset,
limit: summary.limit,
isPartialView: summary.offset !== undefined || summary.limit !== undefined,
})
}
}
}
return buildToolUseContext({
sessionId,
initialMessages: messages,
readFileState,
})
}
总结
上下文管理展示了状态管理的复杂设计:
| 状态类型 | 管理策略 |
|---|---|
| 文件读取 | 缓存 + 去重 + 修改检测 |
| Token | 预算分配 + 监控 + 自动压缩 |
| 消息历史 | 过滤 + 截断 + 类型管理 |
| CLAUDE.md | 加载 + 嵌套引用 + Token 控制 |
| 系统提示 | 动态构建 + 预算平衡 |
| 子 Agent | 克隆 + 独立 + 清理 |
| 持久化 | 保存 + 恢复 + 验证 |
核心设计:缓存 + Token 预算 + 克隆 + 持久化。
下一篇,我们将深入子 Agent 架构------Fork 模式如何工作,父子 Agent 如何通信。