📄 设计文档智能体发现 · 能力索引 · DAG 编排 · 实时 Trace

← 返回控制台 索引 JSON

Agent 发现与 DAG 编排系统 · 设计文档

⚠ 架构已迁移(2026-10-29):本文描述的是 v2.0 的 DNS SRV + 静态 dns/agents.db 方案, 已被 SVCB + CoreDNS 动态注册 取代。现网实现见 docs/svcb-registration.md,本文仅作历史设计留档, 其中的 agents.db、_tcp SRV 查询名、固定 IP 等细节均已不再适用。 版本 2.0.0 · 域名 agents.internal · 控制台 http://192.168.1.30:18089/ · 本页 /docs 本文所有数字均来自 2026-10-23 在 9400f 上的真实运行结果(GET /api/discovery、GET /api/traces),不是估算值。

0. 这个系统要验证什么

它不是「让几个 Agent 串起来跑通」的演示,而是一组可被证伪的工程命题的验证装置。每个命题都写清了如果不成立,会在哪里暴露,以及用什么指标判定。

V1 · 能力寻址能否完全取代硬编码调用链

命题:一个任务的执行路径可以完全由「运行时声明的能力」推导出来,调用方不需要知道任何 Agent 的地址、数量或实现。

判定:GET /api/discovery 中 7 个能力全部 available=true 且 source=role_srv,srv_fallbacks=0;编排全程没有任何硬编码 endpoint 常量。

若失败:会出现某个能力 available=false,或必须走 generic_srv_filtered 兜底才能凑齐节点。

实测:7/7 能力 role_srv 直接命中,兜底路径一次都没触发,索引 149.04ms 完成。

V2 · 发现成本能否与 Agent 总数解耦

命题:查询成本应随能力数量增长,而不是随 Agent 实例数量增长。加十个同能力的 Agent 副本,不应让索引变慢。

判定:SRV 查询次数 ≈ 能力数 × 单次查询的记录数,而不是「先拉全量再逐个抓 Card」。

若失败:srv_queries 或 card_fetches 会随实例数线性上升。

实测:srv_queries=56、card_fetches=48、card_cache_hits=57、card_failures=0 —— 缓存命中数高于实际抓取数,说明重复索取确实被拦住了。

V3 · 局部故障能否被限制在局部

命题:单个 Agent 挂掉、Card 超时、或上游模型 5xx,不应让整条任务链中断。

判定:故障发生时应出现 node_attempt_failed → node_retry → 换候选成功,或该节点降级而其它节点继续;最终状态是 degraded 而不是 failed。

若失败:任何一次单点异常都会让 status=failed 并且后续节点全部不执行。

机制:候选链回退 + 8s 负缓存 + return_exceptions=True + provider 回退链(xmkai → deepseek)。

V4 · 串行延迟能否被 DAG 分层真正压掉

命题:互不依赖的节点必须并发执行,总耗时等于关键路径而不是节点耗时之和。

判定:对比「所有节点延迟之和」与「实际端到端耗时」。

若失败:实测耗时 ≈ 各节点之和,说明分层没生效,仍在排队。

实测(trace ed320e1a6e93):

项值
6 节点延迟之和61,439 ms
分层后关键路径41,343 ms(6148 + 15400 + 5454 + 14341)
实际端到端53,223 ms
并发省下的时间20,096 ms(32.7%)

事件流里能看到真实的分层推进:layer_started{0,[n1,n2],parallel} → {1,[n3,n4],parallel} → {2,[n5],serial} → {3,[n6],serial}。剩余 11.9s 是索引、规划模型调用与交付物拼装的开销。

V5 · 编排过程能否被完整复现

命题:任何一次任务事后都应能回答「查了哪些 DNS、每步考虑了谁、实际绑定了谁、Agent 收到了什么上下文、输出了什么」。

判定:trace 详情里 nodes(每节点 Agent/依赖/分组/耗时/字数)+ events(45 条,含 node_input 全文)+ plan 三者齐全,且前端能边跑边看。

若失败:只能拿到最终字符串,出问题必须加日志重跑。

实测:ed320e1a6e93 留下 6 节点、45 条事件、完整 plan,可回放每一层的进出。

V6 · 语义相近的任务能否被区分开

命题:「写一份派对策划」与「实现一个下线接口」都含「方案/步骤」字样,但必须路由到不同能力的 Agent。

判定:前者走 writing,后者走 implementation,且在归一化阶段的 warnings 里能查到改判痕迹。

