Cat Blog
HomeBlogTools
Search
Language
Choose site style
Choose accent color
Click Effect
Theme

Cat Blog · Updated regularly. Source code is available on GitHub.

Index 10: System Prompt — 运行时分段拼接

August 13th, 2026
AI智能体Kotlin后端

Series

使用Kotlin从0开发一个ClaudeCode

Series

使用Kotlin从0开发一个ClaudeCode

Progress 10 / 21

使用Kotlin从0开发一个ClaudeCode

Previous in series

Index 09: Memory — 跨会话记忆管线

Next in series

Index 11: Error Recovery — 重试 / fallback / token 升级

目标

s01 到 s09 让智能体具备了完整的能力闭环:工具调用、权限、Hook、Todo、子智能体、技能、压缩、记忆。但系统提示的组装一直是分散的——基础提示词是 AgentLoop 里的一个固定字符串,skills 索引经 SkillInjectionHook 在请求末尾追加,memory 索引经 MemoryInjectionHook 在请求末尾追加。三块内容最终都被 AnthropicProvider 抽出来并进 API 顶层的 system 字段,但组装逻辑散落三处:顺序不可控、可观测性差、无法独立测试。

Index 10 引入统一的分段系统提示组装机制:

  1. PromptSegment —— 一段可命名的系统提示片段(命名、有序、动态内容)
  2. SystemPromptBuilder —— 运行时按序拼接所有片段,空白片段自动跳过
  3. 注入迁移 —— skills/memory 从 PRE_LLM_REQUEST Hook 迁移为 builder 片段注册
  4. AgentLoop 无感知 —— 构造函数从 systemPrompt: String 换成 systemPromptBuilder,每次请求由 builder 组装

完成后的效果——新增一类提示片段(工具说明、团队上下文、自治循环指令)只需要一行 register(),不用再写一个 Hook、不用动 AgentLoop:

KOTLIN
// s10 之后,新增系统提示来源只需注册一段:
val builder = SystemPromptBuilder().apply {
    register(PromptSegment("base", 0) { systemPrompt })
    register(PromptSegment("skills", 10) { buildSkillsMetadataText(skillStore.getAll()) })
    register(PromptSegment("memory", 20) { buildMemoryIndexText(memoryStore.loadAll(), memoryConfig.maxInjectedSummaries) })
}

为什么需要

现实问题

s09 完成时,系统提示的组装是这样的:

TEXT
AgentLoop.buildRequestMessages()
  = [Message(SYSTEM, systemPrompt)]      ← base,固定字符串(Main.kt 传入)
    + messagesHistory.toList()            ← 对话历史
    + onPreLlmRequest 追加                 ← PRE_LLM_REQUEST Hook 的产物
        = [SYSTEM skills 索引]             ← SkillInjectionHook
        = [SYSTEM memory 索引]             ← MemoryInjectionHook

AnthropicProvider 把所有 SYSTEM 消息过滤出来 \n\n 拼接,放进 API 的 system 字段。所以三块最终汇入同一个 system 字段,但有三处独立代码在维护它:

  • 顺序靠注册时序而非声明 —— skills 先注册先追加,想调整顺序得改 ReplLoop 的注册顺序,没有显式声明
  • 新增片段 = 新写一个 Hook —— s20 要加团队上下文,就得再造第三个 InjectionHook
  • 无法独立测试 —— 文本构建逻辑藏在 Hook 类内部,测试要经过 HookManager 的事件分发才能触达
  • base 与其他片段不对称 —— base 是构造函数参数,skills/memory 是 Hook 追加,两种机制并存

一个具体的困境

假设 s20 要注入"团队上下文"(当前有几个 agent 在协作)。s10 之前,你只能:

  1. 复制 SkillInjectionHook 写一个 TeamInjectionHook,改文本构建逻辑
  2. 在 ReplLoop 里手动排好注册顺序(还要祈祷顺序对了)
  3. 决定"团队上下文"应该排在哪——是 skills 之前还是之后?没有声明式的答案,只能靠注册时序碰运气

