使用 Docker 构建可落地运行的 AI 系统——构建自主 AI Agents

10 阅读49分钟

在第 6 章中,你已经通过 Model Context Protocol 将 AI models 连接到了 external tools。Claude Desktop 可以与 GitHub 交互、查询 Postgres databases,并执行各种 tasks——这一切都是响应你的 commands。AI 会等待你提出请求,然后执行你要求的内容。

但问题在于:并不是每一个 AI system 都应该等待 human input。假如你需要一些 systems 24/7 监控 repositories,并在 bugs 出现时自动修复它们,会怎样?或者,你需要 agents 持续处理 data streams,并在没有 human intervention 的情况下做出 decisions,又会怎样?

本章讨论的就是如何构建这类 systems——autonomous AI agents。它们并不只是 respond to commands,而是会主动 pursue goals。它们感知 environment,reason about 正在发生的事情,decide 要做什么,take action,并持续 iterate。除非出现问题,否则不需要 human in the loop。

提前提醒:autonomous agents 会增加 operational complexity。你需要 sophisticated error handling、comprehensive monitoring 和 strict security controls。不过,当你需要在没有 human oversight 的情况下 continuous operation 时,这个 trade-off 是值得的。

本章将覆盖以下主要主题:

  • Understanding autonomous AI agents
  • Implementing container isolation for agent security
  • Building production-ready agent controllers
  • Designing agent communication patterns
  • Configuring Docker networking for multi-agent systems
  • Implementing security and monitoring essentials

读完本章后,你将拥有运行在 Docker containers 中的 autonomous agents——它们被 secured、isolated,并且能够彼此协作来完成 complex tasks。

Technical requirements

为了跟随本章 examples,你需要在自己的机器上设置好以下 tools 和 accounts:

Software

Docker Desktop 4.42.0 or later:Reasoning engine 和 tool access examples 需要 MCP Toolkit 和 Model Runner features。请从以下地址下载:

https://www.docker.com/products/docker-desktop/

可以在 terminal 中运行 docker version,或者在 Docker Desktop 中检查 Settings > About 来验证版本。

Python 3.11 or later:Agent controller 和 worker implementations 需要它。大多数 examples 使用 Python 编写 agent loop logic。使用以下命令验证:

python --version

Node.js 18 or later:展示 LLM integration 的 reasoning engine examples 需要它。使用以下命令验证:

node --version

Git:用于 clone 本章 code repository。使用以下命令验证:

git --version

curl:本章中会反复用于测试 REST API endpoints 和验证 services。macOS 和大多数 Linux distributions 都会预装。

jq(optional but recommended) :一个 command-line JSON processor,用于解析 structured agent logs。macOS 上可通过 brew install jq 安装,Ubuntu 上可通过 apt install jq 安装。没有它也可以跟着做,但有了 jq,log parsing examples 会更容易。

Hardware

至少 8 GB RAM 的机器,推荐 16 GB。运行多个 agent containers,并同时运行一个 LLM model,需要足够 memory。如果你同时运行所有 examples,16 GB 会提供更舒适的 headroom。

至少 10 GB free disk space,用于 container images。Reasoning engine、Redis、MCP Gateway,以及多个 agent containers 会很快累积空间。可以运行以下命令检查当前 Docker disk usage:

docker system df

Accounts and API keys

GitHub Personal Access Token(optional) :如果你想在 tool access layer examples 中用 GitHub tools 测试 MCP Gateway,请在以下地址生成一个 token,并勾选 repo scope:

https://github.com/settings/tokens

这是你在第 6 章中使用过的同一种 token format。

Knowledge prerequisites

本章直接建立在第 3 章和第 6 章之上。你应该熟悉用于运行 local LLMs 的 Docker Model Runner(第 3 章),以及用于将 AI 连接到 external tools 的 MCP Gateway configuration(第 6 章)。理解前面章节中的 Docker Compose services、volumes、networks 和 health checks 非常重要——本章会大量使用这些 concepts。

熟悉 Python 会有助于理解 agent implementations,不过即使 Python 不是你的主要语言,这些代码也足够 straightforward,仍然可以跟着理解。对 REST APIs 和 Redis 有基本了解也会有帮助。

本章代码 examples 可在以下位置获取:

https://github.com/PacktPublishing/Operational-AI-with-Docker/tree/main/chap-07

Understanding autonomous AI agents

在你构建 autonomous agents 之前,必须先理解它们与目前已经构建过的 AI applications 有什么 fundamental differences。本节覆盖 conceptual foundation:什么是 agents,它们如何 operate,以及什么时候它们是正确的 architectural choice。我们会先从 definitions 开始,然后进入驱动所有 agents 的 core loop,接着考察每个 agent 都需要的四个 essential components,最后说明什么时候 agents 合理,什么时候更简单的 approaches 更好。

Defining autonomous agents

想一想你目前已经构建过的 AI applications。在第 3 章中,使用 Docker Model Runner 时,你发送一个 prompt,然后得到一个 response。在第 6 章中,使用 Claude Desktop 时,你要求它执行 actions,它就去执行。这些都是 interactive systems——它们会等待你告诉它们要做什么。

Agents 不一样。它们不会等待。它们持续运行,在没有 constant human supervision 的情况下 pursue goals。下面用实际例子说明它是什么样子。

下面是一个 interactive AI application 处理 bug report 的方式:

Human: "Create a GitHub issue for the bug in user authentication"
AI: [Calls create_issue tool]
AI: "Issue #1234 created"
[System stops, waits for next human input]

现在把它与 autonomous AI agent 对比:

[Agent monitors GitHub repository continuously]
[New issue #1234 appears: "Users can't log in"]
Agent: [Reads issue, analyzes codebase]
Agent: [Identifies bug in auth.py line 42]
Agent: [Creates branch, writes fix, runs tests]
Agent: [Tests pass - creates pull request #567]
Agent: [Sends Slack notification to team]
[Agent continues monitoring for next issue]

注意到区别了吗?Agent 感知到一个 event(new issue),reasoned about 这个 problem,planned 一个 solution,executed multiple actions,observed results,然后继续运行——整个过程中没有任何人告诉它要做什么。这就是 autonomy in action。

如果你已经理解了 interactive systems 与 autonomous systems 之间的区别,就该看看使 autonomy 成为可能的 mechanism 了。

The agent loop architecture

每个 agent——无论是 monitoring GitHub、processing data,还是 managing infrastructure——都运行同一个 fundamental loop。理解这个 loop 非常重要,因为你稍后会在本章中用 code 实现它。

这个 loop 有六个 phases,并且会 indefinitely repeat:

Perceive:Agent 主动检查它的 environment。对于 GitHub agent 来说,这意味着调用 API 来 list new issues。对于 deployment agent 来说,这可能意味着检查 message queue 中的 pending requests。Agent 会按 schedule 或响应 events 执行这些 checks——它不是 idle 地等着 instructions。

Reason:当 agent perceives 到某些 interesting 的东西时,它需要理解其含义。Agent 会把 data 发送给 LLM,并使用类似 “Analyze this GitHub issue. What type of problem is this? How urgent is it?” 的 prompt。LLM 会 interpret the situation,并提供 context。这就是 “intelligence” 发生的地方。

Plan:基于 analysis,agent 决定要采取哪些 actions。它可能要求 LLM 生成一系列 steps:“Check git history, read authentication code, identify the bug, create a fix, run tests.” Plan 可以很简单,也可以很复杂,取决于 task。

Act:现在 agent 开始执行 plan。每个 step 都涉及 calling tools——reading files、creating branches、committing code、triggering builds。这就是 agent 使用你在第 6 章配置的 MCP Gateway 与 external systems 交互的地方。

Observe:执行 action 之后,agent 检查 results。文件是否创建成功?Tests 是否通过?Agent 会检查 return codes、读取 logs,并验证 actions 是否成功。如果某个东西 failed,agent 必须在继续之前知道。

Iterate:基于 observations,agent 决定下一步做什么。如果 actions 成功,它继续处理 next task,并回到 Perceive phase。如果 actions failed,它会尝试另一种 approach,或者将 task 标记为 requiring human intervention。然后 loop 重新开始。

图 7.1 展示了这种 fundamental difference。左侧是 traditional generative AI system,它从 input 到 output 走一条 linear path——system respond,然后 stop。右侧是 agentic AI system,它运行一个 continuous loop,在 perception、reasoning、planning、action 和 observation 之间无限循环。

image.png

图 7.1:Generative AI versus Agentic AI——traditional AI systems 遵循 linear request-response pattern(左),而 agentic systems 运行一个 continuous loop,除非被显式终止,否则永不停止(右)

这个 loop 会持续运行,直到你显式 stop agent。Traditional programs 有一个 clear end state——它们完成工作并 exit。Agents 被设计用于 continuous operation。这就是它们被称为 autonomous 的原因。

现在,你已经从概念上理解了这个 loop。接下来谈谈你实际构建一个 agent 需要什么。

Core agent components

每个 autonomous agent,无论是 monitoring GitHub、processing data,还是 managing infrastructure,都需要四个 fundamental components 协同工作。如果缺少其中任何一个,你的 agent 都无法 properly function。我们将逐一考察这些 components,看看如何在 Docker Compose 中 configure 它们,并用 working code 进行测试。

Component 1: Reasoning engine

Reasoning engine 为 agent 做 decisions。在 Docker-based agents 中,这通常是一个通过 Docker Model Runner 访问的 large language model。Reasoning engine 接收来自 perception layer 的 observations,并为 action layer 生成 plans。

我们先从 reasoning engine configuration 开始。下面这个 Docker Compose file 设置了一个 service,它连接到 Docker Model Runner,并 expose 一个 health check endpoint:

services:
  reasoning:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8082:8080"
    environment:
      - PORT=8080
    models:
      - llama
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

models:
  llama:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048

models section 引用了一个名为 llama 的 model。Docker Compose 会自动注入 LLAMA_URLLLAMA_MODEL 作为 environment variables。你的 code 会读取这些 variables 来连接到 model。

context_size parameter 控制 model 一次能处理多少 text。2048 tokens 大约等于 1,500 到 1,800 个 words。对于需要分析更长 code files 或 documents 的 agents,可以将其增加到 4,096 或 8,192。

配置好 Docker Compose 中的 model 后,你的 application code 需要连接到它。下面展示如何读取 Docker 注入的 environment variables,并调用 model 的 chat completions endpoint:

const axios = require('axios');

function getLLMEndpoint() {
    const llamaUrl = process.env.LLAMA_URL;
    return `${llamaUrl}/chat/completions`;
}
function getModelName() {
    return process.env.LLAMA_MODEL;
}
async function callLLM(userMessage) {
    const chatRequest = {
        model: getModelName(),
        messages: [
            {
                role: "system",
                content: "You are a helpful assistant."
            },
            {
                role: "user",
                content: userMessage
            }
        ]
    };
   
    const response = await axios.post(
        getLLMEndpoint(),
        chatRequest,
        {
            headers: { 'Content-Type': 'application/json' },
            timeout: 30000
        }
    );
   
    return response.data.choices[0].message.content.trim();
}

Docker Model Runner 会基于 docker-compose.yaml 中的 model configuration 自动注入 LLAMA_URLLLAMA_MODEL environment variables。

现在测试这个 configuration。Clone repository 并导航到 reasoning example:

cd Operational-AI-with-Docker/chap-07/reasoning
docker compose up --build

你会看到 Docker building image 并 configuring model 的 output:

=> => naming to docker.io/library/reasoning-node-genai:latest                              0.0s
 => => unpacking to docker.io/library/reasoning-node-genai:latest                           0.3s
 => resolving provenance for metadata file                                                  0.0s
[+] up 3/4
✓ Image reasoning-node-genai       Built                                                  16.8s
⠼ llama                            Configuring                                             0.3s
✓ Network reasoning_default        Created                                                 0.0s
✓ Container reasoning-node-genai-1 Created                                                 0.2s
Attaching to node-genai-1
node-genai-1  | Server starting on http://localhost:8080
node-genai-1  | Using LLM endpoint: http://model-runner.docker.internal/v1/chat/completions
node-genai-1  | Using model: ai/llama3.2:1B-Q8_0

Server 现在正在运行,并 ready to accept reasoning requests。

image.png

图 7.2:Docker Desktop Containers view,展示 reasoning engine——reasoning model container 和 node-genai service 都在运行,port 8082 映射到 internal port 8080,用于 API access

用一个 simple question 测试 API endpoint:

curl -X POST http://localhost:8082/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Explain Docker in simple terms"}'

Server 会返回一个 JSON object,其中包含 LLM 的 explanation:

{"response":"Docker in Simple Terms\n\nDocker is a popular containerization platform that allows you to package, ship, and run applications in isolated environments. Here's how it works in simple terms:\n\n**What is a Container?**\n\nA container is a self-contained environment that runs a specific version of an application. It's like a virtual machine, but much faster and more secure.\n\n**How Does Docker Work?**\n\nHere's a step-by-step explanation:\n\n1. **Create a Docker Image**: You create a Docker image by writing a set of instructions..
}

你还可以验证 server 的 health status:

curl http://localhost:8082/health

这会返回 running service 的信息:

{
  "status": "healthy",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "llm_endpoint": "http://model-runner.docker.internal/v1/chat/completions",
  "model": "ai/llama3.2:1B-Q8_0"
}

在 browser 中打开:

http://localhost:8082

即可使用 web interface。Interface 提供一个 simple chat box,你可以直接与 reasoning engine 交互。你发送的每条 message 都会由 LLM 处理,展示 agents 如何使用 reasoning engines 来理解并 respond to inputs。

image.png

图 7.3:Reasoning engine web interface——一个 simple chat box,用于在将 LLM interactions 集成到 agent workflows 之前进行测试

Reasoning engine 赋予 agent intelligence,但没有 action 的 intelligence 是没有用的。你的 agent 需要与 external systems 交互——create GitHub issues、query databases、send Slack messages。这就是 tool access layer 登场的地方,而你已经在第 6 章构建了它的基础。

Component 2: Tool access layer

Agents 通过 MCP servers 提供的 tools 与 external systems 交互。你在第 6 章中已经为 GitHub、Firecrawl,以及 Postgres 等 database services 配置过 MCP servers。Agents 使用同样的 tools。

下面用 Docker MCP Gateway 配置 tool access layer:

services:
  mcp-gateway:
    image: docker/mcp-gateway
    command:
      - --servers=github-official
      - --servers=firecrawl
      - --servers=kubernetes
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - "8812:8811"

--servers flags 指定要加载哪些 MCP servers。Docker socket mount(/var/run/docker.sock)赋予 gateway 管理 MCP server containers 的权限。如果没有这个 mount,gateway 无法 start 或 monitor MCP servers。

Note: MCP server credentials

运行 docker compose up 时,gateway 会尝试 initialize all three servers。那些没有在 Docker Desktop 中配置 credentials 的 servers 会失败,并显示类似 errors:

- slack: docker: Error response from daemon: secret not found
 > Can't start slack: failed to connect: calling "initialize": EOF

- kubernetes: docker: invalid spec: :/home/appuser/.kube/config: empty section between colons
> Can't start kubernetes: failed to connect: calling "initialize": EOF

如果你还没有 connected 这些 services,这是 expected behavior。Gateway 会继续 initialize remaining servers——只有具备 valid credentials 的 servers 会 load。

要配置 credentials,请打开 Docker Desktop → Settings → Beta Features → MCP Toolkit,选择对应 server,并在 restart gateway 之前提供 required token。对于 Kubernetes,你需要在提示时提供 local kubeconfig file,通常位于 macOS 和 Linux 上的:

~/.kube/config

通过 clone repository 并进入 tool access layer example 来启动 MCP gateway:

cd ../tool-access-layer
docker compose up --build

Gateway 会启动并 initialize MCP servers。你会看到显示 initialization process 的 output:

- Listing MCP tools...
  - Running ghcr.io/github/github-mcp-server with [run --rm -i --init
    --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never
    -l docker-mcp=true -l docker-mcp-tool-type=mcp
    -l docker-mcp-name=github-official -l docker-mcp-transport=stdio
    --network tool-access-layer_default -e GITHUB_PERSONAL_ACCESS_TOKEN]
  - Running mcp/kubernetes with [run --rm -i --init...]
  - Running mcp/firecrawl with [run --rm -i --init...]

Gateway 会以 isolated Docker containers 的形式启动每个 MCP server,并带有 security constraints。--security-opt no-new-privileges flag 防止 privilege escalation。--cpus 1 --memory 2Gb flag 限制 resource usage。-l docker-mcp=true labels 有助于 identify MCP containers。

每个 server 会启动,并报告其 available tools:

- github-official: time=2025-12-27T13:37:26.229Z level=INFO msg="starting server"
  version=v0.26.3
- github-official: GitHub MCP Server running on stdio
- github-official: time=2025-12-27T13:37:26.241Z level=INFO msg="session initialized"
  > github-official: (40 tools) (2 prompts) (5 resourceTemplates)
 
  > firecrawl: (6 tools)
 
> 46 tools listed in 6.29s
> Initialized in 16.93s
> Start stdio server

GitHub server 加载了 40 个用于 repository operations 的 tools、2 个 common workflows 的 prompt templates,以及 5 个用于 accessing GitHub data 的 resource templates。Firecrawl 添加了 6 个用于 web scraping 和 search 的 tools。Gateway 大约在 17 秒内成功 initialize 了 46 个 total tools。

验证 gateway 是否正在运行:

docker compose ps

你应该会看到 mcp-gateway service 的 status 为 "Up"

下面是你的 agent code 如何通过 gateway 调用 tools:

const axios = require('axios');

async function createGitHubIssue(owner, repo, title, body) {
    const response = await axios.post(
        "http://mcp-gateway:8811/mcp",
        {
            tool: "create_issue",
            arguments: {
                owner: owner,
                repo: repo,
                title: title,
                body: body
            }
        }
    );
   
    return response.data.number;
}

MCP Gateway 接收 request,将它 route 到 appropriate MCP server,并返回 result。

用一个 simple tool call 测试 gateway:

# List available tools
curl http://localhost:8811/tools

# Search GitHub repositories (read-only, safe to test)
curl -X POST http://localhost:8811/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "search_repositories",
    "arguments": {
      "query": "docker language:go stars:>1000"
    }
  }'

这会搜索 Docker-related、Go language、且 stars 超过 1000 的 GitHub repositories。Response 包含 repository names、descriptions 和 URLs,展示 agents 如何通过 MCP Gateway 访问 external tools。

你的 agent 现在可以 think 和 act 了。但是,当 container restart 时会发生什么?

Component 3: Memory and state

没有 memory,agent 会忘记一切——它会重复处理同一个 task、重复失败的 approaches,并丢失全部 context。Production agents 需要 persistent state,能跨 restarts、crashes 和 updates 保存下来。

Redis 为 agent memory 提供 persistent storage。进入 memory-state example directory,以查看完整 configuration:

cd Operational-AI-with-Docker/chap-07/memory-state

查看 docker-compose.yaml 中的 memory configuration:

services:
  # Agent that processes tasks and remembers what it's done
  task-agent:
    build: .
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - AGENT_NAME=task-processor
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      llm:
        endpoint_var: AI_MODEL_URL
        model_var: AI_MODEL_NAME

  # Redis for persistent agent memory
  agent-memory:
    image: redis:7-alpine
    volumes:
      - agent-state:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"
models:
  llm:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048
    runtime_flags:
      - "--temp"
      - "0.7"
volumes:
  agent-state:

depends_on 配合 condition: service_healthy 确保 agent 在 Redis ready 之前不会启动。--appendonly yes flag 启用 AOF(Append Only File)persistence。Redis 会立即将每个 operation 写入 disk,因此你的 agent memory 可以 survive container restarts。

启动 memory-enabled agent:

docker compose up --build

你会看到 agent 在 Redis ready 后启动:

=> => exporting manifest sha256:f19fcac572f978e5ebe20947a5342d222956a408f66ef2c55dad915d8  0.0s
 => => exporting config sha256:811fa2f874a038286a77c20f5f831da8968b85dc446fe02a8e3dc938c30  0.0s
 => => exporting attestation manifest sha256:5dca18d80903a3bc28c3cd088e85edfa2f2a8b31bc0e7  0.0s
 => => exporting manifest list sha256:c298425c36a5e7823d1b769433961ed6333f744fe5862424d104  0.0s
 => => naming to docker.io/library/memory-state-task-agent:latest                           0.0s
[+] up 12/13king to docker.io/library/memory-state-task-agent:latest                        0.2s
[+] up 16/17is:7-alpine  Pulled                                                             5.9s
✓ Image redis:7-alpine                Pulled                                               5.9s
✓ Image memory-state-task-agent       Built                                               11.2s
⠇ llm                                 Configuring                                          8.8s
✓ Network memory-state_default        Created                                              0.0s
✓ Volume memory-state_agent-state     Created                                              0.0s
✓ Container memory-state-agent-memory-1 Healthy                                            5.8s
✓ Container memory-state-task-agent-1 Created

如下图所示,Redis container(agent-memo)提供 persistent storage,而 task-agent-1 负责处理 tasks;两个 containers 共享 memory-state network。

image.png

图 7.4:Docker Desktop Containers view,展示 memory-state stack

Memory implementation 使用两个 key methods。has_completed_task method 会检查某个 task 是否已经完成——在处理任何 task 之前,agent 调用这个 method,如果返回 True 就跳过。remember_action method 则在 action 执行之后,将每个 action 存入 Redis,创建 permanent history。

services:
  task-agent:
    build: .
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - AGENT_NAME=task-processor
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      - llama

  agent-memory:
    image: redis:7-alpine
    volumes:
      - agent-state:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"

models:
  llama:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048

volumes:
  agent-state:

Memory key format agent:{agent_name}:actions:{task_id} 会创建 namespaces,确保多个 agents 不会互相 interfere。如果你运行多个 agents(worker-1worker-2),它们的 memories 会保持 separate。每个 agent 都有自己的 memory space。

通过 listing all memory keys 验证 memory persistence:

# List all memory keys
docker compose exec agent-memory redis-cli KEYS "agent:*"

你会看到以下 task records:

1) "agent:task-processor:actions:task-003"
2) "agent:task-processor:actions:task-002"
3) "agent:task-processor:actions:task-004"
4) "agent:task-processor:actions:task-001"

