【码动四季】AtomCode 源码编译与二次开发入门:从 GitHub 拉代码到自定义一个 Skill 引擎

摘要:AtomCode 开源了你看到了吗?大多数人只用了它的 CLI 能力,却没碰过源码。本文手把手带你完成 AtomCode 的源码编译、项目结构拆解、以及 3 个二次开发实战——添加自定义规则加载器、扩展 Skill 模板引擎的变量函数、接入本地 Ollama 模型。全程代码可复现。读完你不仅能自己编译 AtomCode,还能给它加功能。

1. 为什么需要自己编译 AtomCode

大多数人用 AtomCode 只需要一个命令 atomcode 就够了。但有 3 个场景必须走源码编译路线:

场景一:需要修改内置行为

AtomCode 的 Rules 加载顺序是按文件名排序的。如果你的团队希望按规则优先级(而不是按字母序)加载,就得改源码里的 rule_loader.go

场景二:需要接入内部模型

公司自建了 LLM 推理平台,API 格式和 OpenAI 不完全兼容。AtomCode 的 provider 层需要加一个自定义适配器。

场景三:需要调试疑难问题

Agent 执行到第三步就不动了,日志只显示 “step failed”。这时候看源码里的 agent_executor.go 比猜 log 含义高效得多。

本文基于 AtomCode v4.x 版本,GitHub 仓库地址在文末附注区。


2. 环境搭建与项目结构

2.1 编译环境要求

# 基础要求
Go >= 1.22
Node.js >= 20 (用于前端面板编译)
make
git

# 可选依赖
Docker (用于容器化编译)
gcc (用于 CGO 编译,部分平台需要)

验证环境:

$ go version
go version go1.22.4 darwin/arm64

$ node --version
v20.15.0

$ make --version
GNU Make 3.81

2.2 拉取源码

git clone https://github.com/atomgit/atomcode.git
cd atomcode

仓库体积约 180MB(含依赖和文档),国内用户建议配置 Go proxy:

export GOPROXY=https://goproxy.cn,direct

2.3 项目结构总览

拉完代码后,目录结构如下:

atomcode/
├── cmd/                      # CLI 入口
│   └── atomcode/
│       └── main.go           # 主程序入口
├── internal/                 # 核心逻辑(不对外暴露)
│   ├── rule/                 # 规则引擎
│   │   ├── loader.go         # 规则加载器
│   │   ├── matcher.go        # 规则匹配器
│   │   └── resolver.go       # 冲突解析器
│   ├── skill/                # 技能模块引擎
│   │   ├── engine.go         # 技能引擎核心
│   │   ├── template.go       # 模板渲染(变量替换)
│   │   └── registry.go       # 技能注册表
│   ├── agent/                # Agent 工作流引擎
│   │   ├── executor.go       # 步骤执行器
│   │   ├── pipeline.go       # 流水线编排
│   │   └── context.go        # 上下文管理
│   ├── provider/             # LLM 供应商适配
│   │   ├── openai.go         # OpenAI 兼容接口
│   │   ├── deepseek.go       # DeepSeek 适配
│   │   └── registry.go       # 供应商注册
│   └── config/               # 配置管理
│       ├── config.go         # 配置加载
│       └── defaults.go       # 默认值
├── pkg/                      # 可导出的公共包
│   ├── api/                  # HTTP API 定义
│   └── types/                # 公共类型
├── web/                      # Web 面板(可选)
│   ├── src/                  # React 前端
│   └── dist/                 # 编译产物
├── go.mod
├── go.sum
└── Makefile

核心目录说明

目录 作用 二次开发频率
internal/rule/ 规则加载和匹配逻辑 ⭐⭐⭐⭐⭐
internal/skill/ 技能模板引擎 ⭐⭐⭐⭐
internal/agent/ 工作流执行器 ⭐⭐⭐
internal/provider/ 模型供应商适配 ⭐⭐⭐⭐
internal/config/ 配置加载 ⭐⭐

use_skill

执行 Agent

代码辅助

用户输入

cmd/main.go

命令类型

skill/engine.go

agent/executor.go

provider 调用

rule/loader.go 加载上下文规则

