在 AI 编码代理的开发领域,Python 生态系统长期占据主导地位,但 Swift 正在凭借其独特的语言特性成为一股不可忽视的力量。Ivan Magda 在其系列博客中详细记录了从零构建 Swift 版编码代理的完整过程,展示了 Swift 在类型系统、并发模型和工具链集成方面的显著优势。本文将深入解析其核心架构设计,为希望在 Swift 生态中构建 AI 代理的开发者提供可落地的工程参考。

Agent Loop:自动化的核心机制

编码代理的本质是将语言模型的推理能力与真实世界的操作能力连接起来。在没有 Agent Loop 的情况下,每一次交互都需要人工中转:用户发送提示,模型返回 shell 命令,人工执行命令,再将结果粘贴回对话。这种模式对于涉及多个命令的任务而言效率极低,每个工具调用都意味着一次手动复制粘贴的循环。

Agent Loop 的核心价值在于将这个手动循环自动化。其工作流程可以概括为:用户发送一次提示,模型自主决定调用哪些工具、读取什么文件、运行什么命令,系统执行工具并返回结果,模型根据结果决定下一步行动,直到任务完成。整个过程由单一的退出条件控制 —— 当模型的 stopReason 不再是 toolUse 时,代理认为任务已完成。

这个机制的实现令人惊讶地简洁。整个 Agent Loop 可以在一个方法中完成:首先将用户查询追加到消息历史,然后进入循环,每次循环向 API 发送完整的对话历史加上工具定义,检查响应中的 stopReason,如果模型没有请求使用工具则返回内容,否则执行每个工具调用并将结果作为用户消息追加,最后回到循环顶部继续。这种设计的优雅之处在于:分支点只有一个,工具是变量而循环本身是常量。

双层循环架构:REPL 与 Agent 的职责分离

一个完整的 Swift 编码代理实际上包含两个相互嵌套的循环,每个循环承担不同的职责。外层是用户面向的 REPL 循环,它读取用户输入、调用 Agent 实例、打印结果,然后等待下一条指令。这个循环永久运行,每次迭代处理一个独立的用户查询。

内层循环才是真正的 Agent Loop,它负责与语言模型 API 的多轮交互。在每次 run() 调用中,Agent 可能会进行多次 API 调用、工具执行和结果处理,直到模型认为任务完成。关键的设计细节在于:消息数组必须持久化在 Agent 实例上,而不是每次 run() 调用时重新创建。如果将消息数组作为局部变量,每次调用都会导致代理失去记忆,无法维持多轮对话的上下文。

这种双层架构的优势在于职责清晰:REPL 处理用户交互和会话管理,Agent 负责任务执行和工具编排。两者通过 Agent.run(query:) 方法松耦合,只要消息历史正确维护,REPL 完全不需要关心 Agent 内部如何完成任务。

Shell 工具:单一工具的无限可能

在 Ivan Magda 的实现中,代理最初只配备了一个工具:bash。这个设计选择初看似乎过于简化,但实际上蕴含着深刻的工程洞察。bash 作为操作系统上的通用接口,能够执行几乎所有操作:文件读写、代码搜索、编译器调用、测试执行、包管理、Git 操作等。模型负责决定需要执行什么命令,系统只需执行并报告结果。

在 Swift 中实现 Shell 执行器需要使用 Foundation 的 Process 类。创建一个 Process 实例,配置其可执行文件路径为 /bin/bash,通过参数 -c 传递要执行的命令,设置标准输出和标准错误的管道,最后启动进程并读取输出。一个关键的工程细节是:管道数据必须在调用 waitUntilExit() 之前读取。Foundation 的 Pipe 使用内核缓冲区,大小通常约为 64 KB,如果命令输出超过这个容量,子进程会在 write() 上阻塞以等待缓冲区清空,而父进程在 waitUntilExit() 上等待子进程退出,形成经典的死锁。这个问题静默发生且难以诊断,因此在实现时必须特别注意读取顺序。

类型建模:Swift enum 的天然优势

由于官方没有提供 Swift 版的 Anthropic SDK,Ivan Magda 从头构建了必要的类型系统。其中最值得关注的建模决策是如何表示 API 的多态内容块。API 响应中的 content 数组包含多种类型的块:文本内容、工具调用请求、工具结果。Swift 的枚举与关联值特性为这种场景提供了完美的解决方案。定义一个 ContentBlock 枚举,包含 text(String)toolUse(id: String, name: String, input: JSONValue)toolResult(toolUseId: String, content: String, isError: Bool) 三个 case,每个 case 携带不同的关联值。这种建模方式既类型安全又表达力强,编译器能够确保所有分支都被正确处理。

工具输入是任意 JSON 格式,因此需要定义一个递归的 JSONValue 枚举来建模 JSON 的所有类型。这些支持类型大约需要 200 行代码来编写 Codable 遵循和 API 模型定义,虽然前期投入较大,但一旦完成就成为稳定的底层设施,后续的 Agent 逻辑无需再触碰这些类型。

消息累积与上下文管理

在 Agent Loop 的执行过程中,消息数组会不断增长。假设用户要求代理创建一个文件并验证内容,消息历史会经历以下阶段:用户查询、模型返回的 bash 命令、执行结果、模型验证命令、执行结果、模型确认完成。每一次工具调用都会在历史中增加一对请求和响应,模型能够看到自己做了什么以及发生了什么,从而做出下一步决策。

这种累积机制是代理具备记忆能力的基础,但代价也很明显:消息数组会无限增长,最终可能触及上下文窗口的上限。在当前阶段这不构成问题,但后续需要实现上下文压缩策略来处理长对话。这是一个后续优化方向,核心的 Agent Loop 机制本身不需要改变。

Swift 在 Agent 开发中的独特优势

Swift 为编码代理开发提供了几个显著优势。首先,Swift 的类型系统非常强大,枚举与关联值、Result 类型、可选值等特性使得 API 响应建模既安全又简洁。其次,Swift 的并发模型内置 async/await 和结构化并发,异步代码的编写和推理都比回调风格自然得多。第三,Swift 与 Apple 生态深度集成,对于需要与 iOS、macOS 项目交互的场景,Swift 是原生选择。第四,Swift 的性能表现优异,对于需要高频工具调用的代理场景,高效的执行效率很重要。

通过 Ivan Magda 的实践可以看到,从零构建一个功能完备的编码代理所需的核心代码量出乎意料地小。Agent Loop 本身只有一个方法,双层循环架构清晰,工具系统通过字典分发可以灵活扩展。这种简洁性使得 Swift 版代理不仅具有工程价值,也成为学习代理原理的理想实践平台。


资料来源:本文核心内容参考 Ivan Magda 的系列博客「Building a Coding Agent in Swift」第一部分,该系列详细记录了用 Swift 从零构建 AI 编码代理的完整工程实现。