查看某个 specific task 的 history:

# View specific task history
docker compose exec agent-memory redis-cli LRANGE agent:task-processor:actions:task-001 0 -1

结果如下:

"{"action": "Identify the source of customer feedback data.", "result": "Successfully completed: Identify the source of customer feedback data.", "success": true, "timestamp": "2025-12-27T14:13:42.205306"}"

这些 commands 展示了 Redis 中实际存储的内容。你会看到 agent 每次 action 的 JSON records,证明 memory 能跨 restarts 工作。

你已经拥有用于 intelligence 的 reasoning engine、用于 action 的 tools,以及用于 memory 的 Redis。最后一个组件是 controller。

Component 4: Agent controller

Controller 是实际实现 agent loop 的 code,它 orchestrates 其他 components,并在 perceive-reason-plan-act-observe cycle 中调度它们。对于 simple single-agent systems,它可以是一个 Python script。对于 production multi-agent deployments,你需要更 sophisticated 的东西。

下面是一个 simplified agent controller structure:

import time
import requests
import redis
# Initialize connections:
def call_llm(prompt):
    response = requests.post(
        f"{model_url}/chat/completions",
        json={
            "model": model_name,
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": 200
        }
    )
    return response.json()['choices'][0]['message']['content']

memory = redis.Redis(host='redis', port=6379)

def agent_loop():
    """Main agent loop: perceive → reason → plan → act → observe → iterate"""
   
    while True:
        # Perceive: Check environment
        events = perceive_environment()
       
        if events:
            # Reason: Understand what happened
            analysis = call_llm(f"Analyze: {events}")
           
            # Plan: Decide actions
            plan = call_llm(f"Plan for: {analysis}")
           
            # Act: Execute plan
            results = execute_plan(plan)
           
            # Observe: Check results
            observe_results(results)
           
            # Store in memory
            memory.rpush("actions", json.dumps({
                "events": events,
                "results": results,
                "timestamp": time.time()
            }))
       
        # Iterate: Brief pause before next loop
        time.sleep(10)

if __name__ == "__main__":
    agent_loop()

while True: loop 会无限运行。sleep(10) 会在 iterations 之间 pause 10 秒。Controller 会按顺序 orchestrate 所有六个 phases,使用 LLM 执行 reasoning 和 planning,使用 Redis 作为 memory,并使用 external tools 执行 actions。

对于包含多个 agents 的 production systems,请用 workers 配置 controller:

services:
  # Agent Controller with REST API and dashboard
  controller:
    build:
      context: .
      dockerfile: Dockerfile.controller
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - CONTROLLER_NAME=main-controller
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      - llm

  # Worker Agent
  worker-1:
    build:
      context: .
      dockerfile: Dockerfile.agent
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - AGENT_NAME=worker-1
      - AGENT_TYPE=data-processor
      - CONTROLLER_URL=http://controller:8000
    depends_on:
      controller:
        condition: service_started
    models:
      - llm
    restart: on-failure

  # Redis for shared memory
  agent-memory:
    image: redis:7-alpine
    volumes:
      - agent-state:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"

models:
  llm:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048

volumes:
  agent-state:

Build 并启动 compose services:

cd ../memory-state/
docker compose up --build

你可以使用 Docker dashboard 验证它是否正在运行。

image.png

图 7.5:Docker Dashboard,展示 agent controller system——controller、worker agents 和 Redis memory service 都在运行且 healthy

Note: Each example runs independently

这里的 Redis configuration 被有意设置为与 memory-state example 完全相同;两者都使用同一个 redis:7-alpine image、--appendonly yes persistence 和 port 6379。不过,每个 example 都位于自己的 directory 中,并作为一个 separate Docker Compose project 运行,有自己的 isolated network 和 volumes。只要你一次只运行一个 example,就不会出现 port conflict。如果你想同时运行两个,请在其中一个里 override port:

agent-memory:
  ports:
    - "6380:6379"  # Map to a different host port

这里的 Redis instance 作用与之前一样——persistent agent memory——但它的 scope 完全限定在当前 project 的 containers 中。

Controller 会在 Docker container 中 continuous run,协调多个 workers。每个 worker 会向 controller register、request tasks,并 report results。restart: on-failure policy 确保 workers 在 crashes 之后自动 restart。

Worker agents 使用三个 key methods 与 controller 交互:

class WorkerAgent:
    def register(self):
        """Register with the controller"""
        response = requests.post(
            f"{controller_url}/api/agents/register",
            json={"name": self.agent_name, "type": self.agent_type}
        )
        return response.status_code == 200
   
    def send_heartbeat(self):
        """Send heartbeat to controller"""
        requests.post(
            f"{controller_url}/api/agents/{self.agent_name}/heartbeat"
        )
   
    def get_task(self):
        """Get next task from controller"""
        response = requests.post(
            f"{controller_url}/api/tasks/dequeue",
            json={"agent_name": self.agent_name}
        )
        return response.json() if response.status_code == 200 else None

Controller 在以下地址提供 web dashboard:

http://localhost:8001

Dashboard 展示:

  • Total agents(active / inactive)
  • Pending tasks in queue
  • Success rate across all agents
  • Agent list with status and statistics
  • Auto-refreshes every 5 seconds

Controller 还 expose REST API endpoints,用于 programmatic access:

# Get all agents
curl http://localhost:8001/api/agents

# Assign a new task
curl -X POST http://localhost:8001/api/tasks/assign \
  -H "Content-Type: application/json" \
  -d '{"id": "task-123", "description": "Process customer data"}'

# Get system statistics
curl http://localhost:8001/api/stats

完整的 multi-agent implementation 会在本章后面的 “Scaling to multi-agent systems” section 中详细覆盖。

When to use agents versus interactive applications

你现在已经知道如何构建 autonomous agent 的四个 components。但在你急着把每个 AI application 都改成 agent 之前,我们需要诚实地讨论:agents 什么时候才真的有意义。Agents 会增加 operational complexity——它们需要 sophisticated error handling、comprehensive monitoring 和 strict security controls。理解这种 complexity 什么时候值得付出,可以帮你避免 over-engineering simple problems。

When agents make sense

Agents 非常适合 continuous operations——比如 security monitoring 需要 24/7 watch for threats,infrastructure management 需要 immediately respond to issues,或者 data processing pipelines 需要 around the clock 运行。如果你晚上会 shutdown,大概率不需要 agent。

它们也非常适合具有大量 conditional logic 的 multi-step workflows。如果你的 process 包含五个或更多 sequential actions,并且每一步都依赖前一步的结果,agent 处理这种 complexity 会比 human click through steps 更好。

Long-running tasks 也是 agents 的 sweet spot。Large-scale data analysis 需要 hours 或 days?Iterative code improvements 需要 multiple rounds of refinement?这些都受益于 agent autonomy,因为没人想 babysit 一个 process 那么久。

如果你需要 proactive responses——systems 能够 detect 和 respond to events,而不是等 humans——agents 通常也是正确选择。Incident response、automated testing 和 performance optimization,在 system 可以立即 act 时,效果都更好。

When to stick with interactive applications

