福生无量摸鱼天尊

github源码阅读:(三)多agent管理台之Multica

主页:https://multica.ai/

github link:https://github.com/multica-ai/multica

概述

一图胜千言,这个项目是多agent的管理项目:

我最关心三件事:

  • 多agent的harness

  • task的生命周期是如何运作的

多agent的harness

agent的检索召回

Context Engineering

每次任务即将执行时实时生成生成的 CLAUDE.md,是一份动态适配的检索说明书:buildMetaSkillContent()(server/internal/daemon/execenv/runtime_config.go)

  生成的 CLAUDE.md 结构:
  ├─ Agent Identity      ← 来自 agent.instructions (数据库字段)
  ├─ Available Commands  ← 硬编码的 CLI 命令参考 (所有 Agent 通用)
  ├─ Repositories        ← 来自 workspace.repos (数据库 JSONB 字段)
  ├─ Workflow            ← 根据 trigger 类型动态选择:
  │   ├─ assignment → 完整工作流 (status 管理 + 代码实现 + PR)
  │   ├─ comment    → 只读 + 回复工作流
  │   └─ chat       → 交互式模式
  ├─ Skills              ← 来自 agent_skill join 查询
  └─ Output Guidelines   ← 硬编码的输出准则

这份claude.md是在task执行之前生成的:

  pollLoop()                          // daemon.go:684  — daemon 主循环
    └─ handleTask()                   // daemon.go:771  — 拿到一个任务
         └─ runTask()                 // daemon.go:875  — 准备执行
              ├─ execenv.Prepare()    // execenv.go:68  — 创建隔离目录
              ├─ InjectRuntimeConfig() // daemon.go:924 — ★ 这里生成 CLAUDE.md
              │    └─ buildMetaSkillContent() // runtime_config.go:33
              └─ backend.Execute()    // daemon.go:979 — 启动 Agent CLI

在构建claude.md之前就会动态的组装上下文:(daemon.go)

  // runTask() 中组装动态上下文
  taskCtx := execenv.TaskContextForEnv{
      IssueID:           task.IssueID,
      TriggerCommentID:  task.TriggerCommentID,   // ← 决定走哪套 workflow
      AgentInstructions: instructions,             // ← 来自 agent 表当前值
      AgentSkills:       convertSkillsForEnv(skills), // ← 来自 agent_skill join
      Repos:             convertReposForEnv(task.Repos), // ← 来自 workspace.repos
      ChatSessionID:     task.ChatSessionID,       // ← 决定是否 chat 模式
  }

  // 每次执行都重新生成
  execenv.InjectRuntimeConfig(env.WorkDir, provider, taskCtx)

Harness Engineering

Context Engineering 写的 CLAUDE.md 告诉 Agent "你可以用 multica issue get 来读issue"。但这只是"说明书"——好比给了你一张超市会员卡的使用指南,却没给你卡本身。

上图就对应代码中 runTask() 的装配过程:(daemon.go)

  agentEnv := map[string]string{
      "MULTICA_TOKEN":        d.client.Token(),       // ② 认证凭证
      "MULTICA_SERVER_URL":   d.cfg.ServerBaseURL,    // API 地址
      "MULTICA_WORKSPACE_ID": task.WorkspaceID,        // ③ workspace 隔离
      "MULTICA_AGENT_NAME":   agentName,               // Agent 身份
      "MULTICA_AGENT_ID":     task.AgentID,
      "MULTICA_TASK_ID":      task.ID,
  }
  // ① 把 multica 二进制所在目录插到 PATH 最前面
  agentEnv["PATH"] = binDir + string(os.PathListSeparator) + os.Getenv("PATH")

Identity & Auth

Agent 调用 multica issue get <id> 后,CLI 发出的 HTTP 请求要过三道关卡:

具体来说:

  • 关卡 1 —— 认证(MULTICA_TOKEN → Bearer token):

    • "MULTICA_TOKEN": d.client.Token() , daemon.go — Harness 注入的 token

  • 关卡 2 —— Workspace 鉴权(RequireWorkspaceMember 中间件):

    • MULTICA_WORKSPACE_ID 环境变量让 CLI 自动带上这个 header。

    • 这保证 Agent 只能查自己 workspace 的数据——Agent A 在 workspace X 里,绝对查不到 workspace Y 的 issue。

  // middleware/workspace.go
  func RequireWorkspaceMember(next http.Handler) http.Handler {
      // 从 X-Workspace-ID header 取 workspace_id
      // DB 查询:这个人是不是这个 workspace 的成员?
      // 不是 → 403
  }
  • 关卡 3 —— Actor 解析(resolveActor()):

    • 这一步的意义是可追溯:Agent 发的评论标记为 author_type = 'agent',创建的 issue 标记为 creator_type = 'agent'。

    • 你在 UI 上看到一条评论,能清楚知道这是BackendBot 说的,不是张三说的。

  // handler/handler.go
  func (h *Handler) resolveActor(r *http.Request) (actorType string, actorID string) {
      agentID := r.Header.Get("X-Agent-ID")
      taskID := r.Header.Get("X-Task-ID")
      if agentID != "" && taskID != "" {
          // 交叉验证:这个 task 确实属于这个 agent?
          return "agent", agentID
      }
      return "member", userID
  }

Transport

transport模块是在讲数据在 Agent 和 Server 之间怎么走, Agent 检索数据的完整路径:

这里有一个容易忽略的细节:--output json 不是可选的格式美化,而是 Agentic RAG 的核心需求。没有这个标志,CLI会输出人类可读的表格(带对齐、颜色代码),Agent 很难解析。

加上 --output json 后返回的是干净的结构化 JSON,Agent 可以精确地按字段读取title、status、assignee_id 等。