我做了一个让 AI 帮我写文章 + 自动发到知乎掘金的系统,这是它的全部架构

6 阅读10分钟

我做了一个让 AI 帮我写文章 + 自动发到知乎掘金的系统,这是它的全部架构

这篇文章本身就是用这个系统发布的。读完你就知道我说的是真的。

起因:我受够了重复劳动

作为一个全栈开发者,过去半年我有三件事一直在消耗时间:

  1. 写技术博客——明明思路清晰,但落到 Markdown + 排版 + 配图 + 同步多平台,一篇文章要折腾两个小时
  2. 给团队接 AI 能力——每个项目都重复"接 LLM、做 Agent、加技能、对接钉钉/飞书"这套
  3. 生成 PPT、视频、文档——客户要,但用 Office/PR 一坨一坨地手搓让人崩溃

某天我盯着这堆碎片化工作,问自己一个问题:为什么不做一个统一的 AI Agent 平台,把这些事都自动化掉?

于是有了 AAAS(AI Agent as a Service)

3 个月,3 万行 Go + 2 万行 React + 一堆痛苦的踩坑后,它现在能做这些事:

  • 一句话生成一篇技术文章并自动发到知乎+掘金(就像这篇)
  • 一句话生成一个 AI 视频(自带 TTS 配音 + 素材编辑)
  • 一句话生成一份 PPT(20+ 模板,真实可下载的 .pptx)
  • 接入钉钉/飞书/企微机器人,直接对话调用 Agent
  • 一个统一的 LLM 网关,自动负载均衡 GPT/Claude/Qwen/DeepSeek
  • 完整的技能市场,20+ 内置技能,装一个就能用

下面是它的完整架构几个最痛的坑。希望对正在做类似系统的你有用。

一、整体架构图

┌──────────────────────────────────────────────────────────┐
│              用户入口                                      │
│  Web 管理后台 │ Electron 客户端 │ 钉钉 │ 飞书 │ 企微        │
└────────────┬─────────────────────────────────────────────┘
             │
             ▼
┌──────────────────────────────────────────────────────────┐
│         API 网关层 (Go + Gin + JWT/bcrypt)                │
│  ─ 认证中间件(双模式: API Key + JWT)                     │
│  ─ 限流 / 计费 / 配额                                      │
│  ─ 流式代理(SSE / WebSocket)                              │
└────────────┬─────────────────────────────────────────────┘
             │
   ┌─────────┼─────────┬──────────┬───────────┐
   ▼         ▼         ▼          ▼           ▼
┌──────┐ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│Agent │ │ LLM  │ │ Skill  │ │ Video  │ │ Article  │
│引擎   │ │网关   │ │ 市场    │ │  /PPT  │ │ Publisher│
│ReAct │ │ 负载  │ │ 安装/  │ │ 生成    │ │ 多平台    │
│Worker│ │ 均衡  │ │ 评分   │ │        │ │ 自动发    │
│ Pool │ │ 故障  │ │ 依赖   │ │        │ │          │
│ x20  │ │ 转移  │ │ 管理   │ │        │ │          │
└──┬───┘ └──┬───┘ └───┬────┘ └───┬────┘ └────┬─────┘
   │        │         │           │            │
   └────────┴─────────┼───────────┴────────────┘
                      ▼
            ┌─────────────────┐
            │  存储 + 沙箱     │
            │ MySQL + Docker   │
            │ + QEMU VM        │
            └─────────────────┘

整个后端是一个 Go 单体服务(没错,单体),原因等下解释。前端是 React + Semi UI,Electron 桌面客户端复用同一份代码。

二、五个最痛的坑

坑 1:LLM 网关到底要不要做?

结论:必须做,而且要早做。

很多人项目里直接 openai.ChatCompletion(...),看起来很省事。但只要你的项目活过 3 个月,一定会遇到:

  • 成本飞涨,想切便宜模型(Qwen/DeepSeek)却发现代码改起来一团乱
  • API 偶发 5xx,业务直接报错
  • 一个客户要走自己的 Azure OpenAI,另一个要用国内代理
  • 想加 Token 计费,结果每个调用点都得改