另一方面,如果 simple workflows 只需要一到三个 actions,就不值得引入 agent complexity。如果 human 几次点击就能完成,保持 interactive 即可。

Human-in-the-loop scenarios 中每个 decision 都需要 explicit approval?这绝对不是 agent territory。Exploratory analysis 也是一样,你在每一步都要做 judgment calls;或者 high-stakes operations 中 errors 的代价极高,也不适合 agents。

来看一个具体 example:bug-fixing system。

With an interactive application:Developer 发现一个 bug,请 AI 提供 fix,review suggestion,manual apply,run tests,然后 create pull request。AI assists,但 human drives every step。

With an autonomous agent:Agent continuous monitors repository,detects new bug reports,analyzes codebase,generates fixes,automatically runs tests,如果 tests pass 就 creates pull requests,并 notifies team。Human 在 work complete 之后 review。

Agent approach 只有在你具备 robust testing infrastructure、对 agent capabilities 有 confidence,并且能接受 occasional errors(这些 errors 会在 human review 中被 caught)时才有意义。否则,带 human oversight 的 interactive applications 提供更好的 safety guarantees。

请先从 interactive applications 开始,只有当你已经证明 task 是 well-defined、testable、reversible 的时候,再 graduate 到 autonomous agents。我见过太多 teams 对本该需要 human judgment 的 tasks 直接上 agents。请相信我:很多看起来适合 agents 的 tasks,实际上更适合 human-in-the-loop interactive systems。

你已经构建了 core agent components,也理解了 agents 什么时候有意义。现在,我们来讨论一个关键问题:security。

Implementing container isolation for agent security

在第 6 章中,你运行 MCP servers in containers,主要是为了 convenience 和 standardization。但当 autonomous agents 在没有 human oversight 的情况下运行时,containerization 从 “nice to have” 变成了 “absolutely essential”。一个 unsandboxed agent 中的 bug,可能会 cascade 成 system-wide disaster。

Understanding agent isolation requirements

为什么 isolation 对 autonomous agents 如此重要?因为 agents 在没有 human oversight 的情况下 operate,需要 strict boundaries 来 prevent disasters。我不是夸张——如果没有 proper isolation,你只差一个 bug,就会让 agent 在 system 中造成破坏。

Real-world incidents 精确展示了这点。CVE-2025-24362(ClawHavoc)展示了 compromised MCP server 如何 escalate privileges,并访问 intended scope 之外的 resources。CVE-2025-24363(ClawJacked)展示了通过 agents 之间 shared environment variable exposure 进行 credential theft。这两个 vulnerabilities 都利用了 proper isolation boundaries 缺失的问题——而这些 boundaries 正是 container isolation 提供的。

想想可能出什么问题。一个 cleaning temporary files 的 agent 可能 accidentally delete 另一个 agent 的 critical data。一个 analyzing large codebase 的 agent 可能 consume all available memory,导致其他 agents starvation。一个为了 debugging 而 reading environment variables 的 agent 可能 accidentally log 并 expose credentials。一个 binding to network port 的 agent 可能与另一个试图使用同一 port 的 agent 发生 conflict。

Container isolation 通过几个 mechanisms 防止这些 failures:

Dedicated filesystem access:每个 agent 只能看到自己 container 中的 files。没有 cross-contamination。

Resource limits:Guaranteed CPU 和 memory allocation。一个 agent 不能 starve 其他 agents。

Isolated environment variables:每个 agent 的 secrets 对其他 agents invisible。没有 credential leakage。

Separate network namespace:每个 agent 都有自己的 IP address,并且可以 bind to same port numbers,而不会发生 conflicts。

没有这些 boundaries,agents 会互相 interfere、consume excessive resources、leak credentials,并制造几乎 impossible to debug 的 race conditions。相信我:proper isolation 对 production agents 来说不是 optional。

Building a multi-container system with isolation

现在把理论付诸实践。你将构建一个完整的 multi-agent system,其中两个 agents——一个 bug tracker 和一个 status reporter——独立运行,同时共享一个 language model,并通过 Redis 进行 coordination。这个 example 会准确展示 isolation 如何工作:separate filesystems、必要时 shared infrastructure,以及 failures 后的 automatic recovery。

进入 multi-agent-architecture sub-folder:

cd Operational-AI-with-Docker/chap-07/container-isolation-tests/multi-agent-architecture

Project structure 很简单:

multi-agent-architecture/
├── docker-compose.yml
├── Dockerfile.agent
└── agent.py

查看 Docker Compose configuration。注意 agents 是如何带着 isolation 考量来设置的:

services:
  # Agent 1: Bug tracking agent
  bug-tracker:
    build:
      context: .
      dockerfile: Dockerfile.agent
    environment:
      - AGENT_NAME=bug-tracker
      - AGENT_TYPE=monitoring
      - REDIS_URL=redis://agent-memory:6379
    volumes:
      - bug-tracker-state:/data
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      - llama
    restart: unless-stopped

  # Agent 2: Status reporter agent
  status-reporter:
    build:
      context: .
      dockerfile: Dockerfile.agent
    environment:
      - AGENT_NAME=status-reporter
      - AGENT_TYPE=reporting
      - REDIS_URL=redis://agent-memory:6379
    volumes:
      - reporter-state:/data
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      - llama
    restart: unless-stopped

  # Shared memory for coordination
  agent-memory:
    image: redis:7-alpine
    volumes:
      - shared-memory:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"

models:
  llama:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048

volumes:
  bug-tracker-state:
  reporter-state:
  shared-memory:

Bring up compose services:

docker compose up --build

如下图所示,agent-memory Redis container 提供 shared coordination,而 bug-tracker-1status-reporter-1 作为 isolated agents 运行,各自拥有自己的 container instances。

image.png

图 7.6:Docker Desktop Containers view,展示 multi-agent architecture

这个 architecture 做得好的地方包括:

Cost optimization through sharing:两个 agents 都引用同一个 llama model,因此 Docker Model Runner 只创建一个 LLM container,而不是两个。运行多个 models 很快就会变得 expensive;sharing 能节省 resources。

Isolation where it matters:每个 agent 都有自己的 dedicated volume(bug-tracker-statereporter-state)。Bug tracker 不能 accidentally corrupt status reporter 的 data,反之亦然。

Coordination through Redis:Shared Redis instance(agent-memory)允许 agents coordinate,但它们使用 separate key namespaces。可以把它想成同一个 filing cabinet 中的不同 folders——shared infrastructure,separate data。

Automatic recoveryrestart: unless-stopped policy 意味着如果 agent crashes,Docker 会自动将它 bring back up。不需要 human intervention。

State persistence:Named volumes 会在 container restarts 之间保留 agent memory。更新 code、restart container 后,agent 会从它离开的地方继续。

我们必须强调 isolation aspect,因为它非常关键。每个 agent 都在完整 filesystem isolation 中运行——bug tracker 无法访问 status reporter 的 /data volume。Environment variables 是 isolated 的——每个 container 的 AGENT_NAME 不同。Processes 是 isolated 的——一个 agent 看不到或 kill 另一个 agent 的 processes。Network namespaces 也是 separate 的——每个 agent 都有自己的 IP address,并且可以 bind 到相同 port numbers 而不会 conflict。

现在看 agent implementation。注意 state persistence pattern:

import os
import time
import redis
import json
from datetime import datetime

def main():
    agent_name = os.getenv('AGENT_NAME', 'agent')
    agent_type = os.getenv('AGENT_TYPE', 'worker')
    redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379')
   
    print(f"🤖 Starting {agent_name} ({agent_type})")
    print(f"   Redis: {redis_url}")
   
    # Connect to shared memory
    redis_client = redis.from_url(redis_url, decode_responses=True)
    state_key = f"agent:{agent_name}:state"
   
    # RESUME FROM LAST STATE (production pattern)
    try:
        last_state = redis_client.get(state_key)
        if last_state:
            state_data = json.loads(last_state)
            iteration = state_data.get('iteration', 0) + 1
            print(f"📥 Resuming from iteration {iteration}")
        else:
            iteration = 1
            print(f"🆕 Starting fresh at iteration 1")
    except Exception as e:
        iteration = 1
        print(f"🆕 No previous state, starting at iteration 1")
   
    while True:
        # Write current state
        state = {
            "agent": agent_name,
            "type": agent_type,
            "iteration": iteration,
            "timestamp": datetime.utcnow().isoformat(),
            "status": "active"
        }
       
        redis_client.set(state_key, json.dumps(state))
        print(f"✅ {agent_name} iteration {iteration} - state saved to {state_key}")
       
        # Read other agents' states (shared memory)
        all_keys = redis_client.keys("agent:*:state")
        print(f"   Visible agents: {len(all_keys)}")
        for key in all_keys:
            if key != state_key:
                other_state = json.loads(redis_client.get(key))
                print(f"   - {other_state['agent']}: iteration {other_state['iteration']}")
       
        iteration += 1
        time.sleep(10)

if __name__ == "__main__":
    main()

注意 comment “RESUME FROM LAST STATE(production pattern)”——这就是让 agents production-ready 的关键。Agent 在 startup 时检查 Redis 中是否有 previous state。如果它找到了 saved state,就从上次 iteration 继续。没有 repeated work,也没有 lost progress。

看看它的实际运行:

docker compose up --build

观察两个 agents 启动时发生了什么:

bug-tracker-1      | 🤖 Starting bug-tracker (monitoring)
bug-tracker-1      |    Redis: redis://agent-memory:6379
bug-tracker-1      | 🆕 Starting fresh at iteration 1
bug-tracker-1      |      bug-tracker iteration 1 - state saved to agent:bug-tracker:state
bug-tracker-1      |    Visible agents: 1


status-reporter-1  | 🤖 Starting status-reporter (reporting)
status-reporter-1  | 🆕 Redis: redis://agent-memory:6379
status-reporter-1  | ✅ Starting fresh at iteration 1
status-reporter-1  |         status-reporter iteration 1 - state saved to agent:status-reporter:state
status-reporter-1  |    Visible agents: 2
status-reporter-1  |    - bug-tracker: iteration 1

