今年以来智能体的热度一直是居高不下的,尤其是编码相关的智能体广受大家的关注。现在市面上最火的编码智能体主要是Claude Code
、Openai CodeX
以及谷歌最近发布的Jules
,可以看出他们都是国外大厂发布的,需要一些魔法才能使用。那如何开发一款在终端运行的Coding Agent
呢?
本文相关的代码我已经发布到GitHub仓库Synapse,欢迎大家star
,一起共建。
什么是Coding Agent?
智能体(Agent): 是指能够感知环境并采取行动以实现特定目标的实体。它可以是软件程序、机器人或其他形式的自动化系统。
Coding Agent:是一种专门用于编程任务的智能体,它能够在软件开发过程中根据环境中的工具,执行相应的操作,去辅助用户做一些功能,如代码生成、调试、优化等。
- 应用场景 :
- 代码生成:根据自然语言描述自动生成代码。
- 代码补全:在编写代码时提供实时的建议和补全。
- 错误检测与修复:识别代码中的错误并提供修复建议。
- 文档生成:自动生成代码注释和文档。
- 测试用例生成:为代码生成单元测试用例。
整体方案
从上面对智能体相关的概念阐述,可以简单的理解Coding Agent
就是给大模型一些工具,让它基于上下文去使用工具处理问题。那基于此,我们要开发的agent就要能处理以下问题:
- 接受用户请求;
- 能连接大模型;
- 提供tools;
- 输出大模型的响应;
- 模型能根据调用tool决定什么时候结束任务。
再进一步分析,我们对主要逻辑模块进行分层,可以归纳为以下:
用户界面层 (User Interface Layer)
Command-Line Interface (CLI) : 这是用户与 Coding Agent
交互的唯一入口。
- 职责: 接收用户输入、处理本地命令(如 /add, /reset)、启动和停止加载动画、打印 AI 的流式输出。
- 特点: 它是"哑"的,不关心业务逻辑,只负责输入和输出,这使得未来替换成 Web UI 或其他界面变得容易。
核心逻辑层 (Core Logic Layer)
这是整个应用的大脑和状态中心。
- 职责: 管理"思考-行动"循环、调用 LLM 和工具、维护整个对话流程。
- 特点: 它通过接口与下一层(服务层)交互,而不是具体的实现。这是实现可扩展性的关键。
服务与抽象层 (Service & Abstraction Layer)
这一层是解耦的关键,它定义了"能力"的接口,而不关心如何实现。
- LLM Provider Interface: 定义了一个 AI 模型提供商必须具备的能力。Agent 只依赖这个接口。
- Tool Executor: 定义了执行一个工具的单一入口 (Execute)。Agent 只需告诉它工具的名字和参数,无需关心工具箱里具体有什么。
实现层 (Implementation Layer)
这一层是具体实现者,负责与外部世界打交道。
- Client: 它们分别实现了 LLM Provider 接口,封装了与各自 API 端点进行 HTTP 通信的细节。
- Tool Registry & Impl.: registry.go: 维护一个所有可用工具的"注册表"(map)。 file_tools.go: 提供了 read_file, edit_file 等函数的具体实现,直接调用 Go 的 os 包来操作文件。
核心代码
模型响应
go
func (a *Agent) handleStreaming(ctx context.Context, outputChan chan<- string) {
defer close(outputChan)
// 循环直到获得最终的文本响应,或者发生不可恢复的错误
// 添加一个循环次数限制,防止无限循环
const maxTurns = 10
for i := 0; i < maxTurns; i++ {
a.session.TrimHistory()
req := llm.ChatCompletionRequest{
// Model 字段不在这里设置,让 provider 来决定默认值
Messages: a.session.GetHistory(),
Tools: a.llmProvider.GetTools(),
Stream: true,
}
stream, err := a.llmProvider.CreateChatCompletionStream(ctx, req)
if err != nil {
// **修复 #1: 使用 fmt.Sprintf 然后发送到 channel**
errorMsg := fmt.Sprintf("API Error: %v", err)
outputChan <- ui.Red(errorMsg) // 使用 UI 包来给错误上色
return
}
var fullResponse strings.Builder
var accumulatedToolCalls []llm.ToolCall
for {
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
// **修复 #2: 使用 fmt.Sprintf 然后发送到 channel**
errorMsg := fmt.Sprintf("Stream Error: %v", err)
outputChan <- ui.Red(errorMsg)
stream.Close()
return
}
delta := response.Choices[0].Delta
if delta.Content != "" {
fullResponse.WriteString(delta.Content)
// 将正常的 token 直接发送出去
outputChan <- delta.Content
}
if len(delta.ToolCalls) > 0 {
accumulatedToolCalls = accumulateToolCalls(accumulatedToolCalls, delta.ToolCalls)
}
}
stream.Close()
// 记录助手的回复,即使是空的,也要记录工具调用
a.session.AddAssistantMessage(fullResponse.String(), accumulatedToolCalls)
if len(accumulatedToolCalls) > 0 {
// 如果有工具调用,执行它们并继续循环
executeToolCalls(a.session, accumulatedToolCalls, outputChan)
continue
}
// 没有工具调用,这是对话的终点,循环结束
return
}
// 如果循环达到最大次数,发送一个警告信息
outputChan <- ui.Yellow("\nWarning: Maximum conversation turns reached.")
}
终端响应
go
func runCLI(coreAgent *agent.Agent) {
ui.PrintWelcomeMessage()
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Printf("%s ", ui.Blue("🔵 You>"))
if !scanner.Scan() {
break
}
userInput := strings.TrimSpace(scanner.Text())
if len(userInput) == 0 {
continue
}
lowerInput := strings.ToLower(userInput)
if lowerInput == "exit" || lowerInput == "quit" {
fmt.Println(ui.BrightCyan("👋 Goodbye!"))
break
}
if handled := handleLocalCommand(userInput, coreAgent); handled {
continue
}
// 在调用 agent 之前,启动加载动画
ui.StartSpinner("Thinking...")
ctx := context.Background()
responseChan, err := coreAgent.ProcessUserMessage(ctx, userInput)
if err != nil {
// 如果 agent 立即返回错误,也要停止动画
ui.StopSpinner()
log.Printf(ui.Red("Error processing message: %v"), err)
continue
}
// 我们需要一个变量来跟踪是否已经打印了助手的头部信息
assistantPrefixPrinted := false
// 在循环从 channel 读取数据之前,停止动画
// 但是,我们需要确保在打印任何内容之前停止它。
// 最好的时机是在我们收到第一个 token 之后。
for token := range responseChan {
if !assistantPrefixPrinted {
// 这是我们收到的第一个 token
// 在打印它之前,停止动画并打印助手的前缀
ui.StopSpinner()
ui.PrintAssistantPrefix()
assistantPrefixPrinted = true
}
// 打印收到的 token
fmt.Print(ui.Green(token))
}
// 确保即使 channel 为空(例如只有工具调用,没有文本输出),动画也能被停止
if !assistantPrefixPrinted {
ui.StopSpinner()
}
fmt.Println() // 在每次对话结束后换行
}
}
实现效果


小结
以上就是本次分享的全部内容,相关的代码我已经发布到GitHub仓库 Synapse ,欢迎大家star
,一起共建。