这还不是最糟的。真正的问题是系统提示的顺序不可见:你在 ReplLoop 里看到两行 Hook 注册,但完全无法确定最终 system 字段里三块内容谁先谁后、中间有没有空行、某个 store 为空时会不会输出一个孤零零的标题。

s10 把这些全部变成声明式:order 字段显式声明顺序,build() 确定性输出,空白自动跳过。

设计原则

  • 统一组装入口 —— SystemPromptBuilder 是系统提示的唯一组装点,片段 = 注册,不再散落
  • 片段运行时求值 —— provider 每次 build() 重新执行,memory 索引在会话中增长后(memory_write)下一次请求立即反映,无需手动失效
  • 空白即禁用 —— 片段不设 enabled 开关,provider 返回空白就跳过(YAGNI)
  • 保持行为等价 —— 注入位置从"请求末尾"移到"系统提示顶部",因 AnthropicProvider 会合并所有 SYSTEM 消息,API 收到的内容完全一致
  • skill → system 依赖间接满足 —— 设计文档标注的依赖通过 ReplLoop 组装实现,skill/memory 包不 import system 包

核心设计与实现

架构全景

TEXT
s10 之前(分散)                          s10 之后(统一)
                                       
AgentLoop                                SystemPromptBuilder
  │  systemPrompt: String                 │  register(PromptSegment(...))
  │                                       │    base   (order 0) ← systemPrompt
  │  buildRequestMessages                 │    skills (order 10) ← buildSkillsMetadataText(store)
  │    [SYSTEM base] + history            │    memory (order 20) ← buildMemoryIndexText(store)
  │  + onPreLlmRequest 追加               │  build() → 按 order 升序拼接
  │    [SYSTEM skills]   ← Hook           │
  │    [SYSTEM memory]   ← Hook           │  AgentLoop
  │                                       │    │  systemPromptBuilder
  │  AnthropicProvider 合并所有 SYSTEM    │    │  buildRequestMessages()
  │    → system 字段                      │    │    [SYSTEM builder.build()] + history
  │                                       │    │
  │                                       │    AnthropicProvider → system 字段

重构前:两个 Hook 的全貌

理解这次重构的价值,先看被删掉的代码长什么样。这是 s09 之前的 SkillInjectionHook:

KOTLIN
class SkillInjectionHook(private val store: SkillStore) {
    fun registerTo(manager: HookManager) {
        val handler: HookHandler = handler@{ event ->
            if (event.type != HookEvent.Type.PRE_LLM_REQUEST) {
                return@handler HookResult.Continue
            }
            val skills = store.getAll()
            if (skills.isEmpty()) {
                return@handler HookResult.Continue
            }
            val text = buildSkillsMetadataText(skills)
            HookResult.AppendMessages(listOf(Message(Role.SYSTEM, text)))
        }
        manager.on(HookEvent.Type.PRE_LLM_REQUEST, handler)
    }

    internal fun buildSkillsMetadataText(skills: List<SkillManifest>): String = buildString {
        append("# Available Skills\n\n")
        append("The following skills are available. Call the `load_skill` tool with the skill's name to retrieve its full instructions, then follow them.\n\n")
        for (skill in skills) {
            append("## ${skill.name}\n")
            append("${skill.description}\n\n")
        }
    }.trimEnd()
}

MemoryInjectionHook 是几乎相同的形状。这类 Hook 的问题:

  • 三个职责揉在一个类里:事件分发(registerTo)、空库判断、文本构建(buildSkillsMetadataText)——后两者与"事件"毫无关系
  • 测试要绕道:想测文本格式,得先构造 HookManager + 注册 handler + fire 事件,才能拿到 append 结果
  • 顺序隐式:skills 先注册所以先注入,改顺序 = 改注册时序

s10 把事件分发留给 HookManager(保留为通用扩展点),把空库判断 + 文本构建提取为纯函数,把注入位置统一到 builder。职责各归其位。

核心模型:PromptSegment

一段系统提示片段,三个字段各司其职:

KOTLIN
data class PromptSegment(
    val name: String,          // 片段标识(测试/日志)
    val order: Int = 0,        // 组装顺序,升序(base=0 → skills=10 → memory=20)
    val provider: () -> String // 内容提供者;空白内容在 build 时跳过
)

三个设计点:

  1. provider 是 () -> String 而非 suspend —— 文本构建只读内存 store(SkillStore / MemoryStore),无 I/O
  2. order 用间距 10 —— 预留插入空间(以后要在 base 和 skills 之间插一段,order 5 即可)
  3. name 不做去重 —— MVP 允许重复注册,按注册顺序 + order 稳定排序

SystemPromptBuilder

组装器只有两个方法,职责单一:

KOTLIN
class SystemPromptBuilder {
    private val segments = mutableListOf<PromptSegment>()

    fun register(segment: PromptSegment) {
        segments.add(segment)
    }

    fun build(): String = segments
        .sortedBy { it.order }         // 稳定排序:同 order 保持注册顺序
        .map { it.provider() }          // 每次 build 重新求值(动态)
        .filter { it.isNotBlank() }     // 空白片段跳过
        .joinToString(SEGMENT_SEPARATOR)

    companion object {
        private const val SEGMENT_SEPARATOR = "\n\n"   // 片段间空行分隔
    }
}

四条管线操作各自承担一个语义:

操作语义
sortedBy { it.order }确定性顺序(base 在最前,memory 在最后)
map { it.provider() }每次 build 动态求值——memory 索引会话中增长后立即反映
filter { it.isNotBlank() }空白即禁用——无 skills / 无 memory 时不输出空段落
joinToString("\n\n")片段间空行分隔,人类可读

文本构建函数提取

被删 Hook 的核心逻辑提取为独立函数,留在各自的包(internal 模块内可见,ReplLoop 可直接调用):

skill/SkillPrompt.kt —— 原 SkillInjectionHook.buildSkillsMetadataText:

KOTLIN
internal fun buildSkillsMetadataText(skills: List<SkillManifest>): String {
    if (skills.isEmpty()) return ""   // 空列表返回空串,让 builder 跳过
    return buildString {
        append("# Available Skills\n\n")
        append("The following skills are available. Call the `load_skill` tool with the skill's name to retrieve its full instructions, then follow them.\n\n")
        for (skill in skills) {
            append("## ${skill.name}\n")
            append("${skill.description}\n\n")
        }
    }.trimEnd()
}

memory/MemoryPrompt.kt —— 原 MemoryInjectionHook.buildMemoryIndexText:

KOTLIN
internal fun buildMemoryIndexText(entries: List<MemoryEntry>, maxInjected: Int = Int.MAX_VALUE): String {
    val capped = entries.take(maxInjected)   // 截断下沉到函数内(最终审查修复)
    if (capped.isEmpty()) return ""
    return buildString {
        append("# Persistent Memory\n\n")
        append("The following memories were saved from previous sessions. Call `memory_read` with a memory's name to retrieve its full content.\n\n")
        for (e in capped) {
            append("## ${e.name}\n")
            append("${e.description}\n\n")
        }
    }.trimEnd()
}

空列表返回 "" 是关键行为保持 —— 原 Hook 在 store 为空时返回 Continue(不注入);提取后加 if (isEmpty()) return "" 守卫,配合 builder 的空白跳过,行为完全等价。

maxInjected 参数是最终审查的产物 —— 最初截断逻辑内联在 ReplLoop 的 lambda 里,Hook 删除后该规则失去测试覆盖;下沉到函数内后 MemoryPromptTest 能直接锁定"5 条只注入 3 条"。

AgentLoop 集成

构造函数从字符串换成 builder(这是一次有意的 breaking change):

KOTLIN
class AgentLoop(
    private val llmProvider: LLMProvider,
    private val systemPromptBuilder: SystemPromptBuilder,  // 原 systemPrompt: String
    private val config: AgentConfig = AgentConfig(),
    private val hooks: AgentLoopHooks = AgentLoopHooks(),
    private val toolRegistry: ToolRegistry = ToolRegistry(),
    private val compactor: ContextCompactor = ContextCompactor(llmProvider)
)