很好!两个 agents 都在运行。现在验证 isolation 是否真的 working:

docker volume ls | grep multi-agent

你应该会看到三个 separate volumes:

local     multi-agent-architecture_bug-tracker-state
local     multi-agent-architecture_reporter-state
local     multi-agent-architecture_shared-memory

每个 agent 都有自己的 isolated storage。Bug tracker 的 state 位于 bug-tracker-state,与 reporter 的 reporter-state volume 完全分离。shared-memory volume 只包含 Redis database,也就是它们真正需要 shared 的东西。

现在 peek inside Redis,看看 agents 如何 organize data:

docker compose exec agent-memory redis-cli KEYS "agent:*"

这会显示 key namespaces:

1) "agent:status-reporter:state"
2) "agent:bug-tracker:state"

看到 pattern 了吗?每个 agent 都使用自己的 namespace(agent:{name}:state)。这可以防止 agents accidentally overwrite 对方的数据。看看里面实际存的内容:

docker compose exec agent-memory redis-cli GET "agent:bug-tracker:state"

Output 显示 agent 当前 state:

"{"agent": "bug-tracker", "type": "monitoring", "iteration": 23, "timestamp": "2025-12-28T14:42:34.540596", "status": "active"}"

每个 agent 都写入自己的 namespace:tracker 写入 agent:bug-tracker:state,reporter 写入 agent:status-reporter:state。它们可以读取彼此的 state 以进行 coordination。注意:这种 separation 是 by convention,而不是 Redis 强制执行的;Redis 本身并不会阻止一个 agent 写入另一个 agent 的 key。真正保护你的,是 agent code 中一致的 key-naming pattern,即每个 agent 只写入以自己 name 为 prefix 的 keys。如果你需要在 production 中 hard enforcement,Redis ACLs 可以限制每个 agent 只能 read 和 write 与自己 namespace 匹配的 keys:

#Create a Redis user for bug-tracker that can only access its own keys
ACL SETUSER bug-tracker on >password ~agent:bug-tracker:* +@read +@write

对于本章 examples,这种 convention 已经足够;但在 production multi-agent system 中,如果 agents 是独立 developed 或 deployed 的,Redis ACLs 值得额外配置。

Testing state persistence and resumption

Agents 正在运行,也已经 isolated。但 production system 的真正测试是:things restart 时会发生什么?每次 restart 都丢失 state 的 agents 对 long-running operations 来说毫无用处。我们验证一下你的 agents 能否跨 container restarts 保持 continuity。

首先,检查 bug tracker 当前在哪里:

docker compose exec agent-memory redis-cli GET "agent:bug-tracker:state"

你会看到类似内容:

{"agent": "bug-tracker", "type": "monitoring", "iteration": 4, "timestamp": "2025-12-28T14:39:31.567116", "status": "active"}

记下 iteration number——这里是 4。现在 restart agent:

docker compose restart bug-tracker

观察发生了什么:

docker compose logs bug-tracker --tail 20 -f

Agent 会从上次位置继续:

🤖 Starting bug-tracker (monitoring)
   Redis: redis://agent-memory:6379
📥 Resuming from iteration 8
✅   bug-tracker iteration 8 - state saved to agent:bug-tracker:state
   Visible agents: 2
   - status-reporter: iteration 7
✅   bug-tracker iteration 9 - state saved to agent:bug-tracker:state

看到了吗?Agent 从 Redis 中读取 iteration 7,并从 iteration 8 恢复。没有 data lost,没有 repeated work。这个 pattern 对 production systems 至关重要——你需要 agents 能在 restarts、updates 和 crashes 之间保持 continuity。

这里还有一个容易被忽略的 benefit:filesystem 和 process isolation。如果 bug tracker agent 被 compromised,attacker 无法读取 status reporter 在 /data 中的 state files,无法修改 reporter 的 environment variables,也无法访问 reporter 的 process space。Blast radius 被部分 contained。

不过也必须清楚:isolation 并不能保护所有东西。Shared Redis instance 仍然是 common attack surface。一个 compromised agent 可以 read 和 write Redis 中任何 key,包括属于其他 agents 的 keys。这意味着 attacker 可以读取另一个 agent 的 state,或者写入 malicious values 来改变它的 behavior。对于无法接受这种风险的 production deployments,Redis ACLs 可以将每个 agent 限制在自己的 key namespace 内:

ACL SETUSER bug-tracker on >password ~agent:bug-tracker:* +@read +@write
ACL SETUSER status-reporter on >password ~agent:status-reporter:* +@read +@write

有了 ACLs,compromised agent 无法访问另一个 agent 的 Redis data。Container isolation 和 Redis ACLs 结合起来,才是真正的 defense in depth。

Building production-ready agent controllers

Multi-agent isolation example 对 demonstration 非常好,但 production systems 需要的不只是多个 isolated containers 并行运行。你需要 centralized coordination——某个东西可以 manage agent lifecycles、efficiently distribute work、monitor health,并提供 visibility,让你知道系统正在发生什么。这就是 agent controller 的作用,而你接下来要构建一个。

Understanding the controller architecture

在深入 code 之前,先理解 controller 实际做什么。可以把它看成 agents 的 traffic cop——它有四个 main responsibilities,用来保持整个 system 顺畅运行:

Agent registration:Agents start up 时,会向 controller announce themselves。Controller 维护一个 registry,记录谁 online 且 ready to work。

Task distribution:Controller 维护 task queue,并将 work 分配给 available agents。Smart distribution 确保不会有 agent idle,而另一个 agent overloaded。

Health monitoring:Agents 发送 heartbeat messages,证明自己 alive。如果 heartbeats 停止,controller 会将它们标记为 inactive,并停止向它们发送 work。

Observability:REST API endpoints 和 web dashboard 让你能 real time 看到发生了什么——哪些 agents active、pending tasks 有多少、success rates 等。

开始构建。进入 agent controller example:

cd ../agent-controller

Project structure 与之前类似,但有 separate files for controller:

agent-controller/
├── docker-compose.yml
├── Dockerfile.controller
├── Dockerfile.agent
├── controller.py
└── agent.py

查看 Docker Compose configuration:

services:
  # Agent Controller - manages all agents
  controller:
    build:
      context: .
      dockerfile: Dockerfile.controller
    ports:
      - "8001:8000"  # Map external 8001 to internal 8000
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - CONTROLLER_NAME=main-controller
    depends_on:
      agent-memory:
        condition: service_healthy
    models:
      - llm

  # Worker Agent 1 - processes data tasks
  worker-1:
    build:
      context: .
      dockerfile: Dockerfile.agent
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - AGENT_NAME=worker-1
      - AGENT_TYPE=data-processor
      - CONTROLLER_URL=http://controller:8000  # Added this
    depends_on:
      agent-memory:
        condition: service_healthy
      controller:
        condition: service_started
    models:
      - llm
    restart: on-failure

  # Worker Agent 2 - processes analysis tasks
  worker-2:
    build:
      context: .
      dockerfile: Dockerfile.agent
    environment:
      - REDIS_URL=redis://agent-memory:6379
      - AGENT_NAME=worker-2
      - AGENT_TYPE=analyst
      - CONTROLLER_URL=http://controller:8000  # Added this
    depends_on:
      agent-memory:
        condition: service_healthy
      controller:
        condition: service_started
    models:
      - llm
    restart: on-failure

  # Redis for shared memory
  agent-memory:
    image: redis:7-alpine
    volumes:
      - agent-state:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"

models:
  llm:
    model: ai/llama3.2:1B-Q8_0
    context_size: 2048

volumes:
  agent-state:

启动它:

docker compose up --build -d

Docker 会创建你需要的一切——controller、两个 worker agents 和 Redis:

[+] Running 6/7
 ✓ Container agent-controller-agent-memory-1 Healthy
 ✓ Container agent-controller-controller-1   Created
 ✓ Container agent-controller-worker-2-1     Created
 ✓ Container agent-controller-worker-1-1     Created

快速检查所有东西是否运行:

docker compose ps

你应该看到四个 containers 都是 up:

NAME                                 STATUS
agent-controller-agent-memory-1      Up (healthy)
agent-controller-controller-1        Up
agent-controller-worker-1-1          Up
agent-controller-worker-2-1          Up

现在看看这些 agents 如何与 controller coordinate。

Agent self-registration and coordination

Worker agents 需要以三种 specific ways 与 controller 通信。我们逐一看。

Registration:Agent start 时,会向 controller announce itself:

def register(self):
    """Register with the controller"""
    response = requests.post(
        f"{self.controller_url}/api/agents/register",
        json={"name": self.agent_name, "type": self.agent_type},
        timeout=5
    )
    return response.status_code == 200

很简单——agent 说:“Hey, I’m worker-1, I’m a data-processor, and I’m ready for work.”

Heartbeats:Agents 需要证明自己仍然 alive:

def send_heartbeat(self):
    """Send heartbeat to controller"""
    requests.post(
        f"{self.controller_url}/api/agents/{self.agent_name}/heartbeat",
        timeout=5
    )

这些 heartbeats 会周期性发生。如果 controller 停止接收 heartbeats,它就知道 agent dead 或 stuck。

Task retrieval:Agents 从 controller pull work:

def get_task(self):
    """Get next task from controller"""
    response = requests.post(
        f"{self.controller_url}/api/tasks/dequeue",
        json={"agent_name": self.agent_name},
        timeout=5
    )
    return response.json() if response.status_code == 200 else None

Agent 会问:“Got any work for me?” Controller 要么返回一个 task,要么说 “Nothing right now.”

下面是它们如何在 main agent loop 中组合在一起:

def run(self):
    """Main agent loop"""
    self.register()
    heartbeat_counter = 0
   
    while True:
        heartbeat_counter += 1
        if heartbeat_counter % 10 == 0:
            self.send_heartbeat()
       
        task = self.get_task()
        if task:
            success, result = self.process_task(task)
            self.complete_task(task['id'], success, result)
            time.sleep(2)
        else:
            print("💤 No tasks available, waiting...")
            time.sleep(5)

注意 heartbeat_counter % 10。Agent 并不会在每次 loop iteration 都发送 heartbeat——那太多了。它每 10 次 iterations 发送一次。这就是 smart resource management。

Testing the REST API

Controller expose REST API endpoints,让你看到正在发生什么。下面测试它们。

获取所有 registered agents 列表:

curl http://localhost:8001/api/agents

你会看到两个 workers 的 complete status:

[  {    "name": "worker-2",    "type": "analyst",    "status": "active",    "tasks_completed": 3,    "tasks_failed": 0,    "last_heartbeat": "2025-12-27T16:49:15.816101"  },  {    "name": "worker-1",    "type": "data-processor",    "status": "active",    "tasks_completed": 3,    "tasks_failed": 0  }]

数据很好!每个 agent 都显示 registration time、last heartbeat timestamp、completion counts 和 status。status: "active" 确认两个 workers 都 healthy。注意 worker-1 已经 completed 5 tasks,而 worker-2 completed 3 tasks——controller tracks everything。

现在 assign 一个 new task:

curl -X POST http://localhost:8001/api/tasks/assign \
  -H "Content-Type: application/json" \
  -d '{"id": "task-123", "description": "Process customer data"}'

Task 会立即进入 queue:

{
  "assigned_type": null,
  "id": "task-123",
  "description": "Process customer data",
  "status": "pending",
  "created_at": "2025-12-27T16:50:02.285182"
}

注意 "assigned_type": null 了吗?这个 task 已经 queued,但尚未 assigned 给 worker。几秒内,某个 agent 会 pick it up。

检查 overall system health:

curl http://localhost:8001/api/stats

这会给你 big picture:

{
  "total_agents": 2,
  "active_agents": 2,
  "inactive_agents": 0,
  "pending_tasks": 0,
  "total_completed": 7,
  "total_failed": 0,
  "success_rate": 100.0
}

Perfect health——7 tasks completed,0 failures,100% success rate。两个 agents 都 active,ready for more work。

Accessing the web dashboard

REST API 很适合 programmatic access,但有时候你只是想看发生了什么。Controller 提供 web dashboard 用于 real-time visibility。在 browser 中打开:

http://localhost:8001

你会看到图 7.7 中的 dashboard,它显示 agent status、task queues 和 success metrics。

image.png

图 7.7:Agent controller web dashboard——实时展示 agent health、task queue depth 和 system-wide success rates,并每 5 秒 auto-refresh

Dashboard 顶部提供四个 key metrics:

  • Total Agents:2(worker-1 和 worker-2)
  • Active Agents:2(both healthy)
  • Pending Tasks:0(no backlog)
  • Success Rate:100.0%(no failures)

下面的 Registered Agents section 会细分每个 worker:

  • worker-2(analyst) :3 tasks completed,0 failed
  • worker-1(data-processor) :5 tasks completed,0 failed

绿色 “active” badges 确认它们都在 sending regular heartbeats。

Task Queue section 显示 “0 pending” 和 “No pending tasks”,表示 workers 能跟上 demand。当 tasks pile up 时,你会看到它们以 IDs、descriptions 和 timestamps 形式列在这里。

底部有一个 Refresh Now button,不过你不太需要它——dashboard 每 5 秒自动 refresh。

这正是 production monitoring 所需要的东西。不需要 parsing logs,也不需要 custom queries——只是清楚地看到 agent health 和 system throughput。你可以快速发现 failing agents,观察 task distribution,并验证 high availability。

Designing agent communication patterns

到目前为止,你的 agents 通过 central controller 通信——workers 调用 REST APIs 进行 register、get tasks 和 report completion。这个 pattern 对 task distribution 很有效,但不是 agents coordinate 的唯一方式。如果 agents 需要 direct notify each other about events 怎么办?或者它们需要不经过 coordinator 就共享 information 怎么办?接下来我们探索可用的 communication patterns。

Understanding communication requirements

在选择 communication pattern 之前,需要先理解你的 agents 之间到底需要一起做什么。不同 coordination needs 对应不同 patterns:

Broadcasting events:当 bug-fixing agent 创建 patch 时,多个 testing agents 可能需要立即知道它。Bug-fixer 不应该负责 track 哪些 testing agents exist,也不应该等待 responses。它只需要 announce “patch created”,然后继续。这就是 event notification。

Direct responses:当 coordinator 给 worker 分配 task 时,它需要确认 worker accepted it。Coordinator 发出 request,并在 proceeding 之前等待 response。这就是 request-response coordination。

Shared data:多个 analysis agents 可能从一个 common dataset 读取,而 preprocessing agent 会更新它。它们都需要访问同一 information,同时不能互相踩踏。这就是 shared state management。

Agent controller 使用 REST APIs 实现 request-response。现在我们来看 event-driven communication,即 agents 如何 react to events published by other agents。

Implementing publish-subscribe with Redis

我们构建一个 concrete example,用于演示 event-driven communication。你会创建一个 writer agent 来 publish patch notifications,并创建 reader agents 来 subscribe 并 receive them。这个 pattern 创建 loose coupling——writer 不知道也不关心谁在 listening。

可以把它想象成 radio broadcast。Radio station(publisher)在某个 frequency(channel)上发送 signal。任何 tuned to that frequency 的 radio(subscriber)都会收到 broadcast。Station 不知道有多少 radios 在 listening,也不需要知道谁拥有它们。

获取 code:

cd chap-07/agent-communication

Project structure 很简单:

agent-communication/
├── docker-compose.yml
├── Dockerfile
├── writer.py
└── reader.py

查看 writer.py 中的 writer agent:

import redis
import json
import time
import os

def main():
    redis_client = redis.from_url(
        os.getenv('REDIS_URL', 'redis://redis:6379'),
        decode_responses=True
    )
   
    print("📥 Writer Agent Starting...")
    print("   Publishing patches to 'patches' channel\n")
   
    patch_id = 1
   
    while True:
        # Create patch metadata
        patch_data = {
            "patch_id": f"patch-{patch_id:03d}",
            "location": f"/data/patch-{patch_id:03d}.diff",
            "agent": "bug-fixer",
            "size": 100 + (patch_id * 10)
        }
       
        print(f" Publishing: {patch_data['patch_id']}")
        print(f"   Size: {patch_data['size']} bytes")
       
        # Publish to Redis channel
        redis_client.publish("patches", json.dumps(patch_data))
       
        patch_id += 1
        time.sleep(5)  # Publish every 5 seconds

if __name__ == "__main__":
    main()

Writer 会 continuous run,创建 patch metadata,并 publish 到 patches channel。注意它没有做什么——它不等待 responses,不检查是否有人 listening,也不 track subscribers。它只是 publish,然后 move on。Fire and forget。

现在看 reader.py 中的 reader agent:

import redis
import json
import os

def main():
    redis_client = redis.from_url(
        os.getenv('REDIS_URL', 'redis://redis:6379'),
        decode_responses=True
    )
   
    print("👂 Reader Agent Starting...")
    print("   Listening for patches on 'patches' channel\n")
   
    # Subscribe to the patches channel
    pubsub = redis_client.pubsub()
    pubsub.subscribe("patches")
   
    print("✅ Subscribed successfully!\n")
   
    # Listen for messages
    for message in pubsub.listen():
        if message['type'] == 'message':
            # Parse the JSON data
            data = json.loads(message['data'])
           
            print(f" Received: {data['patch_id']}")
            print(f"   Location: {data['location']}")
            print(f"   Size: {data['size']} bytes")
            print(f"   Processing patch...\n")

if __name__ == "__main__":
    main()

Reader 会 subscribe 到 patches channel 并等待。当 messages arrive 时,它立即 process。这里有个有趣的地方:你可以运行多个 readers,每个 reader 都会 independent receive all messages。这就是 pub/sub 的强大之处。

Dockerfile 很 straightforward:

FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir redis
COPY *.py .
CMD ["python", "-u", "reader.py"]

-u flag 很重要——它让 Python 以 unbuffered mode 运行,这样你可以立即看到 output,而不会被缓存。

下面是 docker-compose.yaml

services:
  # Writer agent - publishes patch events
  writer:
    build: .
    command: python -u writer.py
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy

  # Reader agent - subscribes to patch events
  reader:
    build: .
    command: python -u reader.py
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy

  # Redis for pub/sub
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    ports:
      - "6379:6379"

启动:

docker compose up --build

观察 writer 每 5 秒 publish events:

writer-1  | 📥 Writer Agent Starting...
writer-1  |    Publishing patches to 'patches' channel
writer-1  |
writer-1  | 📥 Publishing: patch-001
writer-1  |    Size: 110 bytes
writer-1  | 📥 Publishing: patch-002
writer-1  |    Size: 120 bytes

Reader 会立即收到它们:

reader-1  | 👂 Reader Agent Starting...
reader-1  |    Listening for patches on 'patches' channel
reader-1  |
reader-1  |  Subscribed successfully!
reader-1  |
reader-1  | 📨 Received: patch-001
reader-1  |    Location: /data/patch-001.diff
reader-1  |    Size: 110 bytes
reader-1  |    Processing patch...
reader-1  |
reader-1  | 📨 Received: patch-002
reader-1  |    Location: /data/patch-002.diff
reader-1  |    Size: 120 bytes
reader-1  |    Processing patch...

现在测试一个有趣的情况。Scale readers 时会发生什么?用 Ctrl + C 停止 system,然后以三个 readers restart:

docker compose up --build --scale reader=3

现在观察——三个 readers 都会收到同样 events:

writer-1  | 📥 Publishing: patch-002
writer-1  |    Size: 120 bytes
reader-1  | 📨 Received: patch-002
reader-2  | 📨 Received: patch-002
reader-3  | 📨 Received: patch-002
reader-1  |    Location: /data/patch-002.diff
reader-2  |    Location: /data/patch-002.diff
reader-3  |    Location: /data/patch-002.diff
reader-1  |    Size: 120 bytes
reader-2  |    Size: 120 bytes
reader-3  |    Size: 120 bytes

看到了吗?每个 reader 都 independent process 同样的 patches。当多个 agents 需要 react to the same event 时,这很完美——例如 testing agents 都对 new patches 运行 tests,monitoring agents 都记录同样 events,或者 analysis agents 都处理同样 data。

这里的 benefits 很清楚:

  • Writer doesn't block——它 publish 后继续运行
  • 可以添加或移除 readers,而无需触碰 writer
  • Multiple readers independent process
  • 没有 request-response cycle 拖慢速度

Choosing the right communication pattern

现在你有了三种 communication patterns:通过 REST APIs 的 request-response、通过 Redis channels 的 publish-subscribe,以及通过 Redis keys 的 shared state。我们澄清什么时候使用哪一种。

Use publish-subscribe:当一个 agent 需要 notify multiple others about events 时。Publisher 不关心谁在 listening,也不关心有多少 listeners。

Examples:Monitoring agents broadcast alerts,data pipeline agents publish “new data available” notifications,或者任何多个 agents 需要 react to same event 的场景。

Use request-response:当一个 agent 需要 confirmation:另一个 agent 已经完成 work。Caller 会等待 response 再继续。

Examples:你之前构建的 agent controller system、需要 acknowledgment 的 administrative commands,或者任何需要 synchronous coordination 的地方。

Use shared state:当多个 agents 需要 read 和 write common data。带 proper locking 的 Redis keys 可以 prevent race conditions。

Examples:Distributed counters、多个 agents 读取的 configuration,或跨 restarts 持久化的 agent state。

现实情况是:大多数 production systems 会 combine patterns。你的 agent controller 使用 REST APIs 做 task assignment(request-response),并使用 Redis keys 做 agent state(shared state)。你可以再加入 pub/sub,让 agents broadcast completion events 给 monitoring systems。

关键在于理解你的 agents 实际需要哪种 coordination,然后选择自然匹配的 pattern。先从简单开始——以后总可以添加更 sophisticated 的 coordination。

Configuring Docker networking for multi-agent systems

Communication patterns 定义了 agents 如何 exchange messages。但还有一个更 fundamental 的问题:agents 首先如何 find each other?在一个 containers 会 start、stop 和 scale 的 dynamic system 中,hardcoding IP addresses 不可行。Docker networking 提供的 automatic service discovery 优雅地解决了这个问题。

Docker Compose 提供 automatic DNS-based service discovery,简单说就是 agents 可以通过 name 互相 find。当你在 docker-compose.yml 中定义 services 时,Docker 会创建 DNS entries,使 agents 可以使用 service names 连接,而不必 memorize IP addresses。

Building a service discovery example

我们构建一个 practical example——coordinator-worker system,其中 coordinator 自动 discover workers,Docker 会在多个 worker replicas 之间 load-balance requests。Agents 不知道任何 IP addresses;它们只使用 service names,然后让 Docker 处理剩下的事情。

我们会创建一个 coordinator agent,它只使用 service name 向 worker agents 发送 tasks。Docker 处理所有其他事情——DNS resolution、load balancing、dynamic scaling。Agents 不需要知道这些 complexity。

获取 code:

cd Operational-AI-with-Docker/chap-07/agent-discovery

Project structure:

agent-discovery/
├── docker-compose.yml
├── Dockerfile.coordinator
├── Dockerfile.worker
├── coordinator.py
└── worker.py

查看 worker.py 中的 worker agent。它只是一个 simple Flask service:

from flask import Flask, jsonify
import os
import socket

app = Flask(__name__)

WORKER_ID = os.getenv('WORKER_ID', socket.gethostname())

@app.route('/process')
def process():
    """Simulate processing work"""
    return jsonify({
        "worker_id": WORKER_ID,
        "message": f"Task processed by {WORKER_ID}",
        "hostname": socket.gethostname()
    })

if __name__ == '__main__':
    print(f"🔧 Worker {WORKER_ID} starting...")
    print(f"   Hostname: {socket.gethostname()}")
    app.run(host='0.0.0.0', port=8000)

很简单。每个 worker 返回自己的 hostname,让你看到哪个 instance 处理了每个 task。当我们测试 load balancing 时,这会很重要。

现在看 coordinator.py

import requests
import time
import os

def main():
    worker_url = os.getenv('WORKER_URL', 'http://worker:8000')
   
    print("🎯 Coordinator Agent Starting...")
    print(f"   Worker URL: {worker_url}\n")
   
    time.sleep(3)  # Wait for workers to be ready
    task_id = 1
   
    while True:
        try:
            print(f"\n 📥 Sending task #{task_id} to worker...")
            response = requests.get(f"{worker_url}/process", timeout=5)
           
            if response.status_code == 200:
                data = response.json()
                print(f"✅ Task completed by: {data['worker_id']}")
                print(f"   Hostname: {data['hostname']}")
                print(f"   Message: {data['message']}")
               
        except Exception as e:
            print(f"❌ Error calling worker: {e}")
       
        task_id += 1
        time.sleep(3)

if __name__ == "__main__":
    main()

关键行是:

worker_url = os.getenv('WORKER_URL', 'http://worker:8000')

Coordinator 会调用 http://worker:8000,不需要知道任何 actual IP addresses。Docker DNS 会处理剩下的事。

import requests
import time
import os

def main():
    worker_url = os.getenv('WORKER_URL', 'http://worker:8000')
   
    print("🎯 Coordinator Agent Starting...")
    print(f"   Worker URL: {worker_url}\n")
   
    time.sleep(3)  # Wait for workers to be ready
    task_id = 1
   
    while True:
        try:
            print(f"\n📥 Sending task #{task_id} to worker...")
            response = requests.get(f"{worker_url}/process", timeout=5)
           
            if response.status_code == 200:
                data = response.json()
                print(f"✅ Task completed by: {data['worker_id']}")
                print(f"   Hostname: {data['hostname']}")
                print(f"   Message: {data['message']}")
               
        except Exception as e:
            print(f"❌ Error calling worker: {e}")
       
        task_id += 1
        time.sleep(3)

if __name__ == "__main__":
    main()

下面是 docker-compose.yml

services:
  # Coordinator - discovers workers by service name
  coordinator:
    build:
      context: .
      dockerfile: Dockerfile.coordinator
    environment:
      - WORKER_URL=http://worker:8000
    depends_on:
      - worker

  # Worker - can be scaled dynamically
  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    deploy:
      replicas: 1

deploy.replicas setting 控制运行多少 worker instances。不过最酷的是:你可以在 runtime 用 --scale flag override 它。

从 single worker 开始:

docker compose up --build

观察 coordinator discover 并调用 worker:

worker-1      | 🔧 Worker 640d12fed193 starting...
worker-1      |    Hostname: 640d12fed193
coordinator-1 | 🎯 Coordinator Agent Starting...
coordinator-1 |    Worker URL: http://worker:8000
coordinator-1 |
coordinator-1 | 📥 Sending task #1 to worker...
coordinator-1 |  Task completed by: 640d12fed193
coordinator-1 |    Hostname: 640d12fed193
coordinator-1 | 📥 Message: Task processed by 640d12fed193

Coordinator 只使用 service name worker 调用了 worker。Docker DNS 自动把它解析到了 actual IP address。

Testing dynamic scaling and load balancing

Coordinator 仅用 service name 就找到了 worker。现在来看看更有意思的部分——当你 scale up 时会发生什么?我们在 system 运行时添加更多 workers,并观察 Docker 如何自动在它们之间 distribute work。

保持 system 运行,打开第二个 terminal,并 scale up 到三个 workers:

docker compose up -d --scale worker=3

Docker 会启动两个更多 workers:

✓ Container agent-discovery-worker-1      Running
✓ Container agent-discovery-worker-2      Created
✓ Container agent-discovery-worker-3      Created
✓ Container agent-discovery-coordinator-1 Running

观察 coordinator logs,看看 load balancing 的实际效果:

docker compose logs coordinator -f

一开始,所有 tasks 仍然会发给第一个 worker:

coordinator-1 | ✅ Task completed by: 640d12fed193
coordinator-1 | ✅ Task completed by: 640d12fed193
coordinator-1 | ✅ Task completed by: 640d12fed193

但一旦 new workers 完成 startup,Docker 就开始在三个 workers 之间 distribute tasks:

coordinator-1 | 📥 ending task #18 to worker...
coordinator-1 | ✅ Task completed by: ebfa6cf86754
coordinator-1 |
coordinator-1 | 📥 Sending task #19 to worker...
coordinator-1 | ✅ Task completed by: ebfa6cf86754
coordinator-1 |
coordinator-1 | 📥 Sending task #20 to worker...
coordinator-1 | ✅ Task completed by: fafe4d7988aa
coordinator-1 |
coordinator-1 | 📥 Sending task #21 to worker...
coordinator-1 | ✅ Task completed by: 640d12fed193

看到三个不同 hostnames 了吗?Docker 正在对所有 worker replicas 进行 load-balancing。验证一下:

docker compose ps

你会看到三个 workers 都在 running:

NAME                            CONTAINER ID
agent-discovery-coordinator-1   627b453bf8f5
agent-discovery-worker-1        640d12fed193
agent-discovery-worker-2        ebfa6cf86754
agent-discovery-worker-3        fafe4d7988aa

令人惊讶的是:coordinator code 从未改变。Docker 的 DNS-based service discovery 会自动对 worker service 的所有 replicas 进行 load-balance。这就是 multi-agent systems 如何 horizontally scale:你添加更多 worker containers,Docker 自动 distribute work。

