如何开发一个可以在命令行执行的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,一起共建。

相关推荐
实在智能RPA4 分钟前
实在 Agent 和通用大模型有什么不一样?深度拆解 AI Agent 的感知、决策与执行逻辑
人工智能·ai
独隅8 分钟前
PyTorch 模型部署的 Docker 配置与性能调优深入指南
人工智能·pytorch·docker
lihuayong15 分钟前
OpenClaw 系统提示词
人工智能·prompt·提示词·openclaw
黑客说29 分钟前
AI驱动剧情,解锁无限可能——AI游戏发展解析
人工智能·游戏
踩着两条虫35 分钟前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
小仙女的小稀罕40 分钟前
听不清重要会议录音急疯?这款常见AI工具听脑AI精准转译
开发语言·人工智能·python
reesn1 小时前
qwen3.5 0.8B纠正任务实践
人工智能·语言模型
实在智能RPA1 小时前
实在Agent 制造业落地案例:探寻工业大模型从实验室走向车间的实战路径
人工智能·ai
阿酷tony1 小时前
Nano Banna 提示词:创意超逼真的3D商业风格产品图
人工智能·3d·gemini·图片生成