若失败:两个任务都被送进 coder,产出无法执行的代码或没有正文的方案。

实测:设计一个用户下线接口 → implementation;设计一份生日派对策划 → writing。


1. 解决了什么问题

1.1 问题域:多智能体协作落地时的六个硬约束

把一个任务交给多个专职 Agent 完成,工程上真正卡住的不是「Agent 能不能写」,而是下面六件事:

#问题具体表现后果
P1端点耦合编排方必须知道每个 Agent 的地址;上线新 Agent 要改代码、重建镜像、重启容器扩容等于发版,横向扩展不成立
P2全量枚举每次任务先拉全部 Agent 再逐个过滤成本 O(实例数),实例越多越慢
P3准静态数据被当动态数据每请求都重新拉取 Agent Card延迟与上游压力线性叠加
P4单点故障连锁任意一个 Agent 挂掉或超时,整条链路抛异常中断局部故障 = 全局失败
P5扁平调用只有「串行几步」的概念,互不依赖的节点也排队延迟 = 所有节点之和
P6黑盒只有最终字符串,看不到 DNS、绑定、耗时、上下文出问题只能加日志重跑

这六条正是 V1~V5 要逐一否证的对象 —— 命题与问题的对应关系:V1↔P1、V2↔P2+P3、V3↔P4、V4↔P5、V5↔P6。

1.2 对应解法

问题解法代码位置
P1DNS 即注册中心。Agent 上线只需在 dns/agents.db 加 A 记录 + _<capability>._tcp SRV 记录,不改代码不重启dns/agents.db、dns/Corefile
P2按能力切片查询:_planning._tcp / _research._tcp … 成本 O(能力数),与 Agent 总数无关index.py: _records_for_capability
P3TTL 缓存 + 随机抖动 + single-flight:60s ± 10s 抖动,并发同 key 只放一个请求出去index.py: card()
P4候选链回退:priority/weight 排序取候选,alternates 逐个重试;Card 抓不到写 8s 负缓存;整条链路降级为 degraded 而非 failedindex.py: candidates/pick、agent.py: run_node
P5DAG + 拓扑分层:模型产出 depends_on,同层并发、层间串行plan.py: levels()、agent.py: run_pipeline
P6trace 落库 + SSE 直播:SQLite 三张表记录节点/事件/计划,浏览器边跑边看trace_store.py、dashboard.py、web.py

1.3 附带解决的语义歧义(V6)

「写一份派对策划」和「实现一个下线接口」都含「方案/步骤」这类词,只靠关键词会把它们路由到同一个 Agent。系统做了两道判定:

  1. 规划器 prompt 明确写死:implementation 只用于交付可运行代码/配置/接口改动;写方案、策划、文案、报告一律 writing。
  2. 归一化阶段硬校验:若节点能力是 implementation,但该节点的 purpose 里没有任何编码信号(且原文也不是编码任务),强制改判为 writing,并写入 warnings。

实测效果:设计一个用户下线接口 → 走 implementation;设计一份生日派对策划 → 走 writing。


2. 设计思路

2.1 四层结构

┌──────────────────────────────────────────────────────────────┐
│  接入层     浏览器 / CLI                                     │
│             POST /api/start  →  GET /api/stream/{trace_id}    │
├──────────────────────────────────────────────────────────────┤
│  观测层     dashboard (:18089)                               │
│             静态页面 + /api/run 代理 + SSE + trace 查询/删除   │
├──────────────────────────────────────────────────────────────┤
│  编排层     analyst-agent                                    │
│             能力索引 → DAG 计划 → 分层执行 → 结果拼装          │
├──────────────────────────────────────────────────────────────┤
│  发现层     agent-dns (CoreDNS :53)  +  Agent Card            │
│             agents.db(A / 通用 SRV / 角色 SRV)               │
├──────────────────────────────────────────────────────────────┤
│  执行层     planner / researcher / verifier / coder /         │
│             writer / synthesizer (各自 :8000)                │
└──────────────────────────────────────────────────────────────┘

设计上让发现层不依赖编排层:CoreDNS 只认 zone 文件,即使 analyst 挂了,任何新进程仍可用同一条 DNS 查询找到全部 Agent。编排层也不持有 Agent 列表 —— 每次任务都从索引现取,索引本身又是 DNS + Card 的投影。

2.2 能力目录(CATALOG)