这个 pattern 适用于任何 agent architecture。Monitoring coordinator 可以 discover multiple data collectors。Task distributor 可以 load-balance across processing agents。Orchestrator 可以根据 workload scale specialist agents。Agents 只需要知道 service names——Docker 处理 complexity。

Implementing security and monitoring essentials

你已经构建了能够 communicate、coordinate 和 scale 的 agents。Production deployment 还剩两个关键 pieces:security controls,用于在 agent 被 compromised 时限制 damage;observability,用于看见 agents 内部正在发生什么。接下来实现二者。

Implementing agent sandboxing

Docker 提供多个 security mechanisms,它们协同形成 defense-in-depth。你将配置一个 sandboxed agent,它拥有 read-only filesystem、dropped Linux capabilities、non-root user execution 和 network isolation。每一层都增加 protection——即使某一层 failed,其他层仍然能够限制 damage。

获取 security example:

cd Operational-AI-with-Docker/chap-07/agent-security

查看 agent.py 中的 agent code。它实现 structured JSON logging,并测试 security boundaries:

import logging
import json
import time
import os
import sys

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "message": record.getMessage(),
            "agent_id": os.getenv("HOSTNAME", "unknown")
        }
       
        if hasattr(record, 'task_id'):
            log_data['task_id'] = record.task_id
        if hasattr(record, 'duration'):
            log_data['duration'] = record.duration
           
        return json.dumps(log_data)

logger = logging.getLogger("agent")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)

def main():
    logger.info("🔒 Secure Agent Starting...")
    logger.info(f"   Running as: {os.getuid()}:{os.getgid()}")
   
    # Test filesystem permissions
    try:
        with open('/test-write.txt', 'w') as f:
            f.write("test")
        logger.info("✅ Filesystem is writable")
    except Exception as e:
        logger.warning(f"⚠ Filesystem is read-only: {str(e)}")
   
    # Test tmpfs write
    try:
        with open('/tmp/test.txt', 'w') as f:
            f.write("test")
        logger.info("✅ /tmp is writable (tmpfs)")
    except Exception as e:
        logger.error(f"❌ /tmp write failed: {str(e)}")
   
    # Process tasks with metrics
    task_id = 1
    while True:
        start = time.time()
        logger.info(f"Starting task", extra={"task_id": f"task-{task_id:03d}"})
       
        time.sleep(1)  # Simulate work
       
        duration = time.time() - start
        logger.info(f"Task completed", extra={
            "task_id": f"task-{task_id:03d}",
            "duration": round(duration, 2)
        })
       
        task_id += 1
        time.sleep(3)

Agent 故意测试 security boundaries——它尝试写入 root filesystem(应该 fail),也尝试写入 /tmp(应该 succeed)。每个 log line 都输出 valid JSON,并且结构一致。你很快会看到为什么这对 monitoring 非常关键。

下面是启用了所有 security layers 的 docker-compose.yaml

services:
  secure-agent:
    build: .
    read_only: true  # Entire filesystem is read-only
    tmpfs:
      - /tmp  # Only /tmp is writable (in-memory)
    cap_drop:
      - ALL  # Drop all Linux capabilities
    security_opt:
      - no-new-privileges:true  # Prevent privilege escalation
    user: "1000:1000"  # Run as non-root user
    networks:
      - isolated-network

networks:
  isolated-network:
    internal: true  # No external network access

逐层解释:

read_only: true 防止 agents 修改自己的 code 或安装 malicious software——整个 filesystem 被 locked down。

tmpfs mount 给 agents 一个 writable space 用于 temporary files,但它只是 in-memory;没有任何东西会 persist 到 disk。

cap_drop: ALL 移除所有 Linux capabilities,例如 raw socket access 和 system administration——agent 不能做任何 privileged 操作。

no-new-privileges 防止通过 setuid binaries 获得 additional privileges。

user: "1000:1000" 让 agent 以 non-root user 运行,因此即使其他 protections fail,它也无法修改 system files。

internal network 阻止 direct internet access,同时仍然允许 internal service communication。

启动:

docker compose up --build

观察 agent 测试 security boundaries:

{"timestamp": "2025-12-29 13:54:09,336", "level": "INFO", "message": "🔒 Secure Agent Starting...", "agent_id": "506278d768cc"}
{"timestamp": "2025-12-29 13:54:09,337", "level": "INFO", "message": "   Running as: 1000:1000", "agent_id": "506278d768cc"}
{"timestamp": "2025-12-29 13:54:09,337", "level": "WARNING", "message": "⚠ Filesystem is read-only: [Errno 30] Read-only file system: '/test-write.txt'", "agent_id": "506278d768cc"}
{"timestamp": "2025-12-29 13:54:09,337", "level": "INFO", "message": "✅ /tmp is writable (tmpfs)", "agent_id": "506278d768cc"}
{"timestamp": "2025-12-29 13:54:09,337", "level": "INFO", "message": "Starting task", "agent_id": "506278d768cc", "task_id": "task-001"}
{"timestamp": "2025-12-29 13:54:10,342", "level": "INFO", "message": "Task completed", "agent_id": "506278d768cc", "task_id": "task-001", "duration": 1.0}

Agent 确认它正在以 non-root user(1000:1000)运行,不能写 root filesystem,但可以写 /tmp。每个 log line 都是 valid JSON——你马上会看到这为什么重要。

Verifying security constraints

Agent logs 显示 security 正按设计工作,但我们主动测试这些 boundaries。你会尝试 break the rules,并确认 container 会 block you。

打开第二个 terminal,尝试在 root filesystem 中创建 file:

# Test read-only filesystem
docker compose exec secure-agent touch /test.txt

Filesystem 会阻止它:

touch: cannot touch '/test.txt': Read-only file system

很好。现在测试 tmpfs 是否能工作:

dockercomposeexecsecure-agenttouch/tmp/test.txt&&echo"✅ tmpfs write succeeded"

这会成功:

✅ tmpfs write succeeded

这些 tests 证明 agent 无法修改自己的 filesystem,但有 temporary data 的 writable space。Defense in depth 正按设计工作。

Parsing structured logs

你实现 JSON logging 是有原因的——它会把 monitoring 从 grepping through text files 转变为 querying structured data。看看如何从 agent logs 中提取 specific information。

获取所有 completed tasks 及其 metrics:

# Get all completed tasks with metrics
docker compose logs secure-agent | grep -o '{.*}' | jq 'select(.message == "Task completed") | {task_id, duration}'

这会返回干净的 task metrics:

{
  "task_id": "task-001",
  "duration": 1.0
}
{
  "task_id": "task-002",
  "duration": 1.01
}
{
  "task_id": "task-003",
  "duration": 1.01
}

想找 security warnings?

docker compose logs secure-agent | grep -o '{.*}' | jq 'select(.level == "WARNING")'

这会展示 filesystem protection 的实际效果:

{
  "timestamp": "2025-12-29 13:54:09,337",
  "level": "WARNING",
  "message": "⚠ Filesystem is read-only: [Errno 30] Read-only file system: '/test-write.txt'",
  "agent_id": "506278d768cc"
}

或者获取 agent startup information:

docker compose logs secure-agent | grep -o '{.*}' | jq 'select(.message | contains("Starting")) | {timestamp, agent_id, message}'

Filtered startup events:

{
  "timestamp": "2025-12-29 13:54:09,336",
  "agent_id": "506278d768cc",
  "message": "🔒 Secure Agent Starting..."
}

注意到这里的 powerful 之处了吗?Structured logging 将 agent monitoring 从 grepping through text files 变成了 querying structured data。你可以用 Elasticsearch 或 Grafana 等 standard log aggregation tools 构建 dashboards、设置 alerts,并对 agent behavior 执行 analytics。

这个 security 和 monitoring foundation 为 safe autonomous agent deployment 提供了所需基础。Sandboxing 会限制 compromised agents 造成的 damage,而 structured logging 则提供 troubleshooting 和 compliance 所需的 visibility。

Summary

你已经构建了 autonomous AI agents——它们是无需 human intervention 即可 continuous operate 的 systems。它们不是简单的 request-response systems;它们会 perceive environment、reason about goals、make decisions、take actions,并基于 results 不断 iterate。

Continuous agent loop(perceive → reason → plan → act → observe → iterate)将 autonomous agents 与 interactive AI applications 区分开来。你用四个 core components 实现了这个 loop:由 Docker Model Runner 驱动的 reasoning engine、通过 MCP Gateway 实现的 tool access layer、使用 Redis 的 persistent memory,以及负责 orchestrate 一切的 agent controller。

Container isolation 被证明对 security 至关重要。每个 agent 都通过 named volumes 拥有自己的 filesystem;environment variables 被隔离,防止 credential leakage;separate network namespaces 支持 independent operation。多个 agents 共享一个 single LLM model,以提高 cost efficiency。你通过 volume inspection、Redis key namespaces 和 state persistence testing 验证了这种 isolation。

你构建了 production-grade agent controllers,提供 REST API endpoints 和 web dashboards,用于 real-time visibility。Controller 管理多个 worker agents,通过 Redis queues 分发 tasks,并 track completion status。Agent communication patterns——用于 event-driven coordination 的 Redis pub/sub,以及用于 dynamic communication 的 Docker automatic service discovery——使 multi-agent collaboration 能够在 loose coupling 和 automatic load balancing 下进行。

Security fundamentals 包括 agent sandboxing with read-only filesystems、dropped Linux capabilities 和 internal networks。Structured logging 提供 troubleshooting 和 performance analysis capabilities。

你构建的 infrastructure 支持 autonomous agents 持续运行、gracefully handle failures,并在 isolated Docker containers 中安全协作。这些 patterns 为结合 AI reasoning 与 container security 的 production deployments 提供了 foundation。

在第 8 章中,你将把这些 patterns 扩展到 multi-model architectures,针对不同 tasks orchestrate specialized models,并构建能够利用不同 model families strengths 的 systems。