【码动四季】AtomCode 源码编译与二次开发入门:从 GitHub 拉代码到自定义一个 Skill 引擎
【码动四季】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/ |
配置加载 | ⭐⭐ |
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
改动量:约 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 五个关键阶段。下面这张流程图帮你建立「端到端」的全局视角,避免在某个环节卡住后不知道下一步该做什么。
该流程图覆盖了本文 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 只支持
skill和command两种步骤,可以扩展支持http_request、database_query等 - 规则冲突自动检测:在
internal/rule/resolver.go中添加规则冲突检测逻辑,输出冲突报告 - Token 用量统计:在 provider 调用层增加 token 计数和成本统计,输出到日志
- 自定义输出格式:修改
cmd/atomcode/main.go中的输出渲染,支持 JSON/YAML 格式导出
8.3 参与的姿势
| 你的 Go 水平 | 推荐起点 | 预期投入 |
|---|---|---|
| 刚入门 | 编译 + 读代码注释 | 2 小时 |
| 能写简单功能 | 修改配置加载、添加日志 | 半天 |
| 熟悉 Go 项目 | 添加新 provider、扩展模板函数 | 1-2 天 |
| 想深度贡献 | 规则冲突检测、Agent 步骤扩展 | 1 周 |
更多推荐



所有评论(0)