Skip to content

第 8 课:Docker Compose + Agent 服务化——为什么企业 Agent 平台不能把所有东西塞进一个进程

今天从“单个 Agent Runner 容器”升级为一个最小企业服务栈:

API
↓
任务队列
↓
Agent Runner
↓
PostgreSQL / Redis

目标是理解一个很核心的架构问题:

为什么企业 Agent 系统不能只是 python agent.py 一直跑,而要拆成 API、Runner、队列、数据库等多个服务?

Docker Compose 正适合用来完成这一阶段。Docker 官方把 Compose 定义为:用一个 compose.yaml 描述和运行多容器应用,包括 services、networks、volumes 等。citeturn965719search8turn965719search33


1. 先看最小企业 Agent 架构

假设用户提交:

修复 Git 仓库里的 timeout bug。

不要直接:

HTTP请求
 ↓
Agent开始跑30分钟
 ↓
HTTP一直等

更好的结构:

用户
 ↓ HTTP
API Service
 ↓
创建 Task
 ↓
Redis Queue
 ↓
Agent Runner
 ↓
Git / Shell / LLM
 ↓
结果
 ↓
PostgreSQL

API 只负责:

接收任务
鉴权
校验参数
创建task_id
查询状态

Runner 负责:

克隆/挂载仓库
构建context
调用模型
执行tool
测试
保存结果

数据库负责:

task状态
Agent执行历史
用户
审计记录
token/cost
结果

队列负责:

“还有哪些任务等待执行?”

这就是第一次真正接触:

control plane 与 worker plane 分离。

可以暂时理解为:

API / Scheduler
= 控制任务

Runner
= 真正干活

2. 为什么不能让 API 进程直接跑 Agent

例如:

@app.post("/agent")
def run_agent():
    subprocess.run(...)

问题很多。

如果 Agent:

运行25分钟

HTTP 请求可能:

timeout

如果 Agent:

npm run dev

卡住,Web API worker 也可能被拖住。

如果一次来了:

100个请求

就可能变成:

100个Agent
+
100组shell
+
100份LLM调用

服务器迅速失控。

所以:

request handling

和:

long-running Agent workload

通常应该分开。

后面 Kubernetes 会把 Runner 进一步升级成:

一个Task
↓
一个Pod/Job

3. Compose 的 service

先写第一版:

services:

  api:
    build: ./api

  runner:
    build: ./runner

  redis:
    image: redis:8

  postgres:
    image: postgres:18

这里每一个:

api
runner
redis
postgres

都是一个 Compose service。

Docker Compose 会依据这个配置创建和管理多个容器,并自动处理默认网络等基础设施。citeturn965719search13turn965719search31

运行:

docker compose up

可能得到:

agent-api-1
agent-runner-1
agent-redis-1
agent-postgres-1

这就比上一课:

docker run ...

手动管理多个 container 更适合完整应用。


4. Compose 自动创建网络

这是今天非常重要的一点。

假设:

services:

  api:
    ...

  postgres:
    image: postgres:18

Compose 会给应用创建网络。

于是 API 中通常不应该:

DATABASE_HOST=localhost

而应该:

DATABASE_HOST=postgres

为什么?

因为:

api container

里的:

localhost

指的是:

api container自己

不是 PostgreSQL container。

真正结构:

Compose Network

┌──────────────┐
│ api          │
│              │
│ localhost    │ ← api自己
└──────┬───────┘
       │
       │ postgres:5432
       ↓
┌──────────────┐
│ postgres     │
└──────────────┘

Compose 提供的服务名可以作为网络内 DNS 名称,所以应用可以通过:

postgres:5432
redis:6379

通信。Docker 官方 PostgreSQL Compose 指南也明确使用 Compose 自动网络实现服务间通信。citeturn965719search31

这是 Docker 初学最常见的问题之一:

宿主机访问数据库用 localhost:5432,为什么容器里 localhost:5432 不通?

原因就是:

localhost是“当前网络命名空间中的自己”

5. 第一次真正理解 Port

我们可以写:

postgres:
  image: postgres:18
  ports:
    - "5432:5432"

意思近似:

Host 5432
   ↓
Container 5432

所以你的 Mac/Windows:

localhost:5432

可以访问 PostgreSQL。

但如果:

只有api需要访问postgres

其实生产环境未必需要把 PostgreSQL 暴露到 host。

因为:

api
 ↓
Compose internal network
 ↓
postgres:5432

已经够了。

也就是说:

能不暴露的端口,不必全部暴露。

这是最小攻击面的一个实际体现。


6. environment

PostgreSQL container 通常需要:

postgres:
  image: postgres:18

  environment:
    POSTGRES_DB: agentdb
    POSTGRES_USER: agent
    POSTGRES_PASSWORD: devpassword

API:

api:
  environment:
    DATABASE_URL: postgresql://agent:devpassword@postgres:5432/agentdb

这里再次用到你已经学过的:

environment variable

Container 启动:

PID 1
 ↓
继承 environment
 ↓
应用读取 DATABASE_URL

例如 Python:

import os

db = os.environ["DATABASE_URL"]

C# 类比:

Environment.GetEnvironmentVariable("DATABASE_URL");

但是注意:

environment variable
≠
完善的Secret管理系统

开发环境暂时可以这样做。

生产密码、API Key 后面会转向:

Docker secrets
Kubernetes Secret
Vault / Cloud Secret Manager

7. Volume:数据库不能随着 container 删除

上一课说过:

Container writable layer

通常和 container 生命周期绑定。

如果 PostgreSQL 数据也只放那里:

docker compose down

删除 container 后,数据可能丢失。

因此:

postgres:

  volumes:
    - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

结构:

postgres container
       │
       ▼
/var/lib/postgresql/data
       │
       ▼
Docker managed volume
       │
       └── postgres_data

Docker 官方把 volume 作为 Docker 管理的持久化存储,并指出其生命周期可独立于具体 container。citeturn965719search8

所以:

container

可以:

destroy / recreate

而数据还在。

这是云原生里一个很重要的思维:

进程/容器可以是短暂的,状态需要单独管理。


8. 为什么 Agent Runner 反而应该尽量无状态

假设:

Runner A

执行任务 101。

如果所有任务状态只保存在 Runner 内存:

tasks = {}

Runner crash:

任务状态一起没了

因此更合理:

Runner
↓
只负责execution

Task state
↓
PostgreSQL

Queue
↓
Redis

那么:

Runner A crash

可以:

Runner B

重新接手。

这叫:

stateless worker

以后 Kubernetes:

Deployment replicas=5

就是建立在类似理念之上。


9. Redis 在这里做什么

Redis 很多人只理解成:

缓存

其实它也可以作为:

queue / stream

例如 Redis Streams 是 append-only log,并支持 consumer group,多 consumer 可以协作消费消息。citeturn965719search4turn965719search11

简化理解:

API
 │
 │ 新任务
 ▼
Redis Stream
 │
 ├── task101
 ├── task102
 └── task103
       │
       ▼
Consumer Group: agent-runners
       │
   ┌───┴────┐
   ↓        ↓
Runner A  Runner B

Redis consumer group 可以让多个 worker 像一个团队一样分摊消息;XREADGROUP 用来消费,而 XACK 表示任务已处理确认。citeturn965719search34turn965719search11

这已经开始接近:

Agent Scheduler

10. 为什么要有 ACK

假设:

Runner A

取得:

task101

然后执行到一半:

crash

如果队列已经直接把 task101 删除:

任务丢失

更好的队列设计是:

任务分配
 ↓
pending
 ↓
Runner处理
 ↓
成功
 ↓
ACK

如果:

Runner挂了

系统可以发现:

pending但很久没ACK

然后:

retry

