如何开发一个可以在命令行执行的Coding Agent

今年以来智能体的热度一直是居高不下的,尤其是编码相关的智能体广受大家的关注。现在市面上最火的编码智能体主要是Claude CodeOpenai 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,一起共建。

相关推荐
struggle2025几秒前
PINA开源程序用于高级建模的 Physics-Informed 神经网络
人工智能·深度学习·神经网络
19894 分钟前
【Dify精讲】第14章:部署架构与DevOps实践
运维·人工智能·python·ai·架构·flask·devops
微信公众号:AI创造财富7 分钟前
文生视频(Text-to-Video)
开发语言·人工智能·python·深度学习·aigc·virtualenv
struggle20257 分钟前
Z-Ant开源程序是简化了微处理器上神经网络的部署和优化
人工智能·深度学习·神经网络
NetX行者15 分钟前
Wordvice AI:Wordvice 推出的免费,基于先进的 AI 技术帮助用户提升英文写作质量
人工智能·ai工具
倔强青铜三26 分钟前
🚀LlamaIndex中文教程(1)----对接Qwen3大模型
人工智能·后端·python
程序员寒山28 分钟前
Ai工具之DeepSiteV2(1):「边聊边建」的智能辅助建站神器
人工智能
AI大模型技术社29 分钟前
工业级Transformer优化手册:混合精度训练+量化部署实战解析
人工智能·llm
ProcessOn官方账号29 分钟前
数据分析对比图表-雷达图全面指南
大数据·人工智能·程序人生·职场和发展·数据分析·学习方法·processon
知其然亦知其所以然34 分钟前
Spring AI:ChatClient API 真香警告!我用它把聊天机器人卷上天了!
后端·aigc·ai编程