本文是灵犀 AI Agent 系列专栏的第三篇,将深入剖析灵犀最具创新性的功能——Project Nexus。这是一个完整的 Agent-to-Agent 通信网络:局域网 mDNS 自动发现、PSK 密钥建联、双向流式对话、人类监督、结果审批。从协议设计到前端渲染,完整展现一个 Agent 间通信系统的构建过程。
GitHub 地址:https://github.com/OdysseyFather/lingxi
一、为什么需要 Agent-to-Agent 对话?
在传统的 AI 应用中,每个用户和自己的 AI 对话,AI 之间互不相通。但在真实的工作场景中,很多任务需要多方协作:
- 产品经理的 AI 需要和开发团队的 AI 讨论技术方案
- 设计师的 AI 需要和前端的 AI 确认实现可行性
- 法务的 AI 需要和业务的 AI 审核合同条款
如果这些 AI 能够直接对话,省去人类的"传话"环节,效率将大幅提升。
灵犀的Project Nexus正是为此而生——让局域网内多台灵犀实例的 Agent 可以自动发现、建立信任、发起对话、实时流式交互,人类随时可以介入监督。
┌──────────────┐ ┌──────────────┐ │ 灵犀实例 A │ ← 双向流式对话 → │ 灵犀实例 B │ │ │ │ │ │ ┌──────────┐ │ mDNS 自动发现 │ ┌──────────┐ │ │ │ 代码审查员│ │ ◄──────────────────► │ │ 架构师 │ │ │ │ Agent │ │ PSK 密钥建联 │ │ Agent │ │ │ └──────────┘ │ │ └──────────┘ │ │ │ token 级流式 │ │ │ 👤 人类 A │ ◄──────────────────► │ 👤 人类 B │ │ (观察/介入)│ │ (观察/介入)│ └──────────────┘ └──────────────┘二、mDNS 自动发现:局域网的"雷达"
Project Nexus 的第一步是发现——同一局域网下的灵犀实例需要能互相感知到对方的存在。
2.1 mDNS 协议选型
灵犀选择了mDNS(Multicast DNS)作为发现协议,使用_lingxi._tcp服务类型。这个选择有几个关键优势:
- 零配置——不需要中心服务器,不需要手动输入 IP 地址
- 局域网限定——mDNS 报文不会穿越路由器,天然限制在本地网络
- 实时性好——灵犀每 10 秒扫描一次,新实例上线后几秒内即可被发现
// nexus/discovery.goconst(serviceType="_lingxi._tcp"// 服务类型scanInterval=10*time.Second// 扫描间隔peerTimeout=60*time.Second// 超时清理)2.2 服务注册
当灵犀启动时,如果用户开启了"对外可见",会通过 mDNS 注册自己的服务:
func(d*Discovery)startServer(settings*db.NexusSettings){info:=[]string{"id="+d.instanceID,// 唯一实例 ID"nick="+nickname,// 显示昵称}service,_:=mdns.NewMDNSService(d.instanceID,// 实例名serviceType,// _lingxi._tcp"",// 域名(默认 .local)"",// 主机名(自动)settings.ListenPort,// 端口nil,// IP(自动)info,// TXT 记录)d.server,_=mdns.NewServer(&mdns.Config{Zone:service})}2.3 服务扫描
扫描逻辑运行在独立的 goroutine 中,每 10 秒执行一次:
func(d*Discovery)scan(){entriesCh:=make(chan*mdns.ServiceEntry,16)gofunc(){forentry:=rangeentriesCh{// 解析 TXT 记录peerID:=""nickname:=""for_,field:=rangeentry.InfoFields{ifstrings.HasPrefix(field,"id="){peerID=field[3:]}ifstrings.HasPrefix(field,"nick="){nickname=field[5:]}}// 跳过自己ifpeerID==d.instanceID{continue}// 获取对方的公开 Agent 信息agentsJSON:=fetchRemoteAgents(host,entry.Port)// 更新到数据库db.UpsertNexusPeer(&db.NexusPeer{ID:peerID,Nickname:nickname,Host:host,Port:entry.Port,AgentsJSON:agentsJSON,})}}()// 发送 mDNS 查询params:=mdns.DefaultParams(serviceType)params.Timeout=3*time.Second mdns.Query(params)// 清理超时的 peerdb.CleanStalePeers(time.Now().Add(-peerTimeout))}发现对方后,灵犀还会调用对方的/api/nexus/info接口,获取对方公开的 Agent 列表。这样在发起对话时,用户可以看到对方有哪些 Agent 可以交谈。
三、建联机制:PSK 密钥验证
发现只是第一步。两个灵犀实例要对话,必须先建立信任关系——灵犀使用PSK(Pre-Shared Key)密钥验证机制。
3.1 建联流程
实例 A 实例 B │ │ │ 1. POST /api/nexus/connect-request │ │ {peer_id, nickname, shared_secret} │ │ ─────────────────────────────────────► │ │ │ │ 2. 用户 B 看到建联请求 │ │ 选择接受/拒绝 │ │ │ │ 3. POST /api/nexus/connect-respond │ │ {peer_id, accepted, shared_secret} │ │ ◄───────────────────────────────────── │ │ │ │ 4. 双方状态变为 connected │ │ 后续通信使用 X-Nexus-Token 验证 │ │ │3.2 Token 验证中间件
建联完成后,双方交换了共享密钥(shared_secret)。后续所有跨实例的 HTTP 请求,都需要在 Header 中携带X-Nexus-Token。
// handler/nexus.gofuncNexusTokenAuth()gin.HandlerFunc{returnfunc(c*gin.Context){path:=c.Request.URL.Path// 免验证路径:info、connect-request、connect-respondifstrings.HasSuffix(path,"/info")||strings.HasSuffix(path,"/connect-request")||strings.HasSuffix(path,"/connect-respond"){c.Next()return}token:=c.GetHeader("X-Nexus-Token")iftoken==""{c.AbortWithStatusJSON(401,gin.H{"error":"missing token"})return}// 验证 token 是否属于已建联的 contact// 兼容 pending/pending_incoming/connected 三种状态contact:=db.FindContactByToken(token)ifcontact==nil{c.AbortWithStatusJSON(403,gin.H{"error":"invalid token"})return}c.Set("nexus_contact",contact)c.Next()}}注意一个精巧的设计:/info、/connect-request、/connect-respond三个接口是免验证的。因为这三个接口的调用发生在建联之前——此时双方还没有交换密钥。
四、对话引擎:双向流式的实现
建联完成后,用户可以从实例 A 发起一场 Agent 对话。灵犀的对话引擎支持 5 种消息类型:
| 类型 | 说明 | 触发条件 |
|---|---|---|
message | 普通消息 | 默认类型 |
proposal | 提案 | Agent 提出需要确认的方案 |
decision | 决策 | Agent 做出明确的决定 |
handoff | 交接 | Agent 认为需要人类介入 |
close | 结束 | Agent 认为对话目标已达成 |
4.1 对话发起流程
用户 A 在 Nexus 页面选择对方的 Agent │ ▼ 填写对话配置:主题、目标、最大轮次、是否需要审批 │ ▼ POST /api/a2a-conversations(创建本地记录) │ ▼ POST 对方的 /api/nexus/conversation/request │ ▼ 对方用户看到邀请通知,选择己方 Agent,接受/拒绝 │ ▼ 接受 POST 回 /api/nexus/conversation/accept │ ▼ RunConversation 引擎启动!4.2 RunConversation:引擎核心
RunConversation是对话引擎的核心函数。它的工作流程如下:
funcRunConversation(convIDint64,sessionIDint64,...){// 1. 加锁——同一对话不能并行执行mu:=getConvMutex(convID)mu.Lock()defermu.Unlock()// 2. 准备暂停通道——人类可以随时暂停pauseCh:=make(chanstruct{},1)pausedConvs.Store(convID,pauseCh)deferpausedConvs.Delete(convID)// 3. 构建流式转发器——将本地 Agent 的输出转发给对方forwarder:=buildStreamForwarder(conv)// 4. 构建首条消息(含对话主题、目标、安全约束)firstMessage:=buildA2AFirstMessage(conv,agent)// 5. 调用流式 Runner——获取 Agent 回复reply,err:=streamRunner(sessionID,firstMessage,conv.LocalAgentID,forwarder)// 6. 解析回复类型(message/proposal/decision/handoff/close)msgType,content,structured:=parseAgentReply(reply)// 7. 发送到远端sendToRemote(remoteURL,convID,"remote",agentName,msgType,content,structured,token)}4.3 双向流式转发
这是 Project Nexus 最有技术含量的部分。每当本地 Agent 产生一个 token(文本或思考过程),灵犀不仅通过 WebSocket 推送给本地前端,还要实时转发给对方。
// 构建流式转发器funcbuildStreamForwarder(conv*db.A2AConversation)StreamForwarder{contact,_:=db.GetNexusContactByPeerID(conv.RemotePeerID)remoteURL:=fmt.Sprintf("http://%s/api/nexus/conversation/stream-token",net.JoinHostPort(contact.Host,fmt.Sprintf("%d",contact.Port)))token:=contact.SharedSecretreturnfunc(event,datastring){payload:=map[string]interface{}{"conversation_id":convID,"event":event,// "text" 或 "thinking""data":data,// token 内容}// 异步发送,不阻塞主流程gohttpPost(remoteURL,payload,token)}}对方实例收到stream-token后,通过 WebSocket 广播a2a_remote_stream事件,前端接收后更新a2aRemoteLiveBlocks状态——实现了对方 Agent 输出的实时渲染。
本地 Agent 产生 token "你好" │ ├──► 本地 WebSocket → 前端渲染(己方 Agent 输出) │ └──► HTTP POST → 对方 /api/nexus/conversation/stream-token │ └──► 对方 WebSocket → 对方前端渲染(对方视角下的远端 Agent 输出)4.4 持久会话
A2A 对话不是一次性的。灵犀为每个对话创建了独立的 session,关联到sessions表:
a2a_conversations 表 ├── local_session_id ──► sessions 表的 id ├── topic ──► 对话主题 ├── goal ──► 对话目标 ├── current_round ──► 当前轮次 └── max_rounds ──► 最大轮次这意味着 Agent 在对话中的上下文是跨轮次保持的。它记得之前说过什么,可以在后续轮次中引用之前的讨论内容。
五、前端渲染:统一的 Bubble 体验
A2A 对话的前端渲染与主聊天使用相同的消息渲染组件(BlocksRenderer),这意味着 Agent 间对话完整支持:
- Markdown 渲染
- 代码块语法高亮
- 思考过程折叠块
- 工具调用状态块
5.1 视觉区分
己方和对方的 Agent 通过颜色区分:
- 主题色= 己方 Agent(与应用主题一致)
- 紫色= 对方 Agent
每条消息气泡都有 Agent 名称标签和颜色头像,确保用户一眼就能分清消息来源。
5.2 Zustand 状态设计
A2A 对话在 Zustand Store 中有独立的状态切片,与主聊天完全隔离:
// A2A 对话状态(独立于主聊天)activeA2ASessionId:null,activeA2AConvId:null,a2aLiveBlocks:[],// 己方 Agent 流式输出a2aIsStreaming:false,a2aRemoteLiveBlocks:[],// 对方 Agent 流式输出a2aRemoteIsStreaming:false,a2aMessages:[],// 历史消息WebSocket 事件路由逻辑会根据sessionId判断:如果匹配activeA2ASessionId,事件会路由到 A2A 状态切片;如果匹配activeSessionId,事件会路由到主聊天状态切片。
// WS 事件路由handleWSEvent:(msg)=>{const{event,sessionId}=msg;// 远端 Agent 流式 token 转发(广播事件,无 sessionId)if(event==='a2a_remote_stream'){// → 更新 a2aRemoteLiveBlocksreturn;}// A2A 会话的流式事件if(sessionId===state.activeA2ASessionId){// → 更新 a2aLiveBlocksreturn;}// 主聊天的流式事件if(sessionId===state.activeSessionId){// → 更新 liveBlocks}}六、人类监督:暂停、接管、终止
AI 间的自动对话虽然高效,但必须保留人类的控制权。灵犀实现了完整的人类监督机制。
6.1 暂停
用户随时可以暂停对话。暂停信号通过 Go channel 传递:
funcPauseConversation(convIDint64){ifch,ok:=pausedConvs.Load(convID);ok{select{casech.(chanstruct{})<-struct{}{}:default:}}}在对话循环中,每轮开始前都会检查暂停信号。
6.2 接管
用户可以"接管"对话——此时 Agent 停止自动回复,人类手动输入消息。对话从 Agent 自动模式切换为人类手动模式。
6.3 终止
用户可以随时终止对话。终止信号会同时发送给对方,确保双方都停止。
6.4 Handoff
Agent 自身也可以请求人类介入。当 Agent 判断某个决策超出自己的能力范围时,会发送[HANDOFF]标记,灵犀会自动暂停对话并弹出桌面通知。
ifmsgType=="handoff"{db.UpdateA2AConversationStatus(convID,"paused")broadcast("a2a_handoff",...)broadcast("desktop_notify",`{ "title": "Agent 请求人类介入", "body": "..." }`)}七、对话摘要与审批
当对话结束(Agent 发送[CLOSE]标记)时,灵犀会自动生成对话摘要。
7.1 摘要生成
funcgenerateSummary(convIDint64)string{messages,_:=db.ListA2AMessages(convID)conv,_:=db.GetA2AConversation(convID)varsb strings.Builder sb.WriteString(fmt.Sprintf("对话主题:%s\n",conv.Topic))sb.WriteString(fmt.Sprintf("对话目标:%s\n",conv.Goal))sb.WriteString(fmt.Sprintf("总轮次:%d\n\n",conv.CurrentRound))// 提取关键决策for_,m:=rangemessages{ifm.MsgType=="decision"||m.MsgType=="close"{sb.WriteString(fmt.Sprintf("【%s】%s\n",m.MsgType,m.Content))}}returnsb.String()}7.2 审批流程
如果对话创建时设置了"需要审批",对话完成后不会立即标记为完成,而是进入pending_approval状态。用户需要审阅 Agent 的决策,确认或修改后才能最终确认。
Agent 对话完成 │ ▼ 需要审批?─── No ──► 直接标记为 completed │ │ Yes ▼ 状态变为 pending_approval │ ▼ 用户审阅摘要和决策 │ ├── 批准 → approved └── 驳回 → rejected(可修改后重新提交)八、安全设计:Agent 对外设置
每个 Agent 参与 Nexus 通信时,有独立的安全配置:
| 配置项 | 说明 |
|---|---|
| 公开开关 | 是否在 mDNS 中广播此 Agent |
| 能力标签 | 标记 Agent 的专长(“代码审查”、"产品设计"等) |
| 授权级别 | 控制 Agent 能做什么(只读/建议/执行) |
| 禁止透露 | 安全约束——指定 Agent 绝对不能透露的信息 |
// 构建 A2A 首条消息时注入安全约束funcbuildA2AFirstMessage(conv*db.A2AConversation,agent*db.Agent)string{nexusConfig,_:=db.GetAgentNexusConfig(conv.LocalAgentID)forbidden:=""ifnexusConfig!=nil&&nexusConfig.ForbiddenInfo!=""{forbidden=fmt.Sprintf("\n【安全约束】绝对不可透露以下信息:%s\n",nexusConfig.ForbiddenInfo)}// ...}这意味着即使 Agent 在与外部 Agent 对话时,也会遵守预设的安全边界——不会泄露敏感业务信息。
九、统一 Nexus 仪表盘
灵犀将发现、联系人、对话三个功能整合到了统一的 Nexus 仪表盘中:
┌─────────────────────────────────────────────────┐ │ Nexus 仪表盘 │ ├───────────┬──────────────┬──────────────────────┤ │ 附近实例 │ 已建联联系人 │ 进行中/已完成的对话 │ │ │ │ │ │ 🟢 Mac A │ 📍 张三的灵犀 │ 🔵 技术方案讨论 │ │ 🟢 Mac B │ 📍 李四的灵犀 │ 🟡 合同条款审核 │ │ │ │ ✅ 需求分析 │ │ [建联] │ [发起对话] │ [查看详情] │ └───────────┴──────────────┴──────────────────────┘十、技术挑战与解决方案
10.1 并发安全
同一个对话不能被多个 goroutine 同时处理。灵犀为每个对话维护一个独立的 mutex:
varconvMutexes sync.Map// convID -> *sync.MutexfuncgetConvMutex(convIDint64)*sync.Mutex{v,_:=convMutexes.LoadOrStore(convID,&sync.Mutex{})returnv.(*sync.Mutex)}10.2 消息重试
跨实例的 HTTP 通信可能因网络波动失败。灵犀实现了自动重试机制:
funcsendToRemote(urlstring,...)error{forattempt:=0;attempt<3;attempt++{_,err:=httpPost(url,payload,token)iferr==nil{returnnil}// 指数退避time.Sleep(time.Duration(attempt+1)*2*time.Second)}returnlastErr}10.3 WS 事件路由
当同时存在主聊天和 A2A 对话时,WebSocket 事件需要正确路由。灵犀通过sessionId字段区分事件归属,确保不同对话的流式数据不会串台。
十一、总结
Project Nexus 是灵犀最具创新性和技术挑战的功能模块。它实现了一个完整的 Agent-to-Agent 通信网络:
- mDNS 自动发现——零配置,局域网内灵犀实例自动可见
- PSK 建联——共享密钥验证,建立互信关系
- 双向流式对话——token 级实时转发,双方同时观察 Agent 输出
- 5 种消息类型——message/proposal/decision/handoff/close,覆盖协作场景
- 人类监督——暂停/接管/终止,人类始终拥有最终控制权
- 安全约束——Agent 对外设置,防止敏感信息泄露
- 审批流程——对话结果需要人类确认后才能生效
在下一篇文章中,我们将回到日常使用场景,深入探讨灵犀的对话体验设计——多模态输入、RAG 引用可视化、规划模式、流式思考链展示等功能的实现细节。
灵犀—— 让 AI 成为你的工作伙伴,而不只是聊天对象。
GitHub:https://github.com/OdysseyFather/lingxi
如果觉得项目有价值,欢迎 Star 支持!