Agent 发现与 DAG 编排系统 · 设计文档
⚠ 架构已迁移(2026-10-29):本文描述的是 v2.0 的 DNS SRV + 静态dns/agents.db方案, 已被 SVCB + CoreDNS 动态注册 取代。现网实现见docs/svcb-registration.md,本文仅作历史设计留档, 其中的agents.db、_tcpSRV 查询名、固定 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 对应解法
| 问题 | 解法 | 代码位置 |
|---|---|---|
| P1 | DNS 即注册中心。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 |
| P3 | TTL 缓存 + 随机抖动 + single-flight:60s ± 10s 抖动,并发同 key 只放一个请求出去 | index.py: card() |
| P4 | 候选链回退:priority/weight 排序取候选,alternates 逐个重试;Card 抓不到写 8s 负缓存;整条链路降级为 degraded 而非 failed | index.py: candidates/pick、agent.py: run_node |
| P5 | DAG + 拓扑分层:模型产出 depends_on,同层并发、层间串行 | plan.py: levels()、agent.py: run_pipeline |
| P6 | trace 落库 + SSE 直播:SQLite 三张表记录节点/事件/计划,浏览器边跑边看 | trace_store.py、dashboard.py、web.py |
1.3 附带解决的语义歧义(V6)
「写一份派对策划」和「实现一个下线接口」都含「方案/步骤」这类词,只靠关键词会把它们路由到同一个 Agent。系统做了两道判定:
- 规划器 prompt 明确写死:
implementation只用于交付可运行代码/配置/接口改动;写方案、策划、文案、报告一律writing。 - 归一化阶段硬校验:若节点能力是
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 而不是手写注册表
- SRV 自带
priority+weight+port:负载均衡策略不用自己发明,priority做粗筛、weight做加权随机,语义是现成的。 - 零 SDK:任何语言、任何进程,只要会发 DNS 查询就能发现服务,不需要引一个注册中心客户端。
- 天然分层 key:
_<capability>._tcp.<domain>让「按能力查」变成一次独立的 DNS 查询,而不是「先拿全量再过滤」。 - 运维面小:CoreDNS 一个容器 + 一个 zone 文件,没有额外数据库要备份。
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 / outputs | JSON 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_TTL | 60s | 准静态契约的合理新鲜度 |
CARD_TTL_JITTER | 10s | 随机抖动打散过期时刻,避免整批同时失效造成尖峰 |
NEGATIVE_TTL | 8s | 失败负缓存要短:Agent 可能刚重启,太长会误判不可用 |
HEALTH_TTL | 10s | /health 探测结果短缓存 |
CARD_TIMEOUT / HEALTH_TIMEOUT / SRV_TIMEOUT | 3s / 2s / 3s | 发现链路必须有界,不能拖住整个任务 |
single-flight:每次抓取前先抢 asyncio.Lock("card:<endpoint>"),抢到后再查一次缓存。并发 N 个节点同时需要同一个 Agent 的 Card 时,只有一个请求真正发出去。
两级回退:
- 先查
_<capability>._tcp.<domain>(source = role_srv)。 - 无记录时回落
_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() 做七道硬处理,不信任模型输出:
- 非 dict / 无 nodes → 直接降级为
fallback_plan(固定顺序执行,链路仍可跑)。 - 未知能力 → 丢弃并记
warnings。 - 重复能力 → 去重。
implementation语义误判 → 改判writing(见 1.3)。- 非法依赖(引用不存在 / 自引用)→ 剔除并记
warnings。 - 依赖环 → 只保留「依赖序号小于自身」的边,天然打断环。
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] ← 依赖全集,只跑一次
每个节点内部:
bind_agents()已提前绑定服务与alternates;绑定为空 → 记capability_unavailable事件、节点标unavailable,不静默跳过。build_context()压缩上游输出 → 发node_input事件(含 Agent 实际收到的上下文文本与字数)。- 按
[primary] + alternates逐个POST /process;每个失败都发node_attempt_failed,换候选时发node_retry。 - 全部失败 → 节点
failed+ 记入failures,链路继续(不会中断其它节点)。 - 成功 → 发
node_succeeded与node_output(全文),落库字数与 token 估算。
超时矩阵(全部可在 compose 调):
| 层级 | 参数 | 值 |
|---|---|---|
| DNS 查询 | SRV_TIMEOUT | 3s |
| Card 抓取 | CARD_TIMEOUT | 3s |
| 健康探测 | HEALTH_TIMEOUT | 2s |
| 单节点 | NODE_TIMEOUT | 240s |
| 单次模型调用 | LLM_TIMEOUT | 180s |
| 整条 DAG | RUN_TIMEOUT | 900s(实测 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 随层数线性爆炸。策略:
- 预算:
CONTEXT_BUDGET_TOKENS=1800,单节点CONTEXT_PER_NODE_TOKENS=320(约 1 token ≈ 1.8 中文字符)。 - 单节点先裁剪(头 65% + 尾 + 截断标记),再按总量裁剪:每轮挑当前最长的非交付物节点压到 60%,压到 120 字仍超标则丢弃最早的普通中间节点。
PRESERVE_CAPS = {writing, implementation}的输出是交付物正文,上限放到整个上下文预算,且最后才压、绝不丢弃。理由:实测宽松策略会把 2400 字的方案正文截断,最终答案变成半截方案。- 记录
compaction明细(原始字数、保留字数、丢弃节点、估算 token、是否因正文超预算),随context_compacted事件进 trace。
2.9 可观测性
SQLite(WAL 模式,analyst 写、dashboard 读)三张表:
| 表 | 内容 |
|---|---|
traces | trace_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 |
events | seq 单调递增、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)
INDEX.warm()对所有CATALOG能力并发candidates(cap)。- 每个能力发一次
SRV _<cap>._tcp.agents.internal(CoreDNSfile插件 +cache 30)。 - 拿到
(target, port, priority, weight),拼成endpoint = target:port。 - 并发抓该能力所有候选的 Card(带缓存与 single-flight),失败的写 8s 负缓存并进
failed[]。 - 并发探测
/health,status=="ok"才算健康。 - 汇总为
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)
plan_prompt(CATALOG, available)把能力表 + 索引里真实可用的能力喂给模型,要求只输出 JSON。- 模型返回 →
_extract_json()容错解析(先找 json 围栏代码块,再取首个{到末个},最后修补,}/,])。 - 解析失败或调用异常 →
fallback_plan()(固定顺序 planning→research→security_review→implementation→synthesis)并标degraded=True。 normalize_plan()七道校验(见 2.6)。- 落库
save_plan(),发plan_ready(含levels分层结果,前端据此立即可视化)。
3.4 阶段三:绑定(Binding)
对每个节点 INDEX.pick(capability):
- 有候选 → 记录
service、完整card、selection(priority/weight/pool/strategy)、最多 3 个alternates。 - 无候选 → 记录
reason(no_srv_record/all_cards_unreachable/capability_unavailable)与failed[],发capability_unavailable事件。
前端收到 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 的完整子流程:
node_start落库 → 发node_started。- 无绑定 →
node_failed(stage=binding)→ 返回unavailable。 build_context():取depends_on指向的上游输出 →compact_context()压缩 → 发context_compacted。- 组装 payload
{input, caller:"analyst", trace_id, context, provider, capability}→ 发node_input。 - 遍历
[primary] + alternates:POST http://<endpoint>/process(NODE_TIMEOUT);失败发node_attempt_failed,换人发node_retry。 - 全部失败 →
node_failed+failures[],链路继续。 - 成功 →
node_succeeded(耗时/字数/模型/尝试次数)+node_output(全文)。
3.6 阶段五:汇总与交付物拼装
- 取
final_node(必然是synthesis)的输出作为摘要。 - 确定性兜底:把所有
writing/implementation节点的正文原样附加到摘要之后,并标注<!-- 交付物正文 · <node> · <capability> -->。理由:synthesis 的 prompt 只能「尽量」引用正文,实测模型会只写摘要,导致用户拿不到方案内容。 status判定:final 失败且无任何成功 →failed;final 失败但有其它成功,或存在failures→degraded;否则ok。end_trace(status, duration, summary, stats)+trace_finished事件。
3.7 前端实时流(SSE)
POST /api/start先写一行running的 trace,立刻返回trace_id。这样 SSE 首帧就能查到 trace,不会 404。- 前端用
EventSource('/api/stream/{trace_id}'),服务端每 250ms 轮询 SQLite,按seq > last增量下发;trace 结束发__done__;空闲超过 420s 发stream_timeout。 - 保留
POST /api/run同步接口给 CLI/脚本,但前端不用它 —— 否则只能看到终态,看不到动画。
事件类型与页面反应:
| 事件 | 页面效果 |
|---|---|
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 服务拓扑
| 服务 | 容器 | 地址 | 说明 |
|---|---|---|---|
| DNS | agent-dns | 172.19.0.5:53 | CoreDNS 1.12.1,file 插件读 agents.db,cache 30 |
| 编排 | analyst-agent | 172.19.0.2:8000 | 能力索引 + DAG 编排 + trace 写入 |
| 计划 | planner-agent | 172.19.0.6:8000 | planning |
| 研究 | researcher-agent | 172.19.0.8:8000 | research |
| 验证 | verifier-agent | 172.19.0.7:8000 | security_review、validation |
| 实现 | coder-agent | 172.19.0.9:8000 | implementation |
| 撰写 | writer-agent | 172.19.0.10:8000 | writing |
| 汇总 | summarizer-agent | 172.19.0.3:8000 | synthesis(服务别名 synthesizer-agent) |
| 控制台 | agent-dashboard | 0.0.0.0:18089 → 8080 | 页面 + SSE + trace 管理 |
所有业务容器 dns: 172.19.0.5,共享卷 tracedata → /data/traces.db。
4.2 接口一览
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | / | 控制台页面 |
| GET | /docs | 本文档页面 |
| GET | /health | dashboard 健康(含 analyst 地址、trace 数) |
| POST | /api/start | 异步启动任务,立即返回 trace_id |
| GET | /api/stream/{trace_id} | SSE 实时事件流 |
| POST | /api/run | 同步运行(CLI/脚本用) |
| GET | /api/discovery | 能力索引视图(代理 analyst /discovery) |
| GET | /api/traces | trace 列表(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.json | Agent 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_queries | 56 |
srv_role_queries | 56(角色型全部命中) |
srv_fallbacks | 0(兜底路径未被触发) |
card_fetches | 48 |
card_cache_hits | 57 |
card_failures | 0 |
health_probes | 80 |
7 个能力全部 available=true、source=role_srv,Card version=2.0.0:
| 能力 | 绑定 | prio | weight | healthy |
|---|---|---|---|---|
| planning | planner-agent:8000 | 10 | 10 | ✓ |
| research | researcher-agent:8000 | 10 | 10 | ✓ |
| security_review | verifier-agent:8000 | 10 | 10 | ✓ |
| implementation | coder-agent:8000 | 10 | 10 | ✓ |
| writing | writer-agent:8000 | 10 | 10 | ✓ |
| validation | verifier-agent:8000 | 10 | 10 | ✓ |
| synthesis | synthesizer-agent:8000 | 10 | 10 | ✓ |
近期 trace:
| trace_id | 状态 | 耗时 | 输入 |
|---|---|---|---|
| ed320e1a6e93 | ok | 53.2s | 设计一个家庭局域网安全智能体发现系统… |
| d59e02f9dfc9 | ok | 48.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。