能力目录是唯一的枚举,同时被三处消费:规划器 prompt 的可用能力表、索引的切片维度、Card 的能力白名单。

能力语义落到哪个 Agent
planning任务拆解、步骤规划、依赖关系与验收标准planner-agent
research事实核查、证据来源、不确定项标注researcher-agent
security_review安全风险审查、验证命令、通过条件verifier-agent
implementation仅用于交付可运行代码、配置文件、接口定义或改动coder-agent
writing撰写非代码交付物:方案、策划、文案、报告、清单、时间线writer-agent
validation验收检查与质量校验verifier-agent
synthesis汇总全部上游结果,输出最终结论与风险清单synthesizer-agent

注意 verifier-agent 同时承载 security_review 和 validation —— 一个实例注册多种能力,是 SRV 按能力切片后自然支持的结果,不需要额外抽象。

2.3 为什么用 DNS SRV 而不是手写注册表

dns/agents.db 里三类记录:

; 1) 地址:给 SRV target 落地
planner-agent IN A 172.19.0.6

; 2) 通用索引:仅兜底,按能力查不到角色型 SRV 时才回落到这里,再按 Card 过滤
_agent._tcp IN SRV 10 10 8000 planner-agent.

; 3) 能力切片:查询成本 O(能力数),与 Agent 总数无关
_planning._tcp IN SRV 10 10 8000 planner-agent.

一个必须注意的细节:compose 里每个容器的 aliases 必须与 zone 文件里的 SRV target 完全一致(本版统一为 <name>-agent 短名,且容器 DNS 指向 172.19.0.5),否则 SRV 解出的短名会被 Docker 内嵌 DNS 判成 NXDOMAIN。

2.4 Agent Card:能力索取的契约

每个 Agent 暴露 GET /.well-known/agent-card.json(schema_version: 2.0):

字段作用
name / service / version身份与版本,前端直接展示
url + transport调用入口(默认 POST /process,application/json)
capability_ids兜底路径下按 Card 过滤的判据
capabilities[]{id, description, version},与 CATALOG 对齐
inputs / outputsJSON Schema,声明必填 input 与 context.original/intent/upstream
health{endpoint:"/health", interval_s:10}
limits{concurrency, timeout_s, max_input_chars}
auth当前为 required:false(仅内网)

Card 是准静态数据:版本化发布、很少变。因此它适合被缓存,而不适合每请求现取 —— 这是 P3 的直接依据。

2.5 索引与缓存策略

参数默认值设计理由
CARD_TTL60s准静态契约的合理新鲜度
CARD_TTL_JITTER10s随机抖动打散过期时刻,避免整批同时失效造成尖峰
NEGATIVE_TTL8s失败负缓存要短:Agent 可能刚重启,太长会误判不可用
HEALTH_TTL10s/health 探测结果短缓存
CARD_TIMEOUT / HEALTH_TIMEOUT / SRV_TIMEOUT3s / 2s / 3s发现链路必须有界,不能拖住整个任务

single-flight:每次抓取前先抢 asyncio.Lock("card:<endpoint>"),抢到后再查一次缓存。并发 N 个节点同时需要同一个 Agent 的 Card 时,只有一个请求真正发出去。

两级回退:

  1. 先查 _<capability>._tcp.<domain>(source = role_srv)。
  2. 无记录时回落 _agent._tcp.<domain> 全量 SRV,再按每个 Card 的 capability_ids 过滤(source = generic_srv_filtered),结果按 CARD_TTL 缓存。

选择策略:先按 priority 取最优组,再在组内按 weight 加权随机 —— priority_then_weighted_random。健康探测失败的候选被剔出可用集;全部不健康时退回未验证列表(宁可用不确定的,也不返回空)。

2.6 DAG 计划与校验

规划器(一次模型调用)输出:

{
  "intent": "≤30字意图",
  "nodes": [{"id":"n1","capability":"research","depends_on":[],"parallel_group":"g1","purpose":"一句话目的"}],
  "final_node": "n4",
  "reasoning": "为什么这样分层"
}

normalize_plan() 做七道硬处理,不信任模型输出:

  1. 非 dict / 无 nodes → 直接降级为 fallback_plan(固定顺序执行,链路仍可跑)。
  2. 未知能力 → 丢弃并记 warnings。
  3. 重复能力 → 去重。
  4. implementation 语义误判 → 改判 writing(见 1.3)。
  5. 非法依赖(引用不存在 / 自引用)→ 剔除并记 warnings。
  6. 依赖环 → 只保留「依赖序号小于自身」的边,天然打断环。
  7. synthesis 强制为 final_node,依赖全集,且移到列表末尾;删除重复能力后再闭合一次依赖。