buildRequestMessages() 用 builder 组装,并处理"全部空白"的退化路径:

KOTLIN
private fun buildRequestMessages(): List<Message> {
    val system = systemPromptBuilder.build()
        .takeIf { it.isNotBlank() }       // 全部片段空白 → build() 返回 "" → null
        ?.let { Message(Role.SYSTEM, it) }
    return listOfNotNull(system) + messagesHistory.toList()
}

listOfNotNull 保证:builder 返回空串时不产生 SYSTEM 消息(AnthropicProvider 对空 system 字段处理为 null,API 请求省略该字段)——这是最终审查要求补测试锁住的分支。

SubagentFactory:子智能体隔离

子智能体需要隔离上下文(s06 的契约:独立 messagesHistory、独立工具、独立 Hook)。s10 后子智能体只注册 base 片段,不注入 skills/memory:

KOTLIN
val loop = AgentLoop(
    llmProvider = llmProvider,
    systemPromptBuilder = SystemPromptBuilder().apply {
        register(PromptSegment("base", 0) { SubagentPrompts.DEFAULT })
    },
    config = config,
    hooks = AgentLoopHooks(
        onPreToolUse = { tc -> hookManager.firePreToolUse(tc) },
        onPostToolUse = { tc, result -> hookManager.firePostToolUse(tc, result) }
    ),
    toolRegistry = toolRegistry
)

ReplLoop:三段组装

记忆系统构建之后、AgentLoop 构造之前,组装完整的三段系统提示:

KOTLIN
// s10: 组装系统提示片段(base → skills → memory,order 升序)
val systemPromptBuilder = SystemPromptBuilder().apply {
    register(PromptSegment("base", 0) { systemPrompt })
    register(PromptSegment("skills", 10) { buildSkillsMetadataText(skillStore.getAll()) })
    register(PromptSegment("memory", 20) {
        buildMemoryIndexText(memoryStore.loadAll(), memoryConfig.maxInjectedSummaries)
    })
}

组装后的系统提示(三条终态):

TEXT
You are a helpful coding assistant. Answer concisely.

# Available Skills

The following skills are available. Call the `load_skill` tool with the
skill's name to retrieve its full instructions, then follow them.

## code-review
Review code changes for correctness and quality.

# Persistent Memory

The following memories were saved from previous sessions. Call `memory_read`
with a memory's name to retrieve its full content.

## user-prefers-vim
用户偏好 vim 编辑器

关键机制:AnthropicProvider 的 SYSTEM 合并

这次重构的"行为等价"依赖于 AnthropicProvider 的一个实现细节——它会把请求里所有 SYSTEM 消息过滤出来,\n\n 拼进 API 顶层的 system 字段:

KOTLIN
val systemPrompt = messages
    .filter { it.role == Role.SYSTEM }     // 抽出所有 SYSTEM 消息
    .joinToString("\n\n") { it.content }   // 空行分隔拼接
    .takeIf { it.isNotBlank() }            // 全空白 → null → 请求省略 system 字段

val apiMessages = messages
    .filter { it.role != Role.SYSTEM }     // 剩余消息转为 API 消息列表
    .map { msg -> convertToAnthropicMessage(msg) }

这带来两个推论:

  1. 注入位置不影响内容 —— 无论是"base 在头部 + skills/memory 追加在请求末尾"(s10 前),还是"builder 拼成单个 SYSTEM 放在头部"(s10 后),filter + joinToString 之后 API 收到的 system 字段完全一致。唯一受影响的只是 system 内部三段的相对顺序,而这正由 builder 的 order 声明式控制
  2. s08 的压缩摘要不受影响 —— [Conversation Summary] 系统消息仍留在 messagesHistory(它是会话状态,不是静态片段),同样被 filter 抽出并入 system 字段,无需改动

Hook 删除

SkillInjectionHook / MemoryInjectionHook 及其测试全部删除。HookManager 的 PRE_LLM_REQUEST 事件类型与 AgentLoopHooks.onPreLlmRequest 机制保留——当前无 handler 注册时 firePreLlmRequest 返回空列表,行为不变;它是未来的请求级注入扩展点。