skill/template.go 渲染模板

provider 生成

输出结果

步骤1: skill调用

步骤2: 命令执行

步骤3: 结果汇总

2.4 首次编译

# 开发模式编译(含调试符号)
make build-dev

# 生产模式编译(优化+strip)
make build

# 仅编译 CLI(不含 Web 面板,更快)
make build-cli

编译成功后,二进制文件在 bin/atomcode

$ ./bin/atomcode version
AtomCode v4.x (commit: a1b2c3d4, built: 2026-05-20T10:00:00Z)

踩坑提示:如果遇到 CGO_ENABLED=0 导致的构建失败,检查是否使用了需要 CGO 的依赖。AtomCode 核心不依赖 CGO,但某些平台特定的扩展需要。遇到问题先跑 make build-cli CGO_ENABLED=0


3. 二次开发实战一:自定义规则加载器

3.1 需求场景

AtomCode 默认的规则加载顺序是按文件名排序。团队有 20 条规则,希望按文件中定义的 priority 字段排序,而不是文件名。

3.2 定位源码

规则加载的核心在 internal/rule/loader.go

// internal/rule/loader.go (简化)
package rule

import (
    "os"
    "sort"
    "path/filepath"
)

// LoadRules 加载指定目录下的所有规则文件
func LoadRules(dir string) ([]*Rule, error) {
    files, err := filepath.Glob(filepath.Join(dir, "*.md"))
    if err != nil {
        return nil, err
    }
    
    // 默认按文件名排序
    sort.Strings(files)  // ← 这里是我们需要改的地方
    
    var rules []*Rule
    for _, f := range files {
        rule, err := ParseRuleFile(f)
        if err != nil {
            continue // 跳过解析失败的文件
        }
        rules = append(rules, rule)
    }
    return rules, nil
}

3.3 实现优先级排序

在规则文件中约定用 frontmatter 定义优先级:

---
priority: 10
scope: global
---

# API 命名规范

- RESTful 资源命名统一用复数名词

修改 loader.go,添加 frontmatter 解析和优先级排序:

// 在 ParseRuleFile 中增加 frontmatter 解析
func ParseRuleFile(path string) (*Rule, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    
    rule := &Rule{
        FilePath: path,
        FileName: filepath.Base(path),
    }
    
    // 解析 frontmatter(--- 分隔的 YAML 头)
    if bytes.HasPrefix(data, []byte("---")) {
        parts := bytes.SplitN(data[3:], []byte("---"), 2)
        if len(parts) == 2 {
            var meta struct {
                Priority int    `yaml:"priority"`
                Scope    string `yaml:"scope"`
            }
            if err := yaml.Unmarshal(parts[0], &meta); err == nil {
                rule.Priority = meta.Priority
                rule.Scope = meta.Scope
            }
            data = parts[1] // 去除 frontmatter 后的纯内容
        }
    }
    
    rule.Content = string(data)
    return rule, nil
}

// 修改 LoadRules,按优先级排序
func LoadRules(dir string) ([]*Rule, error) {
    files, err := filepath.Glob(filepath.Join(dir, "*.md"))
    if err != nil {
        return nil, err
    }
    
    var rules []*Rule
    for _, f := range files {
        rule, err := ParseRuleFile(f)
        if err != nil {
            continue
        }
        rules = append(rules, rule)
    }
    
    // 按优先级降序排列(高优先级在前)
    sort.Slice(rules, func(i, j int) bool {
        return rules[i].Priority > rules[j].Priority
    })
    
    return rules, nil
}

3.4 编译验证

# 修改后重新编译
make build-cli

# 验证规则加载顺序
./bin/atomcode debug rules --dir .atomcode/rules/

# 输出示例:
# [P:100] api-naming-rule.md       ← 优先级高的在前面
# [P:50]  security-rule.md
# [P:10]  code-style-rule.md
# [P:0]   legacy-rule.md           ← 未定义优先级的排最后

改动量:约 80 行代码,改动 2 个文件。从阅读源码到验证通过,约 1.5 小时。


4. 二次开发实战二:为 Skill 模板引擎添加自定义函数

4.1 需求场景