Redis Streams 的 consumer group 就维护 pending entries,可用于追踪已投递但尚未确认的消息。citeturn965719search34

这直接关系到 Agent 后面最重要的问题之一:

Agent任务到底执行了一次,
还是可能执行两次?

稍后我们会进入:

at-least-once
idempotency
retry

11. PostgreSQL 为什么不直接被 Redis 替代

可以先这么分:

Redis

适合:

任务排队
临时状态
锁
缓存
事件流

PostgreSQL

适合:

Task记录
User
Repository
Audit
Agent run历史
Token cost
最终结果

PostgreSQL 最大价值之一是:

transaction

数据库事务可以把多个操作看成一个整体:全部完成或全部失败。citeturn965719search7

例如创建 Agent Task:

插入task
+
写audit log
+
扣quota

可以放入 transaction。

如果中间失败:

rollback

避免:

quota扣了
但task没创建

这就是数据库工程开始进入 Agent 平台。


12. 第一个 compose.yaml

这一周的小项目可以升级成:

mini-agent-platform/
│
├── compose.yaml
├── api/
│   ├── Dockerfile
│   └── app.py
│
└── runner/
    ├── Dockerfile
    └── runner.py

先写:

services:

  redis:
    image: redis:8-alpine

  postgres:
    image: postgres:18-alpine

    environment:
      POSTGRES_DB: agentdb
      POSTGRES_USER: agent
      POSTGRES_PASSWORD: agent-dev

    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    build: ./api

    environment:
      REDIS_HOST: redis
      DATABASE_HOST: postgres

    depends_on:
      - redis
      - postgres

  runner:
    build: ./runner

    environment:
      REDIS_HOST: redis
      DATABASE_HOST: postgres

    depends_on:
      - redis
      - postgres

volumes:

  postgres_data:

执行:

docker compose up --build

现在有:

API
Runner
Redis
PostgreSQL

一起启动。


13. 但 depends_on 有一个大坑

很多初学者看到:

depends_on:
  - postgres

以为:

PostgreSQL 已经准备好接受连接了。

不一定。

Docker 官方明确区分:

container已经启动

和:

service已经ready

depends_on 可以控制启动顺序,但真正等待服务健康通常应该结合 healthcheck 和 condition: service_healthy。citeturn965719search2turn965719search20

例如:

postgres container started
↓
PostgreSQL还在初始化数据库
↓
API马上连接
↓
Connection refused

于是出现经典:

“明明 depends_on 了为什么数据库还是连不上?”

因为:

started ≠ ready

14. Healthcheck

可以写:

postgres:

  image: postgres:18-alpine

  healthcheck:
    test:
      [
        "CMD-SHELL",
        "pg_isready -U agent -d agentdb"
      ]

    interval: 5s
    timeout: 3s
    retries: 10

然后:

api:

  depends_on:

    postgres:
      condition: service_healthy

结构:

postgres container start
       ↓
healthcheck
       ↓
not healthy
       ↓
继续检查
       ↓
healthy
       ↓
API start

Docker 官方 Compose 指南也直接使用 healthcheck + depends_on 来消除服务启动 race condition。citeturn965719search20

这个概念后面会直接升级成 Kubernetes:

readinessProbe
livenessProbe
startupProbe

所以现在一定要理解。


15. started ≠ healthy ≠ useful

再深入一点。

有三层:

Process started

表示:

PID存在

第二层:

Service healthy

例如:

PostgreSQL接受连接

第三层:

Application ready

例如:

migration已经完成
required table存在
模型服务加载完成

对 Agent 模型服务尤其明显。

例如 Hugging Face inference server:

container已经启动

但还在:

加载30GB模型权重

此时:

PID存在

不代表:

可以推理

所以以后 Agent 部署:

Process
Container
Service
Model

每一层都可能有自己的 ready 条件。


16. Agent Runner 应该怎样拿任务

现在写最简单伪代码:

while True:

    task = queue.pop()

    if task is None:
        sleep()

    else:
        execute_agent(task)