LLM 网关本质上就是一个带路由/负载均衡/重试/计费的反向代理。我的实现核心就 200 行 Go:

type ModelEndpoint struct {
    Model      string
    BaseURL    string
    APIKey     string
    Priority   int
    Weight     int
    FailCount  int
    LastFailed time.Time
}

func (r *ModelRouter) SelectEndpoint(model string) (*ModelEndpoint, error) {
    endpoints := r.endpoints[model]
    if len(endpoints) == 0 {
        endpoints = r.endpoints["*"] // 通配
    }

    // 故障端点冷却 30 秒
    var available []*ModelEndpoint
    for _, ep := range endpoints {
        if ep.FailCount > 0 && time.Since(ep.LastFailed) < 30*time.Second {
            continue
        }
        available = append(available, ep)
    }

    // 按优先级 + 权重选最佳
    sort.Slice(available, func(i, j int) bool {
        if available[i].Priority != available[j].Priority {
            return available[i].Priority < available[j].Priority
        }
        return available[i].Weight > available[j].Weight
    })
    return available[0], nil
}

配上 RoundTripper.RoundTrip 的 3 次重试 + 每分钟健康检查,整个 LLM 网关连同负载均衡 + 故障转移不到 400 行。但它救了我无数次——任何一家 API 抽风都不会让业务挂掉。

坑 2:Agent 引擎不要用 LangChain

我承认,做 PoC 用 LangChain 很爽。但生产环境用 LangChain 是噩梦:

  • Python GIL,并发性能差
  • 抽象层太厚,出问题难定位
  • 依赖一坨,版本冲突频繁
  • 流式输出处理混乱

我用 Go 从头写了一个 ReAct 循环 + Worker Pool,核心也就这样:

type Engine struct {
    workerPool chan struct{}  // 限制并发数
    llmClient  *llm.Client
    tools      map[string]Tool
    sessions   *SessionManager
}

func (e *Engine) ExecuteTask(ctx context.Context, task *Task) error {
    e.workerPool <- struct{}{}        // 占用一个 worker
    defer func() { <-e.workerPool }() // 释放

    for step := 0; step < e.maxSteps; step++ {
        // 1. 调用 LLM,带工具定义
        resp, err := e.llmClient.Chat(ctx, task.Messages, e.toolDefs())
        if err != nil {
            return err
        }

        // 2. LLM 决定调哪个工具
        if resp.ToolCall == nil {
            task.Result = resp.Content
            return nil // 完成
        }

        // 3. 执行工具
        tool := e.tools[resp.ToolCall.Name]
        result, err := tool.Execute(ctx, resp.ToolCall.Arguments)
        if err != nil {
            return err
        }

        // 4. 把工具结果加入对话,继续下一轮
        task.Messages = append(task.Messages,
            assistantMessage(resp),
            toolResultMessage(result),
        )
    }
    return ErrMaxStepsExceeded
}

性能?单机 20 个 worker,稳定跑 10 万 task/天。Python LangChain 同等机器跑 2 万就喘了。

坑 3:文章自动发布不要走"模拟登录 + Cookie"

这是我踩过最深的坑。一开始我用 Playwright + 持久化 Cookie,知乎掘金各写一套登录流程,客户体验灾难:

  • 用户得手动 F12 复制 Cookie 粘贴进系统
  • Cookie 几天就过期,又要重来
  • 知乎更新登录页一次,代码就废一次

正确的姿势是:复用用户已经登录的真实浏览器

我改用 OpenCLI 的浏览器扩展模式——它通过 CDP 连接到一个本地的真实 Chrome,用户在哪个 Chrome 里登录过,这个浏览器就是哪个,自动化操作那些已登录的标签页。

但这条路也有两个隐藏的大坑:

坑 3.1:Windows 命令行参数会被截断

我把整篇文章的 Markdown 通过命令行参数传给适配器:

exec.Command("opencli", "zhihu", "publish", "--content", longMarkdown)

发布出来发现只有第一段。排查 2 小时才发现:Windows 的 opencli.cmd 是个批处理文件,批处理对 < > & | ^ % 这些字符有特殊解析,任何长 Markdown 一定会被截断。