注意:onPreLlmRequest 在上下文压缩触发时,单次迭代内会触发两次(压缩前与压缩后重建)——这是 s08 遗留行为,已在 AgentLoopHooks KDoc 注明(最终审查 F4)。


端到端流程

一次"技能/记忆进入系统提示"的完整生命周期:

启动时:

TEXT
ReplLoop.start()
  → skillStore = SkillLoader(config.skillPaths).load()
  → memoryStore = MemoryStore("~/.cat-code/memory")
  → systemPromptBuilder = SystemPromptBuilder().apply {
      register(base, 0, { systemPrompt })
      register(skills, 10, { buildSkillsMetadataText(skillStore.getAll()) })
      register(memory, 20, { buildMemoryIndexText(memoryStore.loadAll(), maxInjected) })
    }
  → AgentLoop(systemPromptBuilder = ...)

每次 LLM 请求(AgentLoop.run 的每次迭代):

TEXT
buildRequestMessages()
  → systemPromptBuilder.build()
      → 对每个片段重新执行 provider()
      → skillStore.getAll() → 当前技能索引
      → memoryStore.loadAll().take(maxInjected) → 当前记忆索引
      → 按 order 升序拼接 → "base\n\nskills\n\nmemory"
  → [Message(SYSTEM, 拼接结果)] + history
  → AnthropicProvider 放入 API system 字段

会话中 memory 变化:

TEXT
用户会话中 LLM 调 memory_write 新增一条记忆
  → memoryStore 立即持久化
  → 下一次请求 build() 重新求值 memory 片段
  → 新记忆自动出现在系统提示中(无需重启、无需手动刷新)

设计取舍深挖

几个值得展开的设计决策,为什么这么选:

为什么 provider 是 lambda 而非类

一个备选方案是给每个片段定义子类(BaseSegment、SkillsSegment、MemorySegment),各实现一个 content() 方法。但那需要:

  • 每个片段一个文件 + 一个类
  • 状态注入(store 引用)靠构造
  • 测试要逐个类写

lambda 方案把"片段"降维成一个 data class + 一个闭包,内容来源(store 查询)在闭包里闭包捕获,测试直接构造 lambda 就能测。这是"最简单模式解决问题"的体现——不需要为三行逻辑引入类层次。

为什么 order 间距 10

不是 1、2、3,而是 0、10、20。间距是预留的插入空间:

  • 将来要在 base 和 skills 之间插一段"工具说明",用 order 5,不用改任何已有片段的 order
  • 如果间距是 1,插入就得把后面的全部重排

间距 10 与 s09 的 MemoryConfig 各参数默认值一样,是"稍大但够用"的宽松设计。

为什么"空白即禁用"而不是 enabled 开关

备选是给 PromptSegment 加 enabled: Boolean 字段,ReplLoop 里 register(PromptSegment(..., enabled = skillStore.size() > 0))。但那引入两个问题:

  • enabled 在注册时求值,而 builder 要支持运行时动态(memory 会话中增长)——开关反而限制了动态性
  • 一个布尔字段 + 一个内容字段,语义重叠(内容为空不就是禁用吗)

让 provider 返回空白 = 禁用,一个机制覆盖两种情况,语义更一致。最终审查的 maxInjected 截断测试正是靠"空白跳过"锁住的。

为什么 skill → system 依赖通过 ReplLoop 间接满足

设计文档 §5.2 标注 skill → system, tool。如果严格实现,skill 包要 import system 包(比如 SkillPrompt 返回 PromptSegment)。但我们让 buildSkillsMetadataText 返回纯 String,由 ReplLoop 包成 PromptSegment:

TEXT
skill.buildSkillsMetadataText(skills) : String   ← 纯函数,不感知 system
repl: PromptSegment("skills", 10) { buildSkillsMetadataText(...) }  ← 组装在 ReplLoop

