尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

围棋小程序多Agent架构实战:七个Agent的职责拆分与提示词设计

围棋小程序多Agent架构实战:七个Agent的职责拆分与提示词设计 1. 为什么一个围棋小程序要拆出七个 Agent先说结论把七个 Agent 塞进一个围棋小程序不是为了炫技而是被逼出来的。围棋这个场景有个很讨厌的特点——它同时要求规则绝对严谨、表达足够自然、交互还得跟得上手。你如果只用一个通用大模型硬扛会出现三种典型翻车该算清死活的地方它给你写散文该说人话的地方它给你背棋谱该秒回的地方它卡三秒。我最早做这个项目的时候走的就是单 Agent 一把梭的路线。用户落子我把棋盘状态、历史走法、当前问题拼成一大段 prompt 丢给 LLM让它既判断形势、又给建议、还负责聊天。跑了两天我就放弃了原因很具体职责混在一起提示词没法收敛。你既要它输出结构化的形势判断又要它输出口语化的安慰模型会在两种语气之间反复横跳输出格式极不稳定。token 消耗失控。每次对话都要把完整棋谱塞进去一盘棋下到中盘单次请求轻松破万 token成本和延迟都顶不住。错误无法定位。用户说你这步建议是错的我根本不知道是形势判断错了、候选点生成错了还是措辞让人误解了。后来我把它拆成了七个 Agent每个 Agent 只干一件事输入输出都用结构化数据约束死。拆完之后单次请求的 token 降了大概六成输出格式的解析成功率从七成出头提到了接近百分之百最重要的是——出问题的时候我知道该去改哪个 Agent 的提示词。这七个 Agent 大致是这样分工的Agent 名称核心职责输入输出类型规则校验 Agent判断落子是否合法、是否提子、是否终局棋盘矩阵 落子坐标布尔值 事件列表形势判断 Agent评估当前局面优劣棋盘矩阵 贴目规则数值评分 文字说明候选点生成 Agent给出若干推荐落点棋盘矩阵 形势评分坐标数组 理由死活分析 Agent局部死活与对杀判断局部棋盘切片死活结论 变化图复盘讲解 Agent把关键手翻译成人话棋谱 关键节点分段讲解文本对话陪聊 Agent承接用户的闲聊与提问用户消息 上下文自然语言回复调度路由 Agent决定当前请求交给谁用户意图 会话状态路由指令这张表是整个项目的骨架。下面我逐个拆开讲重点讲提示词怎么写、结构化输出怎么约束、以及踩过的坑。2. 七个 Agent 的职责边界与提示词设计2.1 规则校验 Agent能不用 LLM 就别用 LLM这是我最想强调的一点。规则校验这种有确定答案的事情根本不该交给大模型。围棋的落子合法性、提子、打劫、终局判定全都是可以用代码精确算出来的。我一开始图省事让 LLM 判断这步棋合不合法结果它偶尔会把打劫的禁着点判成合法用户直接炸了。正确做法是规则校验用纯代码实现Agent 只负责把代码的判定结果翻译成结构化事件。比如用户落子后代码算出提掉了对方两颗子Agent 的输出就是{ legal: true, captured: [{x: 3, y: 4}, {x: 3, y: 5}], is_ko: false, game_over: false, next_player: white }提示凡是能用确定性算法算出来的东西都不要塞给 LLM。LLM 的价值在于处理模糊、开放、需要语言理解的部分规则判定属于它的短板区。这个 Agent 的提示词其实极短因为它几乎不需要思考只需要做格式转换。我给它定的系统提示词大意是你是一个围棋规则事件的格式化器输入是引擎给出的原始判定输出是固定 schema 的 JSON不要添加任何额外解释。关键词就三个固定 schema、不加解释、原样转换。2.2 形势判断 Agent数值和文字必须分开输出形势判断是围棋里最玄学的部分。职业棋手都经常判断失误指望 LLM 给出精确到半目的形势判断是不现实的。所以我给这个 Agent 的定位是给出一个粗略的倾向性评分外加一段人话解释而不是假装精确。它的输出 schema 我设计成这样{ score: -3.5, advantage: white, confidence: 0.6, reason: 黑棋右下角实地较多但白棋外势厚实中腹潜力大, key_areas: [[10, 10], [12, 8]] }这里有个关键设计score 和 reason 分开。score 是给程序用的用来驱动候选点生成reason 是给用户看的用来解释。如果混在一起输出模型很容易在数字里夹带文字解析就崩了。提示词上我用了角色 约束 示例三段式。角色是你是一位业余5段的围棋形势判断助手约束是只输出 JSONscore 范围 -30 到 30正数表示黑优示例给一组完整的输入输出。实测下来给一个完整示例比给十条文字规则都管用。2.3 候选点生成 Agent让 LLM 做筛选而不是穷举围棋棋盘 19x19理论落点三百多个。你不可能让 LLM 去穷举也没必要。我的做法是先用传统方法比如基于形势评分的热力图筛出 10 到 15 个候选点再让 LLM 从中挑 3 个并说明理由。这个 Agent 的提示词核心是从给定候选中选择不要发明新坐标。我踩过的坑是如果不明确禁止模型会自己编一个不在候选列表里的坐标然后煞有介事地解释。后来我在提示词里加了一句硬约束你只能从 candidates 数组中选择输出的坐标必须能在 candidates 中找到否则视为错误。加上这句之后越界的情况基本消失了。输出长这样{ recommendations: [ {x: 15, y: 3, reason: 守住右下角实地同时威胁对方薄味, priority: 1}, {x: 10, y: 10, reason: 扩张中腹呼应外势, priority: 2} ] }2.4 死活分析 Agent局部切片是省 token 的关键死活题是围棋里最吃算力的部分。整盘棋丢给 LLM 去算死活既慢又不准。我的做法是先做局部切片把涉及死活的区域比如一个 7x7 的小块单独抠出来只把这个切片喂给死活分析 Agent。这个 Agent 的提示词里我会明确告诉它这是一个局部死活问题边界外的棋子视为已定型。输出包括死活结论和一条主要变化{ status: alive, best_line: [[3, 3], [4, 3], [3, 4]], explanation: 白棋做眼空间充足黑棋无法净杀 }注意死活分析对模型的推理能力要求很高建议用推理能力强的模型并且把 temperature 调到很低我一般用 0.1 到 0.2否则同一个局面两次问会给你两个答案。2.5 复盘讲解 Agent把棋谱翻译成人话复盘讲解是用户感知最强的部分。用户下完一盘棋最想知道的是我哪步下臭了。这个 Agent 的输入是棋谱加上前面几个 Agent 算出来的关键节点输出是分段讲解。我给它设计的输出是数组每段对应一个关键节点{ segments: [ {move_number: 23, comment: 这步棋过于保守此时应该抢占左边大场, severity: medium}, {move_number: 45, comment: 好棋一举确立优势, severity: good} ] }提示词上我要求它用业余棋友能听懂的话避免专业术语堆砌每段不超过 50 字。这条不超过 50 字的约束非常重要不加的话模型会写小作文用户根本没耐心看。2.6 对话陪聊 Agent唯一允许自由发挥的 Agent前面六个 Agent 都在做结构化输出只有这个陪聊 Agent 是自由文本。用户问今天天气怎么样或者你觉得我这盘下得如何都归它管。但即便是它我也做了约束不允许它给出具体的落子建议因为那属于候选点生成 Agent 的职责越界就会导致前后矛盾。它的提示词里有一句如果用户询问具体落子建议请引导用户使用分析功能不要自行给出坐标。这样就把职责边界守住了。2.7 调度路由 Agent整个系统的交通警察七个 Agent 谁来调用靠调度路由 Agent。它接收用户消息判断意图然后决定路由。输出是一个简单的指令{ intent: ask_suggestion, target_agent: candidate_generator, need_context: [board_state, score] }这个 Agent 的提示词核心是分类我给了它六种意图类别和对应的目标 Agent 映射表。实测下来意图分类这种任务用小模型就够了没必要上大模型能省不少成本。3. 结构化输出让 LLM 说程序能听懂的话3.1 为什么必须做结构化输出前面反复提到 schema这里集中讲透。LLM 的输出本质是文本而程序需要的是数据。如果你让 LLM 自由输出然后自己写正则去解析那你会陷入无尽的维护地狱——模型今天说建议下在 D4明天说推荐坐标 (3,4)后天说我认为 D4 是个好点你的正则永远追不上。结构化输出解决的就是这个问题用 schema 把输出格式钉死让解析变成确定性的。我用的方案是 JSON Schema 约束配合模型本身支持的 JSON 模式。如果模型不支持原生 JSON 模式就在提示词里强制要求只输出 JSON不要有任何 markdown 代码块标记不要有任何前后缀文字。3.2 三个让 schema 稳定的技巧技巧一字段名用英文值可以用中文。字段名是给程序读的必须稳定值是给用户看的可以灵活。我见过有人用中文字段名结果模型偶尔给你加个空格或者换个同义词解析直接挂。技巧二枚举值一定要穷举。比如 advantage 字段我只允许 black、white、even 三个值在提示词里明确列出。不穷举的话模型会给你输出黑棋略优这种没法程序判断的字符串。技巧三给一个完整的输入输出示例。这是最有效的一招。与其写十条规则不如给一组输入是这样输出是这样的完整示例。模型模仿示例的能力远强于理解抽象规则的能力。3.3 解析失败时的兜底策略再稳的 schema 也有翻车的时候。我的兜底策略分三层第一层重试。解析失败时把错误信息拼回提示词让模型重新输出一次。实测重试一次能救回大部分失败。第二层降级。如果重试还失败就退回到一个默认值比如形势判断失败就返回 score 为 0至少保证程序不崩。第三层记录。所有解析失败的原始输出都落库定期分析是哪些字段容易出问题反过来优化提示词。提示不要指望一次就把 schema 设计完美。我的经验是上线后前两周每天看解析失败日志根据失败案例迭代提示词两周之后失败率能降到千分之一以下。4. 用 trpc-agent-go 和 Golang 把 Agent 编排起来4.1 为什么后端选 Golang这个项目后端我用的是 Golang框架是 trpc-agent-go。选它的理由很实在并发处理能力强、部署简单、和前端通信的协议清晰。围棋小程序会有多个用户同时在线每个用户的每一步棋都可能触发多个 Agent 调用这种场景下 Golang 的 goroutine 模型非常合适。trpc-agent-go 这个框架的好处是它把 Agent 的注册、调用、上下文传递都封装好了。我只需要定义好每个 Agent 的输入输出结构体框架负责把它们串起来。比如形势判断 Agent 的定义大概是这样type PositionEvalInput struct { Board [][]int json:board Komi float64 json:komi NextPlayer string json:next_player } type PositionEvalOutput struct { Score float64 json:score Advantage string json:advantage Confidence float64 json:confidence Reason string json:reason KeyAreas [][]int json:key_areas }结构体定义好之后框架会自动做 JSON 序列化和反序列化我不用手写解析逻辑。这一点比裸调 API 舒服太多。4.2 Agent 之间的上下文怎么传七个 Agent 不是孤立的它们需要共享上下文。比如候选点生成 Agent 需要形势判断 Agent 的输出复盘讲解 Agent 需要前面所有 Agent 的结论。我的做法是定义一个统一的会话上下文结构type GameContext struct { BoardState [][]int MoveHistory []Move EvalResult *PositionEvalOutput Candidates []Candidate UserIntent string }每次调用 Agent 时把需要的部分传进去Agent 只读它关心的字段。这样既避免了重复计算又保证了数据一致性。注意上下文不要无脑全传。我一开始把整个 GameContext 传给每个 Agent结果 token 浪费严重。后来改成按需传递只传当前 Agent 真正需要的字段token 消耗又降了一截。4.3 并发调用与超时控制有些 Agent 之间没有依赖关系可以并发调用。比如形势判断和死活分析可以同时跑最后再汇总。Golang 里用 errgroup 就能优雅地处理g, ctx : errgroup.WithContext(context.Background()) var evalResult *PositionEvalOutput var lifeResult *LifeDeathOutput g.Go(func() error { var err error evalResult, err callPositionEval(ctx, input) return err }) g.Go(func() error { var err error lifeResult, err callLifeDeath(ctx, localInput) return err }) if err : g.Wait(); err ! nil { // 处理错误 }超时控制也很关键。LLM 调用有时候会卡住我一般给单个 Agent 设置 8 秒超时超时就降级返回默认值不能让用户干等。5. uni-app 前端的交互设计与 Agent 结果呈现5.1 为什么前端选 uni-app小程序要同时跑在多个平台上uni-app 一套代码多端发布省事。用 HBuilderX 开发入口文件是 main.js页面路由在 pages.json 里配置。这些基础的东西网上教程很多我重点讲和 Agent 交互相关的部分。5.2 流式输出怎么处理对话陪聊 Agent 的输出是流式的用户希望看到文字一个个蹦出来而不是等三秒突然出现一整段。uni-app 里处理流式输出我用的是分块接收 增量渲染。后端通过 SSE 或者分块传输把内容推过来前端每收到一块就追加到页面上。这里有个坑结构化输出的 Agent 不能流式渲染。因为 JSON 在没接收完之前是不完整的你没法解析。所以我的策略是陪聊 Agent 走流式其他六个 Agent 走一次性返回前端显示一个 loading 状态。5.3 棋盘渲染与坐标映射围棋棋盘的渲染看起来简单其实细节很多。19x19 的网格落子位置要精确对齐交叉点还要处理缩放和拖拽。我用的是 canvas 绘制坐标映射关系是屏幕坐标 棋盘边距 格子索引 * 格子大小Agent 返回的坐标是{x, y}的格子索引前端直接映射到屏幕坐标画棋子就行。这里要注意坐标系的一致性——后端用 0 起始的索引前端千万别搞成 1 起始否则棋子会整体偏移一格。提示棋盘状态和 Agent 结果建议分开管理。棋盘状态用响应式数据驱动渲染Agent 结果用单独的 store 管理避免 Agent 返回慢的时候阻塞棋盘操作。6. 实测中踩过的坑与调优经验6.1 提示词里的三个点key、query、value这是我调提示词时悟出来的一个框架分享给大家。任何一个 Agent 的提示词本质上都在回答三个问题key我是谁。也就是角色设定。是规则校验器还是形势判断助手角色不同输出的语气和格式完全不同。query我在找什么。也就是任务目标。是判断合法性还是生成候选点目标不清晰模型就会跑偏。value我能提供什么。也就是可用信息。棋盘状态、历史棋谱、候选列表这些是模型的输入素材。我早期写提示词经常只写了 key 和 query忘了明确 value 的边界结果模型会去脑补它没有的信息。比如死活分析时它看不到棋盘外的棋子却硬要判断全局形势。后来我在每个提示词里都明确列出你只能基于以下信息作答幻觉明显减少。6.2 温度参数的取舍不同 Agent 对 temperature 的要求完全不同Agent建议 temperature原因规则校验0需要确定性输出形势判断0.3允许一定灵活性候选点生成0.5需要多样性死活分析0.1推理必须严谨复盘讲解0.7需要自然表达对话陪聊0.8越自然越好调度路由0分类必须稳定这张表是我反复调出来的。一开始我所有 Agent 都用默认温度结果死活分析偶尔给出矛盾结论陪聊又显得死板。分开调之后每个 Agent 的表现都明显改善。6.3 成本控制的三个手段七个 Agent 听起来很费钱但实际跑下来成本可控靠的是三个手段小模型干小活。调度路由、规则校验这种简单任务用小模型就够了没必要上大模型。缓存重复计算。同一个局面如果用户反复问直接返回缓存结果不重复调用。局部切片。死活分析只传局部棋盘token 能省一大半。我算过一笔账拆成七个 Agent 之后虽然调用次数变多了但因为每次调用的 token 大幅减少加上小模型分流总体成本反而比单 Agent 方案低了大概四成。6.4 一个真实的翻车案例有一次用户反馈复盘讲解说我这步是好棋但形势判断显示我落后了。我去查日志发现是复盘讲解 Agent 和形势判断 Agent 对同一个局面的判断不一致。根因是复盘讲解 Agent 拿到的上下文里形势评分字段是空的——因为那次调用形势判断 Agent 超时降级了返回了默认值 0复盘 Agent 就误以为局面均势。修复方案是在上下文里加一个字段标记数据是否有效。如果形势判断是降级返回的复盘 Agent 就不引用它而是说本步分析暂不可用。这个坑让我明白Agent 之间的数据传递必须带有效性标记不能假设上游一定成功。7. 关于 Agent 编排的一点个人体会做这个项目最大的收获不是学会了怎么调 API而是理解了Agent 编排的本质是职责划分。七个 Agent 之所以比一个 Agent 好用不是因为数量多而是因为每个 Agent 的职责足够单一单一到提示词可以写得很精确输出可以约束得很死。我见过很多 Agent 项目一上来就搞一个万能 Agent什么都能干结果什么都干不好。我的建议是先想清楚有哪些独立的职责再决定拆几个 Agent。围棋这个场景天然就有规则、形势、死活、讲解这些独立维度所以拆七个是合理的。如果你的场景只有一个维度那一个 Agent 就够了别为了拆而拆。另外结构化输出这件事越早做越好。我一开始图快让模型自由输出然后写正则解析结果后期改提示词的时候正则全得重写。后来统一改成 JSON Schema 约束虽然前期多花了点时间设计 schema但后期维护成本低太多了。最后说个细节Agent 的命名很重要。我给七个 Agent 起的名字都是动词开头的比如 validate_move、evaluate_position、generate_candidates一看就知道干什么。团队协作的时候光看名字就能知道该找哪个 Agent 改省了很多沟通成本。
返回列表