之后 levels() 用拓扑排序分层:depends_on 为空的进第 0 层,每层内可并发。带 guard < 20 兜底,剩余节点无法推进时一次性跑完,避免死循环。

节点上限 MAX_NODES = 6:超过后规划质量下降、延迟线性上升,且 context 预算不够分。

2.7 分层执行与故障隔离

层 0: [n1 research] [n2 planning]      ← asyncio.gather,同层并发
层 1: [n3 implementation] [n4 validation]
层 2: [n5 synthesis]                   ← 依赖全集,只跑一次

每个节点内部:

  1. bind_agents() 已提前绑定服务与 alternates;绑定为空 → 记 capability_unavailable 事件、节点标 unavailable,不静默跳过。
  2. build_context() 压缩上游输出 → 发 node_input 事件(含 Agent 实际收到的上下文文本与字数)。
  3. 按 [primary] + alternates 逐个 POST /process;每个失败都发 node_attempt_failed,换候选时发 node_retry。
  4. 全部失败 → 节点 failed + 记入 failures,链路继续(不会中断其它节点)。
  5. 成功 → 发 node_succeeded 与 node_output(全文),落库字数与 token 估算。

超时矩阵(全部可在 compose 调):

层级参数值
DNS 查询SRV_TIMEOUT3s
Card 抓取CARD_TIMEOUT3s
健康探测HEALTH_TIMEOUT2s
单节点NODE_TIMEOUT240s
单次模型调用LLM_TIMEOUT180s
整条 DAGRUN_TIMEOUT900s(实测 6 节点约 175s,留并发排队余量)

模型侧还有一层独立的故障隔离:provider 回退链 xmkai → deepseek(或反向)。xmkai 在 Cloudflare 后面且是单点,实测 2 个并发长请求就 524,因此按 provider 限流(xmkai 并发 1,deepseek 并发 4)。RETRYABLE = {408,425,429,500,502,503,504,520,522,523,524} 重试一次;finish_reason=length 时不把 reasoning_content 当答案,而是把 token 预算翻倍重试(上限 4096)。

2.8 Context 压缩

上游结果不能原样往下传,否则 token 随层数线性爆炸。策略:

2.9 可观测性

SQLite(WAL 模式,analyst 写、dashboard 读)三张表:

表内容
tracestrace_id、input、provider、status、started/ended/duration、error、summary、plan、stats
nodes每节点 capability、agent、depends_on、parallel_group、status、latency、output_chars、tokens_est、order_idx
eventsseq 单调递增、ts、type、node_id、payload(JSON)

WAL 允许一写多读,所以 dashboard 能在 analyst 写的同时轮询增量读。reap_stale(300) 在 dashboard 启动时把「无任何事件超过 300s 的 running」标记为 aborted —— 判活信号取最近事件时间而不是 started_at,避免误杀正在跑的长任务。


3. 请求流程

3.1 端到端时序

浏览器          dashboard          analyst           CoreDNS        Agent实例
  │                │                  │                 │              │
  │ 1 POST /api/start (text,provider)  │                 │              │
  ├───────────────►│                  │                 │              │
  │                │ 2 生成 trace_id,落一行 running     │              │
  │                │ 3 POST /process ─►│                 │              │
  │ 4 {trace_id} ◄─┤   (立即返回,任务在后台跑)         │              │
  │                │                  │                 │              │
  │ 5 GET /api/stream/{trace_id}       │                 │              │
  ├───────────────►│                  │                 │              │
  │                │ 6 轮询 SQLite 增量事件 → SSE 下发    │              │
  │◄═══════════════┤                  │                 │              │
  │                │                  │ 7 SRV _cap._tcp │              │
  │                │                  ├────────────────►│              │
  │                │                  │◄────────────────┤ 记录列表     │
  │                │                  │ 8 GET /.well-known/agent-card.json
  │                │                  ├───────────────────────────────►│
  │                │                  │ 9 GET /health                 │
  │                │                  ├───────────────────────────────►│
  │                │                  │ 10 模型调用 → DAG plan        │
  │                │                  │ 11 分层执行 POST /process     │
  │                │                  ├───────────────────────────────►│
  │                │                  │ 12 结果落库 + 事件 ──► SSE ──► 页面逐节点刷新