好处是 skill/memory 包保持零依赖,system 只被组装方(repl)消费。代价是少了一个编译期约束——但这符合项目"用最简单模式、不为对齐依赖图加空引用"的原则。

与 s09 的衔接:截断规则的下沉

s09 里 maxInjected 截断逻辑在 MemoryInjectionHook 的 registerTo(store.loadAll().take(maxInjected))。Hook 删除后,这条"防 context 膨胀"的安全规则一度只剩 ReplLoop 里的一行内联 .take()——没有任何测试锁定。最终审查发现后,把截断下沉到 buildMemoryIndexText(entries, maxInjected) 函数内部:

KOTLIN
internal fun buildMemoryIndexText(entries: List<MemoryEntry>, maxInjected: Int = Int.MAX_VALUE): String {
    val capped = entries.take(maxInjected)   // 规则从调用点下沉到函数内,可被测试
    if (capped.isEmpty()) return ""
    ...
}

安全规则从"藏在调用点的临时逻辑"变成"函数签名的一部分",测试直接锁定。


测试策略

测试覆盖点
SystemPromptBuilderTest顺序组装(乱序注册按 order 输出)、空白跳过、全空白返回空串、无片段空串、动态重求值(provider 结果变化)、同 order 稳定排序
SkillPromptTestbuildSkillsMetadataText 格式(# Available Skills / ## name / description)、空列表返回空串
MemoryPromptTestbuildMemoryIndexText 格式、只输出索引不输出正文(哨兵值)、空列表返回空串、maxInjected 截断(5 条注入 3 条)
AgentLoopTest构造用 builder、全部空白时无 SYSTEM 消息(none { it.role == Role.SYSTEM })
SubagentFactory / 命令测试子智能体 base-only builder;命令测试空串语义保持

测试设计要点:

  1. 动态重求值用闭包计数器验证 —— PromptSegment("dynamic", 0) { "value-$counter" },两次 build 之间 counter++,断言输出变化。这锁住了"memory 会话中增长后自动反映"的核心机制
  2. 空白分支用 listOfNotNull 验证 —— AgentLoop 层测试构造空 builder,断言 lastMessages.none { it.role == Role.SYSTEM },锁住退化路径
  3. maxInjected 截断下沉后可直接测 —— buildMemoryIndexText(5 条, maxInjected = 3) 断言第 4 条不出现,锁住"防 context 膨胀"的安全规则

开发过程:设计取舍与踩坑记录

s10 经历了与 s09 相同的完整流程(spec → plan → subagent-driven execution + 任务级审查 + 全分支审查),真实踩过的坑:

计划阶段的三个决策(经用户确认):

  • 迁移注入进 builder(而非只加基础设施)—— 单一注入路径,无冗余
  • 删除 Hook、逻辑进 builder(而非保留)—— 文本构建函数提取为独立函数
  • AgentLoop 构造换 builder(而非保留 String + 可选 builder)—— 无双路径

实现中发现并处理的计划偏差:

  • AgentLoopTest 实际 28 处构造(计划写 10)—— 实现者 grep 发现并全部迁移
  • SkillCommandTest / MemoryCommandTest 也直接构造 AgentLoop(计划漏列)—— 实现者补齐,否则编译不过
  • AgentLoopHooks KDoc 展示旧签名 —— Task 4 附带修正

最终全分支审查发现并修复(2 Important + 2 Minor):

  • F1 过期注释 —— AgentLoop 里 "skill 元数据经 PRE_LLM_REQUEST" 的注释已过时(skill 注入迁移了),改为"保留扩展点"
  • F2 maxInjected 截断失去测试 —— Hook 删除后"5 条只注入 3 条"的规则无测试锁定,下沉到 buildMemoryIndexText(maxInjected) 并补测试
  • F3 全空白分支未测 —— 补 AgentLoop 层测试
  • F4 PRE_LLM_REQUEST 压缩双触发 —— 压缩时该回调在单次迭代内触发两次(压缩前/后),KDoc 说明

几个被采纳的关键设计决策:

  • 空白即禁用(不做 enabled 开关)—— provider 返回空白就跳过,YAGNI
  • skill → system 依赖通过 ReplLoop 间接满足 —— 与设计文档 §5.2 的偏差,但避免了为对齐依赖图而增加空引用
  • 注入位置从请求尾部移到系统提示顶部 —— 语义等价(AnthropicProvider 合并所有 SYSTEM),组装更整洁
  • order 间距 10 预留插入空间 —— 避免后续加片段就要重排所有 order

下一站

s10 让系统提示的组装可组合、可扩展、可测试。这是阶段三(系统可靠性)的开端:

  • s11 Error Recovery —— 系统提示分段后,错误恢复(token 升级、fallback 模型、重试)可以在更稳定的请求结构上构建;未来可加"错误诊断"片段动态注入
  • s12 Task System —— 任务上下文可作为新的 PromptSegment 注册,与 skills/memory 平级
  • s20 Comprehensive Agent —— 全机制集成的"大系统提示"由 builder 统一组装,新增来源只需 register()

s10 留给后续最大的礼物是一个可组合的提示组装范式:任何"该让 LLM 知道的东西"(技能、记忆、任务、团队上下文)都变成一段 PromptSegment,按 order 声明顺序,运行时求值,空白自动跳过——系统提示从此不再是写死的字符串,而是声明式的片段集合。

下一篇:Index 11: Error Recovery —— token 升级 / fallback 模型 / 重试策略,让智能体在出错时自愈。

Table of Contents

Current section:目标

  • 1. 目标
  • 2. 为什么需要
  • 3. 现实问题
  • 4. 一个具体的困境
  • 5. 设计原则
  • 6. 核心设计与实现
  • 7. 架构全景
  • 8. 重构前:两个 Hook 的全貌
  • 9. 核心模型:PromptSegment
  • 10. SystemPromptBuilder
  • 11. 文本构建函数提取
  • 12. AgentLoop 集成
  • 13. SubagentFactory:子智能体隔离
  • 14. ReplLoop:三段组装
  • 15. Available Skills
  • 16. code-review
  • 17. Persistent Memory
  • 18. user-prefers-vim
  • 19. 关键机制:AnthropicProvider 的 SYSTEM 合并
  • 20. Hook 删除
  • 21. 端到端流程
  • 22. 设计取舍深挖
  • 23. 为什么 provider 是 lambda 而非类
  • 24. 为什么 order 间距 10
  • 25. 为什么"空白即禁用"而不是 enabled 开关
  • 26. 为什么 skill → system 依赖通过 ReplLoop 间接满足
  • 27. 与 s09 的衔接:截断规则的下沉
  • 28. 测试策略
  • 29. 开发过程:设计取舍与踩坑记录
  • 30. 下一站
Back to top

Related Posts

View all posts
Index 21: Terminal UX —— Claude Code 风格终端交互

Index 21: Terminal UX —— Claude Code 风格终端交互

August 21st, 2026

Index 21 将 cat-code 终端交互升级为 Claude Code 风格,解决旧 REPL 黑箱、无中断及输入体验差的问题。通过 JLine3 与 Mordant 实现历史补全、实时工具可见性、Spinner 状态及 Esc 中断。核心采用 UI 与 AgentLoop 解耦的事件流架构,支持内联权限菜单与优雅降级。同时配置 logback 收敛控制台日志,确保 TUI 清爽且功能无损,显著提升可用性。

Index 20: Comprehensive Agent —— 全机制集成(收口)

Index 20: Comprehensive Agent —— 全机制集成(收口)

August 13th, 2026

作为收官之作,把 s01-s19 的二十个核心机制整合为一个全面智能体:统一的状态查询与运行周期、自治认领与工作区隔离的贯通,以及 /status 命令的全局可视化。全机制协同运转,标志着 Cat-Code 从零完整复刻 Claude Code 核心能力的收官。

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

August 13th, 2026

引入 MCP(Model Context Protocol)插件机制,通过多传输适配与通道路由,把外部 MCP 服务器的工具动态接入智能体的工具池。工具注册从静态编译期扩展为运行时动态组装,让智能体能力随外部服务即插即用。