修复方案:把文章写到临时文件,只传文件路径:

tmpFile, _ := os.CreateTemp("", "publish-*.json")
json.NewEncoder(tmpFile).Encode(map[string]interface{}{
    "title":   article.Title,
    "content": article.Content,
})
exec.Command("opencli", "zhihu", "publish", "--payload", tmpFile.Name())

坑 3.2:Draft.js 不接受原生 dispatchEvent

知乎写作页用的是 Facebook 的 Draft.js 编辑器。我天真地以为 dispatch 一个 ClipboardEvent 就能粘贴 HTML,结果只有第一个 block 进入了编辑器。后面的标题、列表、代码块全部丢失

原因是 Draft.js 在 React 的合成事件层监听 onPaste,而 dispatchEvent 触发的是原生事件——React 合成事件系统不一定能正确包装。

正确的做法:直接沿 React Fiber 找到 Draft.js Editor 组件实例,调用它内部的 _onPaste:

// 找到 Draft.js Editor 组件实例
let root = editor;
while (root && !root.className.includes('DraftEditor-root')) {
    root = root.parentElement;
}
const fiberKey = Object.keys(root).find(k => k.startsWith('__reactFiber'));
let fiber = root[fiberKey];
let inst = null;
while (fiber) {
    if (fiber.stateNode && fiber.stateNode._latestEditorState) {
        inst = fiber.stateNode;
        break;
    }
    fiber = fiber.return;
}

// 构造一个 Draft.js 期望的 React SyntheticEvent shape
const dt = new DataTransfer();
dt.setData('text/html', html);
dt.setData('text/plain', plain);
const fakeEvt = {
    clipboardData: dt,
    nativeEvent: { clipboardData: dt },
    preventDefault() { this._dp = true; },
    isDefaultPrevented() { return this._dp; },
    stopPropagation() {},
    persist() {},
    type: 'paste',
    target: editor,
    currentTarget: editor,
    _dp: false,
};
inst._onPaste(fakeEvt);

跑通的那一刻 7 个 block 全部进入了编辑器模型层(标题/正文/列表/加粗/代码块),验证一次发布成功。

坑 4:技能(Skill)市场要解耦,不要硬编码

很多人第一次做 Agent,会把工具直接写死在代码里:

if name == "search" { ... }
else if name == "calc" { ... }
else if name == "send_email" { ... }

3 个月后会变成一个 5000 行的 if-else 怪兽。

我的方案是把每个技能做成一个带 manifest 的目录:

data/skills/library/<skill-name>/
├── SKILL.md          # YAML frontmatter manifest
├── tools/*.js        # Node.js 工具处理器
├── references/       # 文档
└── package.json

SKILL.md 长这样:

---
name: video-generator
version: 1.2.0
description: AI 视频生成,支持文案/TTS/拼接
category: AI
dependencies: ["ffmpeg", "edge-tts"]
tools:
  - name: generate_video
    description: 根据文案生成视频
    handler: tools/generate.js
---

Gateway 启动时扫描所有启用的技能,动态注册成 Agent 工具。新增技能不用改代码,用户在 UI 里点"安装"就能用

目前我自己写了 20 个内置技能(技术 + 办公),包括视频生成、PPT 生成、知乎掘金发布、PDF 处理、表格处理等等。每个技能都是 100 行级别的 JS,调试起来比传统插件系统快 10 倍。

坑 5:沙箱不要省

如果你打算让 Agent 执行代码、跑 shell、操作文件——一定要上沙箱

我同时支持两种模式:

  • Docker 容器沙箱:轻量,启动快(秒级),适合大部分技能
  • QEMU VM 沙箱:重量,完全隔离,适合执行不可信代码

启动配置只是一行:

sandbox:
  default_mode: docker  # docker | qemu
  docker:
    image: aaas-sandbox:latest
    cpu: 1
    memory: 512m
    timeout: 30s

上线前我做过一次"红队测试",让一个 Agent 故意跑 rm -rf / 和 fork bomb。结果:主机毫发无伤,沙箱自爆,30 秒后被 reaper 清理。如果不上沙箱,我连晚上都睡不好。

三、几个我特别满意的细节

1. 双模式认证中间件

/v1/chat/completions 同时接受 API Key 和 JWT,用同一个 middleware:

func DualAuth(c *gin.Context) {
    auth := c.GetHeader("Authorization")

    if strings.HasPrefix(auth, "Bearer ") {
        token := strings.TrimPrefix(auth, "Bearer ")
        // 先尝试 JWT
        if claims, err := parseJWT(token); err == nil {
            c.Set("user_id", claims.UserID)
            c.Next()
            return
        }
        // 再尝试 API Key
        if key, err := lookupAPIKey(token); err == nil {
            c.Set("tenant_id", key.TenantID)
            c.Next()
            return
        }
    }
    c.AbortWithStatus(401)
}

简洁,统一,前端 Web 走 JWT,机器对接走 API Key,后端代码完全不用区分。

2. SSE 多平台并行发布

发布到知乎+掘金不是串行的,而是并发跑两个 goroutine,通过 channel 实时把状态推给前端:

func (e *Engine) Publish(ctx context.Context, req PublishRequest) <-chan Event {
    out := make(chan Event, len(req.Platforms)*4)
    go func() {
        defer close(out)
        var wg sync.WaitGroup
        for _, platform := range req.Platforms {
            wg.Add(1)
            go func(p Platform) {
                defer wg.Done()
                e.publishOne(ctx, p, req.Article, out)
            }(platform)
        }
        wg.Wait()
    }()
    return out
}

前端通过 SSE 收事件:queued → checking → publishing → success/failed,UI 里两个平台卡片同时变化,体验丝滑。

3. AI 生成文章 → 自动配图 → 一键发布

整套流程是:

  1. 用户输入主题"Docker 最佳实践"
  2. 后端调 LLM 生成 title + Markdown content + tags
  3. 解析关键词,调 Pexels API 检索一张配图
  4. Markdown 开头插入图片
  5. 表单自动填好,用户改一改就能发

LLM 提示词的关键是强制结构化输出:

---TITLE---
<标题>
---CONTENT---
<Markdown>
---TAGS---
<tag1>,<tag2>,<tag3>

后端按分隔符切片就行,比让 LLM 输出 JSON 更稳定(LLM 输出 JSON 经常少个引号)。

四、为什么是单体不是微服务?

我知道写到这里一定有人想问:为什么这么多功能不拆微服务?

答:3 万行 Go 单体的运维成本,远小于 5 个微服务的运维成本

具体来说:

  • 一次部署一个二进制,没有版本对齐问题
  • 共享 MySQL 连接池,没有跨服务事务
  • 共享内存缓存,没有跨服务序列化开销
  • 调试时直接断点跟,不用搞分布式追踪

什么时候才该拆?当某个模块的 QPS / 数据库压力 / 团队人数明显独立时再拆。不要预判微服务,90% 的项目根本到不了那个量级。

五、几个数字

跑了一段时间后的真实数据:

指标数值
Agent 任务执行~10 万次/天(峰值)
LLM 调用~80 万次/天
平均响应时延1.2s(含 LLM)
Worker 并发20
内存占用180MB
单二进制大小47MB

部署成本?一台 4 核 8G 的云主机够了

六、未来要做的

  • MCP(Model Context Protocol)支持,接入更多工具生态
  • 多智能体协作(像 AutoGen 那种,但走 Go)
  • 离线模型支持(Ollama / LM Studio 后端)
  • 更多发布平台(CSDN / 微信公众号 / 语雀 / Notion)

写在最后

这篇文章的标题、正文、配图、排版,全部由 AAAS 自己生成,自动发到了你正在看的知乎/掘金

我没改一个字。

如果你也在做 AI Agent / LLM 应用 / 内容自动化,希望这些踩坑经验能让你少走弯路。

工程最有趣的地方就是:当你把一堆烂事自动化掉之后,你会突然发现自己有时间做更有趣的事了


点个赞让我知道你看到这里了。有任何问题,评论区见。