3.2 阶段一:发现(Discovery)

  1. INDEX.warm() 对所有 CATALOG 能力并发 candidates(cap)。
  2. 每个能力发一次 SRV _<cap>._tcp.agents.internal(CoreDNS file 插件 + cache 30)。
  3. 拿到 (target, port, priority, weight),拼成 endpoint = target:port。
  4. 并发抓该能力所有候选的 Card(带缓存与 single-flight),失败的写 8s 负缓存并进 failed[]。
  5. 并发探测 /health,status=="ok" 才算健康。
  6. 汇总为 discovery_view:每能力的 source / available / reason / candidates / failed / stats。

实测(7 个能力,全 role_srv 命中):149.04ms,srv_queries=56、card_fetches=48、card_cache_hits=57、card_failures=0、health_probes=80。

3.3 阶段二:规划(Planning)

  1. plan_prompt(CATALOG, available) 把能力表 + 索引里真实可用的能力喂给模型,要求只输出 JSON。
  2. 模型返回 → _extract_json() 容错解析(先找 json 围栏代码块,再取首个 { 到末个 },最后修补 ,}/,])。
  3. 解析失败或调用异常 → fallback_plan()(固定顺序 planning→research→security_review→implementation→synthesis)并标 degraded=True。
  4. normalize_plan() 七道校验(见 2.6)。
  5. 落库 save_plan(),发 plan_ready(含 levels 分层结果,前端据此立即可视化)。

3.4 阶段三:绑定(Binding)

对每个节点 INDEX.pick(capability):

前端收到 agents_bound 后把 Agent 名写进对应节点框,在节点开跑之前就能看到「谁将执行」。

3.5 阶段四:执行(Execution)

for layer in levels(plan):                      # 层间串行
    emit layer_started(layer, mode=parallel|serial)
    results = gather(run_node(n) for n in layer) # 同层并发,return_exceptions=True
    emit layer_finished(layer, {node: status})

run_node 的完整子流程:

  1. node_start 落库 → 发 node_started。
  2. 无绑定 → node_failed(stage=binding) → 返回 unavailable。
  3. build_context():取 depends_on 指向的上游输出 → compact_context() 压缩 → 发 context_compacted。
  4. 组装 payload {input, caller:"analyst", trace_id, context, provider, capability} → 发 node_input。
  5. 遍历 [primary] + alternates:POST http://<endpoint>/process(NODE_TIMEOUT);失败发 node_attempt_failed,换人发 node_retry。
  6. 全部失败 → node_failed + failures[],链路继续。
  7. 成功 → node_succeeded(耗时/字数/模型/尝试次数)+ node_output(全文)。

3.6 阶段五:汇总与交付物拼装

  1. 取 final_node(必然是 synthesis)的输出作为摘要。
  2. 确定性兜底:把所有 writing / implementation 节点的正文原样附加到摘要之后,并标注 <!-- 交付物正文 · <node> · <capability> -->。理由:synthesis 的 prompt 只能「尽量」引用正文,实测模型会只写摘要,导致用户拿不到方案内容。
  3. status 判定:final 失败且无任何成功 → failed;final 失败但有其它成功,或存在 failures → degraded;否则 ok。
  4. end_trace(status, duration, summary, stats) + trace_finished 事件。

3.7 前端实时流(SSE)

事件类型与页面反应:

事件页面效果
trace_started状态置 running
capability_index渲染「DNS SRV 查询结果」面板(查询名、记录、health、失败明细)
agent_cards渲染「Agent Card」面板(完整契约原文)
plan_ready按 levels 画出节点与分层,全部置 pending
agents_bound节点框写入 Agent 名
layer_started / layer_finished事件流 + 层推进
node_started节点变橙 + 呼吸点,边上流动性虚线
context_compacted事件流显示保留/原始字数与 token 估算
node_input「节点调用输入/输出」显示该 Agent 收到的上下文全文
node_attempt_failed / node_retry节点名切换为备用 endpoint,事件流留痕
node_output显示该 Agent 返回的全文
node_succeeded / node_failed节点变绿 / 变红,写入耗时
capability_unavailable节点变灰 + 原因
trace_finished状态、总耗时、统计卡、刷新 trace 列表

3.8 请求示例

启动一次任务:

curl -s -X POST http://192.168.1.30:18089/api/start \
  -H 'content-type: application/json' \
  -d '{"text":"设计一个家庭局域网安全智能体发现系统,分析风险、查找证据并给出实现和验收方案","provider":"deepseek"}'
# {"trace_id":"ed320e1a6e93","status":"started"}

看实时事件:

curl -N http://192.168.1.30:18089/api/stream/ed320e1a6e93

直接查发现结果(不经编排):

curl -s http://192.168.1.30:18089/api/discovery | head -c 800

4. 部署与接口

4.1 服务拓扑

服务容器地址说明
DNSagent-dns172.19.0.5:53CoreDNS 1.12.1,file 插件读 agents.db,cache 30
编排analyst-agent172.19.0.2:8000能力索引 + DAG 编排 + trace 写入
计划planner-agent172.19.0.6:8000planning
研究researcher-agent172.19.0.8:8000research
验证verifier-agent172.19.0.7:8000security_review、validation
实现coder-agent172.19.0.9:8000implementation
撰写writer-agent172.19.0.10:8000writing
汇总summarizer-agent172.19.0.3:8000synthesis(服务别名 synthesizer-agent)
控制台agent-dashboard0.0.0.0:18089 → 8080页面 + SSE + trace 管理

所有业务容器 dns: 172.19.0.5,共享卷 tracedata → /data/traces.db。

4.2 接口一览

方法路径用途
GET/控制台页面
GET/docs本文档页面
GET/healthdashboard 健康(含 analyst 地址、trace 数)
POST/api/start异步启动任务,立即返回 trace_id
GET/api/stream/{trace_id}SSE 实时事件流
POST/api/run同步运行(CLI/脚本用)
GET/api/discovery能力索引视图(代理 analyst /discovery)
GET/api/tracestrace 列表(limit/offset)
GET/api/traces/{id}trace 详情(含 nodes、events、plan)
DELETE/api/traces/{id}删除单条(网络设备拦 DELETE 时用 POST /api/traces/{id}/delete 兜底)
DELETE/api/traces?confirm=true清空全部(必须显式 confirm)

Agent 侧:

方法路径用途
GET/.well-known/agent-card.jsonAgent Card
GET/health存活探测
POST/process执行(input、context、capability、provider、trace_id)
GET/discovery仅 analyst:索引视图
GET/traces仅 analyst:trace 列表

4.3 部署

./deploy.sh   # rsync 到 192.168.1.30:/home/shj/agent-discovery-demo,构建并 up -d

rsync --exclude .env 是硬要求 —— 远程 .env 存 AGENT_LLM_API_KEY,不能删。模型走 192.168.1.50:8080 的 Hy2 代理(NO_PROXY 覆盖内网与 .internal)。


5. 实测数据(2026-10-23)

索引(GET /api/discovery):

指标值
域名agents.internal
全能力建索引耗时149.04 ms
srv_queries56
srv_role_queries56(角色型全部命中)
srv_fallbacks0(兜底路径未被触发)
card_fetches48
card_cache_hits57
card_failures0
health_probes80

7 个能力全部 available=true、source=role_srv,Card version=2.0.0:

能力绑定prioweighthealthy
planningplanner-agent:80001010✓
researchresearcher-agent:80001010✓
security_reviewverifier-agent:80001010✓
implementationcoder-agent:80001010✓
writingwriter-agent:80001010✓
validationverifier-agent:80001010✓
synthesissynthesizer-agent:80001010✓

近期 trace:

trace_id状态耗时输入
ed320e1a6e93ok53.2s设计一个家庭局域网安全智能体发现系统…
d59e02f9dfc9ok48.1s设计一个用户下线接口

6. 已知限制与后续方向

限制影响后续
agents.db 手工维护新增 Agent 需改 zone 文件并让 CoreDNS 重载接 CoreDNS 的 etcd 插件或加一个注册 API 自动写 zone
Card 无签名内网可信,跨域不可信加 Ed25519 签名 + 信任锚校验
auth.required=false只能内网部署加 mTLS 或 bearer token
xmkai 单点串行长任务并发能力受限多上游池化 + 按 provider 加权调度
Card 失败负缓存固定 8s重启窗口内可能短暂误判指数退避负缓存
无预算/成本计量只有 token 估算,无费用聚合在 nodes 表加 prompt_tokens/cost 并出日报

文档由 docs/agent-discovery.md 生成,页面路由 /docs 由 dashboard.py 提供,HTML 在 docs_page.py。