但企业版会慢慢变成:

while True:

    task = queue.claim()

    try:

        mark_running(task)

        result = run_agent(task)

        save_result(result)

        queue.ack(task)

    except Exception as e:

        mark_failed(task, e)

        retry_or_dead_letter(task)

这里已经出现几个以后必须掌握的词:

claim
ack
retry
dead-letter queue

Agent 不可能永远成功。

所以:

失败不是异常情况,而是系统设计的一部分。

这也是 Agent 平台比简单 Chatbot 难很多的原因。


17. Agent 为什么尤其需要队列

普通 API:

请求
↓
100ms
↓
响应

Agent:

请求
↓
模型调用
↓
shell
↓
build
↓
test
↓
模型再调用
↓
git diff
↓
可能10分钟

甚至:

1小时

所以 Agent 特别适合:

asynchronous job

用户:

POST /tasks

返回:

{
  "task_id": "task-101",
  "status": "queued"
}

然后:

GET /tasks/task-101

得到:

{
  "status": "running"
}

最后:

{
  "status": "succeeded"
}

这就是企业 Agent API 的雏形。


18. Codex 与这种架构怎样结合

当前 Codex 官方 SDK 就明确定位于把 Codex 集成到:

CI/CD
内部工具
自己的应用

并允许程序化启动、继续和恢复 coding thread。citeturn661093search18

因此我们最终可以把:

Runner

内部的:

run_agent(task)

替换成:

Codex SDK

结构:

Queue
 ↓
Runner
 ↓
Codex SDK
 ↓
Codex Runtime
 ↓
Shell / Files / MCP

而且 OpenAI Agents SDK 当前的 Shell Tool 已明确支持两种环境:

local runtime
hosted container

也允许开发者提供自己的 shell executor。citeturn661093search1turn661093search3

这说明我们现在学习的:

Runner
Container
Shell Executor

并不是脱离 Agent 的 DevOps 知识,而正是现代 Agent SDK 的实际执行基础。


19. Kimi Code 如何对应

Kimi Code CLI 官方当前把自己定义为运行在 terminal 中、能读写代码、执行 shell、搜索网页并自主规划调整动作的 Agent。citeturn661093search2

它当前的插件工具也明确采用:

独立 subprocess
→ stdout 返回给 Agent

这样的执行形式。citeturn661093search8

而截至 2026 年 8 月 19 日,Kimi Code CLI 官方 changelog 最新记录到 0.37.2,其中仍在修 context compaction 重试和 datasource tool 完整响应等运行时问题。citeturn661093search11

这提醒我们:

LLM

只是系统的一部分。

Agent 实际可靠性还受:

tool execution
context management
retry
state persistence

影响。


20. Codex / Kimi / 自研平台今天的对应关系

层 Codex / OpenAI Kimi Code 我们的 Mini Platform
Agent loop Codex/Agents SDK Kimi Runtime 后面自研
Shell ShellTool / Codex runtime Shell tool runner.py
执行环境 local / hosted container 终端/subprocess Docker runner
任务入口 SDK/API/CLI CLI/SDK API service
长任务调度 由应用设计 由应用设计 Redis queue
持久状态 由集成系统管理 session/runtime状态 PostgreSQL
扩展 MCP/Skills/tools MCP/Skills/plugins 后面接MCP/Skills

这里要注意:

我没有说 Codex/Kimi 内部一定使用 Redis/PostgreSQL 这种架构。

今天讲的是:

当你把 Coding Agent 能力做成企业服务时,一个合理的外围平台架构可以怎么设计。

要严格区分:

公开产品实现事实

和:

我们的平台架构设计

这也是以后读技术文档必须养成的习惯。


21. 今日 15 分钟实验

如果已经有 Docker:

先创建最小:

services:

  redis:
    image: redis:8-alpine

  postgres:
    image: postgres:18-alpine

    environment:
      POSTGRES_PASSWORD: agent
      POSTGRES_USER: agent
      POSTGRES_DB: agentdb

    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:

  postgres_data:

启动:

docker compose up -d

查看:

docker compose ps

日志:

docker compose logs postgres

进入 Redis:

docker compose exec redis redis-cli

执行:

PING

应该:

PONG

退出。

进入 PostgreSQL:

docker compose exec postgres \
  psql -U agent -d agentdb

执行:

SELECT version();

然后:

CREATE TABLE tasks (
    id text primary key,
    status text not null
);

插入:

INSERT INTO tasks
VALUES ('task-101', 'queued');

查询:

SELECT * FROM tasks;

如果做到这里,你已经亲手建立了:

Agent Task Persistence

的最小数据库。


22. 今日企业故障案例

现象

API:

could not connect to server
Connection refused
localhost:5432

但:

docker compose ps

PostgreSQL 明明正常。

不要先怀疑数据库。

排查:

API运行在哪里?
 ↓
Container

连接:
localhost:5432
 ↓
localhost是谁?
 ↓
API container自己

正确通常应该:

postgres:5432

于是问题属于:

Docker networking / service discovery

而不是:

PostgreSQL故障

另一个现象:

API偶尔启动失败:

connection refused postgres:5432

重启一下就好了

可能:

Compose启动postgres container
↓
立即启动API
↓
Postgres仍初始化
↓
API连接失败

属于:

startup race

修:

healthcheck
+
service_healthy
+
应用自身retry

而不是简单:

sleep 20

因为:

sleep 20

只是猜测。


23. 今天第一次建立“服务化故障树”

以后 Agent Platform 出问题:

Task一直queued

不要问模型。

故障树:

Task queued
 │
 ├─ API成功写任务了吗?
 │
 ├─ Redis有消息吗?
 │
 ├─ Runner活着吗?
 │
 ├─ Runner连接Redis了吗?
 │
 ├─ Consumer group正常吗?
 │
 ├─ Runner拿到task了吗?
 │
 ├─ PostgreSQL状态更新成功吗?
 │
 └─ Agent真正开始执行了吗?

而:

Task running但永远不结束

才继续:

LLM
Shell
process tree
timeout
container
network
Git

这样故障定位就开始具有层次。


岗位能力映射

今天实际训练的是:

Docker Compose、multi-service architecture、service discovery、container DNS、port、environment、volume、healthcheck、queue、worker、Redis Streams、PostgreSQL transaction、stateless runner。

这已经对应:

Agent Platform Engineer
AI Backend Engineer
Agent Deployment Engineer
DevOps / SRE

的基础能力。

其中最重要的三个工程判断是:

localhost 永远要先问“站在谁的网络空间里?”

Container started 不代表 Service ready。

Agent 长任务应该被视为 Job,而不是普通 HTTP 请求。

今日检查题

  1. 为什么 API container 中访问 PostgreSQL 通常应该写 postgres:5432 而不是 localhost:5432?
  2. 为什么 depends_on 不能简单等价为“数据库已经 ready”?
  3. Redis Queue 与 PostgreSQL 在 Agent 平台里分别适合承担什么职责?
  4. 为什么 Agent Runner 最好尽量设计成 stateless?
  5. 一个 Runner 取得任务后崩溃,为什么队列不能简单认为任务已经完成?

参考答案核心:Compose 服务通过内部 DNS 名通信;启动容器和服务可用是两个阶段;Redis偏任务流和临时协调,PostgreSQL偏持久业务状态;无状态 Runner 才容易替换、扩容、恢复;任务需要 ACK/重试机制,不能以“已经分配”为“已经完成”。

下一课继续这个小项目,进入 HTTP/REST + SSE/WebSocket + Agent 流式输出:为什么 Agent 任务适合异步提交,但用户又希望实时看到“正在读文件、正在执行测试、正在生成结果”,以及 Codex/Kimi 一类 CLI 的流式事件怎样映射到企业 Web API。