Skill 模板支持变量替换(如 ${resourceName}),但缺少字符串处理功能。希望在模板中能用 ${upper(name)} 把变量转大写,用 ${date()} 插入当前日期。

4.2 定位源码

模板引擎在 internal/skill/template.go

// internal/skill/template.go (简化)
package skill

import (
    "strings"
    "regexp"
)

var variablePattern = regexp.MustCompile(`\$\{([^}]+)\}`)

// Render 渲染模板,替换所有变量
func Render(template string, vars map[string]string) string {
    return variablePattern.ReplaceAllStringFunc(template, func(match string) string {
        key := match[2 : len(match)-1] // 去掉 ${ 和 }
        if val, ok := vars[key]; ok {
            return val
        }
        return match // 未找到的变量保持原样
    })
}

4.3 添加函数支持

扩展模板渲染引擎,支持 ${funcName(args)} 语法:

// 在 template.go 中添加函数注册表
var funcMap = map[string]func(args ...string) string{
    "upper": func(args ...string) string {
        if len(args) > 0 {
            return strings.ToUpper(args[0])
        }
        return ""
    },
    "lower": func(args ...string) string {
        if len(args) > 0 {
            return strings.ToLower(args[0])
        }
        return ""
    },
    "date": func(args ...string) string {
        format := "2006-01-02"
        if len(args) > 0 {
            format = args[0]
        }
        return time.Now().Format(format)
    },
    "default": func(args ...string) string {
        if len(args) >= 2 && args[0] == "" {
            return args[1]
        }
        if len(args) > 0 {
            return args[0]
        }
        return ""
    },
}

// 更新 Render 函数
func Render(template string, vars map[string]string) string {
    return variablePattern.ReplaceAllStringFunc(template, func(match string) string {
        inner := match[2 : len(match)-1]
        
        // 检查是否是函数调用:funcName(arg1, arg2)
        if idx := strings.Index(inner, "("); idx > 0 && strings.HasSuffix(inner, ")") {
            funcName := inner[:idx]
            argsStr := inner[idx+1 : len(inner)-1]
            args := strings.Split(argsStr, ",")
            for i := range args {
                args[i] = strings.TrimSpace(args[i])
                // 参数支持变量引用
                if val, ok := vars[args[i]]; ok {
                    args[i] = val
                }
            }
            if fn, ok := funcMap[funcName]; ok {
                return fn(args...)
            }
        }
        
        // 普通变量替换
        if val, ok := vars[inner]; ok {
            return val
        }
        return match
    })
}

4.4 在 Skill 中使用

配置好之后,Skill 模板就可以这样写:

# .atomcode/skills/release-notes/SKILL.md

# 版本发布说明生成 Skill

## 模板

# ${upper(productName)} v${version} 发布说明

发布日期:${date(2006-01-02 15:04)}

## 更新内容

${default(changelog, "本次无重大更新")}

调用时传入变量:

# 使用自定义函数
use_skill release-notes vars:productName=atomcode,version=4.2,changelog="修复了规则冲突问题"

改动量:约 60 行代码,改动 1 个文件。从需求到验证,约 1 小时。


5. 二次开发实战三:接入本地 Ollama 模型

5.1 需求场景

公司有内部 Ollama 服务,部署了 DeepSeek 蒸馏模型。希望 AtomCode 直接调用本地 Ollama,不经过公网。

5.2 定位源码

供应商适配在 internal/provider/ 目录下。OpenAI 兼容接口在 openai.go

// internal/provider/openai.go (简化)
package provider

import (
    "bytes"
    "encoding/json"
    "net/http"
)

type OpenAIProvider struct {
    apiKey  string
    baseURL string
    model   string
}

func NewOpenAIProvider(config ProviderConfig) *OpenAIProvider {
    return &OpenAIProvider{
        apiKey:  config.APIKey,
        baseURL: config.BaseURL,
        model:   config.Model,
    }
}

func (p *OpenAIProvider) Chat(messages []Message) (*Response, error) {
    // 调用 OpenAI 兼容 API
    body := map[string]interface{}{
        "model":    p.model,
        "messages": messages,
    }
    
    jsonData, _ := json.Marshal(body)
    req, _ := http.NewRequest("POST", p.baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
    req.Header.Set("Authorization", "Bearer "+p.apiKey)
    req.Header.Set("Content-Type", "application/json")
    
    // ... 发送请求和解析响应
}

5.3 添加 Ollama 适配器

Ollama 的 API 和 OpenAI 有差异——不需要 API Key,请求格式略有不同。创建一个新文件 ollama.go

// internal/provider/ollama.go
package provider

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type OllamaProvider struct {
    baseURL string
    model   string
}

func NewOllamaProvider(config ProviderConfig) *OllamaProvider {
    baseURL := config.BaseURL
    if baseURL == "" {
        baseURL = "http://localhost:11434"
    }
    return &OllamaProvider{
        baseURL: baseURL,
        model:   config.Model,
    }
}

// Ollama 的聊天请求格式
type OllamaChatRequest struct {
    Model    string          `json:"model"`
    Messages []OllamaMessage `json:"messages"`
    Stream   bool            `json:"stream"`
}

type OllamaMessage struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

// Ollama 的聊天响应格式
type OllamaChatResponse struct {
    Message OllamaMessage `json:"message"`
    Done    bool         `json:"done"`
}

func (p *OllamaProvider) Chat(messages []Message) (*Response, error) {
    // 转换消息格式
    var ollamaMsgs []OllamaMessage
    for _, m := range messages {
        ollamaMsgs = append(ollamaMsgs, OllamaMessage{
            Role:    m.Role,
            Content: m.Content,
        })
    }
    
    reqBody := OllamaChatRequest{
        Model:    p.model,
        Messages: ollamaMsgs,
        Stream:   false,
    }
    
    jsonData, _ := json.Marshal(reqBody)
    url := fmt.Sprintf("%s/api/chat", p.baseURL)
    
    resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, fmt.Errorf("ollama request failed: %w", err)
    }
    defer resp.Body.Close()
    
    var ollamaResp OllamaChatResponse
    if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
        return nil, fmt.Errorf("ollama response parse failed: %w", err)
    }
    
    return &Response{
        Content: ollamaResp.Message.Content,
    }, nil
}

// 注册到供应商工厂
func init() {
    RegisterProvider("ollama", func(config ProviderConfig) Provider {
        return NewOllamaProvider(config)
    })
}

5.4 在供应商注册表中注册

打开 internal/provider/registry.go,确保 init() 函数导入了新适配器:

// internal/provider/registry.go
package provider

import (
    _ "github.com/atomgit/atomcode/internal/provider/ollama" // 导入触发 init
)

// GetProvider 根据名称返回供应商实例
func GetProvider(name string, config ProviderConfig) (Provider, error) {
    factory, ok := providers[strings.ToLower(name)]
    if !ok {
        return nil, fmt.Errorf("unknown provider: %s", name)
    }
    return factory(config), nil
}

5.5 配置和使用

.atomcode/config.yaml 中配置:

provider: ollama
ollama:
  base_url: http://192.168.1.100:11434
  model: deepseek-coder-v2:16b

openai

deepseek

ollama

自定义

AtomCode CLI

provider 选择

api.openai.com

api.deepseek.com

localhost:11434

企业内部 API

改动量:约 100 行代码,新增 1 个文件,修改 1 个文件。从开发到测试,约 2 小时。


6. 编译与调试技巧

6.1 增量编译

开发时不建议每次都全量编译,用 go run 直接跑:

# 直接运行(不生成二进制)
go run ./cmd/atomcode use_skill csdn-article-writer

# 配合热重载(安装 air)
air -- cmd/atomcode use_skill csdn-article-writer

6.2 调试日志

AtomCode 内部有分层日志,编译时开启 debug:

# 编译 debug 版本
make build-dev

# 运行并查看详细日志
ATOMCODE_DEBUG=1 ./bin/atomcode use_skill csdn-article-writer

# 只查看某个模块的日志
ATOMCODE_DEBUG=skill,rule ./bin/atomcode use_skill csdn-article-writer

日志输出示例:

[DEBUG] rule/loader.go:42 加载规则文件: .atomcode/rules/csdn-article-creation-rule.md
[DEBUG] rule/matcher.go:18 匹配规则: scope=csdn, path=knowledge/csdn/test.md → 命中
[DEBUG] skill/engine.go:55 渲染模板: skill=csdn-article-writer, vars=3
[DEBUG] skill/template.go:120 替换变量: ${title} → "AtomCode 源码编译"
[DEBUG] provider/openai.go:88 请求模型: deepseek-v4-flash, tokens=2847

6.3 单元测试

# 运行所有测试
make test

# 只运行某个模块的测试
go test ./internal/rule/...
go test ./internal/skill/...
go test ./internal/provider/...

# 带覆盖率
go test -coverprofile=coverage.out ./internal/...
go tool cover -html=coverage.out -o coverage.html

6. 二次开发全流程示意图

Why:经过前面 3 个实战案例(规则加载器→模板函数→Ollama 适配器),你可能对单个环节的修改已经熟悉了。但完整的二次开发不仅仅是在 IDE 里改代码——从发现需求到提交 PR,中间涉及源码下载、环境搭建、定位修改、编译验证、提交 PR 五个关键阶段。下面这张流程图帮你建立「端到端」的全局视角,避免在某个环节卡住后不知道下一步该做什么。

需修改

通过

发现需求或 Bug

Fork 官方仓库

git clone 到本地

搭建编译环境

Go 版本 ≥ 1.22

make build-cli 验证

安装 Go 1.22+

阅读对应模块源码

找到修改点?

加 Debug 日志重新编译

本地修改代码

go test 单元测试

测试通过?

make build 全量编译

集成验证

功能符合预期?

提交 commit & 推送

发起 Pull Request

Code Review 通过?

按 Review 意见修改

PR 合入主仓库

该流程图覆盖了本文 3 个实战案例的通用步骤。你在做自己的二次开发时,可以对照这个流程检查当前处于哪个阶段——如果「编译验证」反复失败,优先回看第 7 节的踩坑指南;如果不确定改哪个文件,参考第 2.3 节的项目结构总览。


7. 常见踩坑

坑 1:Go 版本不匹配

AtomCode v4.x 要求 Go 1.22+。用低版本编译会报语法错误,比如 range over int 是 1.22 才支持的。

解决go version 确认版本。用 go.mod 中的 go 1.22 行判断最低要求。

坑 2:Web 面板编译失败

如果不需要 Web 面板,用 make build-cli 跳过前端编译。默认 make build 会尝试编译 React 前端,需要 Node.js 和 npm。

解决make build-cli 只编译 CLI,不依赖 Node.js。

坑 3:国内依赖下载慢

go mod tidy 时部分依赖从 GitHub 下载慢。

解决:配置 Go proxy:GOPROXY=https://goproxy.cn,direct。如果某个依赖仍然慢,手动 go get -v 单个包看卡在哪里。


8. 总结与扩展方向

8.1 你学会了什么

能力 改动量 难度 耗时
源码编译 AtomCode 0(直接编译) 10 分钟
自定义规则加载器 ~80 行 / 2 文件 ⭐⭐ 1.5 小时
Skill 模板函数扩展 ~60 行 / 1 文件 ⭐⭐ 1 小时
接入 Ollama 模型 ~100 行 / 2 文件 ⭐⭐⭐ 2 小时

8.2 更多可以尝试的方向

  • 自定义 Agent 步骤类型:当前 Agent 只支持 skillcommand 两种步骤,可以扩展支持 http_requestdatabase_query
  • 规则冲突自动检测:在 internal/rule/resolver.go 中添加规则冲突检测逻辑,输出冲突报告
  • Token 用量统计:在 provider 调用层增加 token 计数和成本统计,输出到日志
  • 自定义输出格式:修改 cmd/atomcode/main.go 中的输出渲染,支持 JSON/YAML 格式导出

8.3 参与的姿势

你的 Go 水平 推荐起点 预期投入
刚入门 编译 + 读代码注释 2 小时
能写简单功能 修改配置加载、添加日志 半天
熟悉 Go 项目 添加新 provider、扩展模板函数 1-2 天
想深度贡献 规则冲突检测、Agent 步骤扩展 1 周

更多推荐