首页 / 资讯中心 / 文章详情

围棋小程序中七个Agent的提示词设计与结构化输出实践

围棋小程序中七个Agent的提示词设计与结构化输出实践 ★ FEATURED ARTICLE
1. 项目缘起为什么要在围棋小程序里塞进七个 Agent围棋这个小程序表面上看就是个下棋工具但真正做过的人都知道它背后牵扯的东西远比“落子、吃子、判断胜负”复杂得多。我最初做这个项目的动机很朴素想给自己和几个棋友做一个能随时复盘、能问问题、能记录棋谱的小工具。但真动手之后才发现如果只是把 KataGo 或者 Leela 这类引擎接进来那顶多算个“带 AI 的棋盘”离“好用”还差得远。用户真正需要的是有人告诉他这步棋为什么臭、布局阶段该往哪走、中盘战斗时哪块棋有危险、官子阶段怎么收才不亏。这些需求单靠一个下棋引擎是解决不了的。于是我把目光转向了 LLM 和 Agent 架构。原因很简单围棋的“解释性”和“教学性”天然适合用自然语言来处理。引擎能算出胜率但说不清“为什么”LLM 能说清“为什么”但算不准胜率。两者结合才是正解。而 Agent 的价值在于它能把一个复杂的围棋任务拆成多个可管理的子任务每个子任务交给专门的 Agent 去处理最后汇总成用户能看懂的结果。这个项目我用了Golang作为后端主力语言框架选的是trpc-agent-go前端则是uni-app配合HBuilderX开发一套代码同时跑微信小程序和 H5。七个 Agent 分别负责棋局解析、形势判断、着法推荐、定式识别、死活题生成、复盘讲解、以及对话式问答。每个 Agent 都有自己的提示词模板和结构化输出格式彼此之间通过消息总线协作。下面我就把这七个 Agent 的场景设计、提示词写法和结构化输出的门道掰开揉碎讲清楚。提示Agent 数量不是越多越好。七个是我在实际调试中反复增删后稳定下来的数量再少会导致职责重叠再多会显著增加编排复杂度和 token 消耗。2. 整体架构七个 Agent 如何分工与协作2.1 Agent 角色划分与职责边界先明确一点Agent 不是“越多越智能”而是“越清晰越可靠”。我见过不少项目一上来就搞十几个 Agent结果每个都模棱两可最后编排逻辑比业务逻辑还复杂。我的划分原则是每个 Agent 必须有一个明确的、可用一句话描述的核心职责且它的输出是另一个 Agent 的输入或用户可直接消费的内容。七个 Agent 的职责如下Agent 名称核心职责输入输出棋局解析 Agent将 SGF 或坐标序列转为结构化棋盘状态原始棋谱字符串棋盘矩阵、手数、提子记录形势判断 Agent基于棋盘状态估算双方地盘和胜率结构化棋盘状态胜率、地盘估算、薄弱棋块着法推荐 Agent给出当前局面的候选着法及理由棋盘状态形势数据候选着法列表解释定式识别 Agent识别角部和边上的定式类型局部棋盘状态定式名称、后续变化死活题生成 Agent根据棋块状态生成死活题局部棋块题目描述、正解、变化图复盘讲解 Agent按手数生成自然语言讲解完整棋谱分析数据分段讲解文本对话问答 Agent回答用户关于棋局或围棋知识的自由提问用户问题上下文自然语言回答这个划分的好处是每个 Agent 的提示词可以写得非常聚焦。比如定式识别 Agent 的提示词里只需要关注角部 10x10 的区域不需要把整个棋盘塞进去token 消耗直接降了一个数量级。2.2 为什么选 trpc-agent-go 而不是自己撸一套Golang 生态里做 Agent 编排的框架不算多trpc-agent-go 是我对比了几个方案后选定的。原因有三点第一它原生支持流式输出和工具调用这对围棋这种需要边算边说的场景很关键第二它的 Agent 生命周期管理比较清晰有明确的 Init、Run、Stop 阶段方便我在每个阶段注入自定义逻辑第三它和 tRPC 生态结合得好我的后端服务本身就是 tRPC 架构接入成本低。自己撸一套不是不行但你要处理的东西太多了消息队列、状态管理、错误重试、超时控制、并发安全。这些 trpc-agent-go 都帮你兜底了你只需要关注业务逻辑。我试过用纯 Goroutine Channel 手写编排代码量大概是现在的三倍而且调试起来非常痛苦。2.3 uni-app 前端的角色与通信方式前端用 uni-app 主要是图它一套代码能同时跑小程序和 H5。HBuilderX 里创建项目后入口文件是main.js页面路由在pages.json里配置。和后端的通信走 WebSocket因为 Agent 的响应是流式的用 HTTP 轮询体验太差。这里有个坑小程序的 WebSocket 连接数有限制而且后台切换时会断开。我的做法是在App.vue的onHide和onShow里做连接管理断开后自动重连同时把未完成的消息 ID 存到本地重连后重新拉取结果。这个逻辑不复杂但如果不做用户切出去回个微信再回来对话就断了。3. 提示词设计每个 Agent 的“人设”怎么立3.1 提示词的基本结构角色、任务、约束、输出格式我写提示词有个固定模板四段式角色定义、任务描述、约束条件、输出格式。这四段缺一不可尤其是输出格式直接决定了后续代码能不能稳定解析。以形势判断 Agent 为例它的提示词大概长这样你是一名职业围棋形势判断专家擅长基于棋盘状态估算双方地盘和胜率。 任务根据给定的棋盘状态估算当前局面的胜率、双方地盘并指出薄弱棋块。 约束 - 胜率用百分比表示保留一位小数 - 地盘估算以“目”为单位给出黑白双方各自的总目数 - 薄弱棋块用坐标范围表示如“左上角星位附近” - 不要给出着法建议那是其他 Agent 的职责 输出格式JSON { win_rate: 52.3, black_territory: 45, white_territory: 42, weak_groups: [左上角星位附近, 右边路三线] }这个提示词里“不要给出着法建议”这条约束非常重要。早期版本我没写这条结果形势判断 Agent 经常越界去推荐着法导致和着法推荐 Agent 的输出冲突前端展示时用户看到两套矛盾的建议体验很差。3.2 结构化输出的三种模式JSON、XML、Markdown结构化输出我试过三种模式各有适用场景JSON 模式适合数据类 Agent比如棋局解析、形势判断。优点是解析稳定Golang 里直接json.Unmarshal就行。缺点是 LLM 有时候会漏掉字段或者多写字段需要在提示词里反复强调“只输出 JSON不要有任何其他文字”。XML 模式适合嵌套结构比如死活题生成 Agent 需要输出题目、正解、变化图三层结构。XML 的好处是标签天然有层级LLM 不容易搞混。但解析起来比 JSON 麻烦我用的是 Golang 标准库的encoding/xml需要提前定义好 struct。Markdown 模式适合讲解类 Agent比如复盘讲解和对话问答。这种场景下用户要的是可读性不是机器解析。但我会在提示词里要求用固定的标题层级比如## 布局阶段、## 中盘战斗这样前端可以用正则提取分段做折叠展示。3.3 提示词里的“防幻觉”技巧围棋领域有个特殊问题LLM 对围棋知识的掌握程度参差不齐经常编造不存在的定式名称或者错误的死活结论。我的防幻觉策略有三条第一给参考数据。定式识别 Agent 的提示词里会附带一个定式库的摘要让 LLM 从已知定式里选而不是自由发挥。这个定式库是我从公开棋谱里整理的一百多个常见定式每个定式有名称、坐标序列和简要说明。第二要求标注置信度。每个 Agent 的输出里都必须包含confidence字段取值 0 到 1。低于 0.6 的结果前端会标注“仅供参考”避免用户被错误信息误导。第三交叉验证。形势判断 Agent 和着法推荐 Agent 的输出会做一致性检查。如果形势判断说黑棋胜率 70%但着法推荐给出的最佳着法却是白棋的那就说明至少有一个 Agent 出错了系统会触发重新计算。注意防幻觉不是靠一句“不要编造”就能解决的。你必须给 LLM 提供可验证的参考数据并且设计好交叉验证机制否则幻觉只是从“明显错误”变成“隐蔽错误”。4. 结构化输出的工程实现从 LLM 输出到可用数据4.1 Golang 侧的数据结构定义在 Golang 里我为每个 Agent 的输出定义了对应的 struct。以形势判断为例type PositionAnalysis struct { WinRate float64 json:win_rate BlackTerritory int json:black_territory WhiteTerritory int json:white_territory WeakGroups []string json:weak_groups Confidence float64 json:confidence }这里有个细节WinRate用float64而不是int因为胜率需要保留小数。WeakGroups用切片而不是固定长度数组因为薄弱棋块的数量不固定。Confidence字段是所有 Agent 输出都必须有的我在基类里做了统一处理。解析的时候我会先做一次json.Valid检查如果不合法就尝试用正则提取 JSON 部分。LLM 有时候会在 JSON 前后加一些解释性文字虽然提示词里明确说了不要加但实际跑起来还是会有。我的做法是写一个extractJSON函数用栈匹配的方式找到第一个完整的 JSON 对象。4.2 流式输出的分块处理trpc-agent-go 支持流式输出但流式输出和结构化解析是矛盾的流式是一块一块来的JSON 必须完整才能解析。我的解决方案是分阶段处理流式阶段只做展示把原始文本实时推给前端等流结束后再做一次完整的结构化解析把解析结果存到数据库。前端展示时我会用一个简单的状态机来判断当前流到哪个字段了。比如检测到win_rate:就准备接收数字检测到weak_groups:就准备接收数组。这个状态机不复杂但能让用户在等待时看到逐步生成的内容体验比转圈圈好很多。4.3 错误处理与重试策略LLM 调用失败是常态不是异常。我统计过在高峰期大概有 5% 的请求会因为各种原因失败超时、限流、返回格式错误。我的重试策略是指数退避 降级第一次失败等 1 秒重试第二次失败等 3 秒重试第三次失败等 9 秒重试三次都失败降级到备用模型或者返回缓存结果降级这块我踩过坑。早期版本没有降级逻辑三次重试失败后直接给用户报错结果用户看到“服务暂时不可用”就流失了。后来我加了一个本地缓存把最近一百次成功分析的结果存下来失败时返回最接近的缓存结果并标注“基于历史数据分析”。虽然不完美但比直接报错好得多。5. 七个 Agent 的实操细节与踩坑记录5.1 棋局解析 AgentSGF 解析的坑比想象中多SGF 格式看起来简单实际解析起来坑很多。比如坐标表示SGF 用的是字母组合aa是左上角ss是右下角但棋盘大小不一定是 19x19可能是 9x9 或 13x13。还有转义字符SGF 里\]表示右括号本身不是结构结束。我一开始用正则解析结果遇到带注释的棋谱就崩了。后来我改用状态机解析逐字符扫描遇到(进入新节点遇到)结束节点遇到[进入属性值遇到]结束属性值。这个状态机大概两百行代码但稳定性比正则高了一个数量级。解析完成后输出一个二维数组表示棋盘状态0 表示空1 表示黑2 表示白。实操心得SGF 解析一定要做边界测试。我整理了一个包含各种奇葩情况的测试集空棋谱、只有一手的棋谱、带嵌套注释的棋谱、棋盘大小不是 19 的棋谱。每次改解析逻辑都跑一遍能避免大部分回归问题。5.2 形势判断 Agent胜率估算的校准形势判断 Agent 的输出是胜率但 LLM 估算的胜率和引擎估算的胜率往往对不上。我的做法是以引擎胜率为锚点让 LLM 做解释性调整。具体来说先把 KataGo 的胜率传给 LLM让它基于这个胜率分析局面特点而不是让它从零估算。提示词里我会写“当前引擎评估黑棋胜率 58%请分析这个胜率背后的局面特征并指出哪些因素可能导致胜率波动。”这样 LLM 的输出就有了锚点不会偏离太远。实测下来这种方式的胜率偏差能控制在 5% 以内而纯 LLM 估算的偏差经常超过 20%。5.3 着法推荐 Agent候选着法的排序逻辑着法推荐 Agent 的输出是一个候选着法列表每个着法有坐标、推荐理由和置信度。排序逻辑我试过几种按置信度排、按引擎胜率排、按位置优先级排。最后发现混合排序效果最好先按引擎胜率分档同一档内按置信度排置信度相同再按位置优先级排。位置优先级是我自己定义的一个规则角部 边路 中腹三线 四线 二线。这个规则不一定符合围棋理论但实测下来用户更倾向于接受角部和边路的推荐因为中腹的着法对业余棋手来说太难理解。5.4 定式识别 Agent局部匹配的精度问题定式识别最难的是局部匹配。用户可能只下了半个定式或者下了一个变招这时候识别精度会大幅下降。我的做法是模糊匹配 候选列表不要求完全匹配而是计算局部棋盘和定式库的相似度返回相似度最高的三个定式并标注匹配度。相似度计算用的是汉明距离把局部棋盘转成字符串和定式库里的字符串逐位比较相同位置相同棋子得 1 分不同得 0 分总分除以总位数就是相似度。这个方法简单但有效实测下来匹配度 0.8 以上的识别结果基本可信。5.5 死活题生成 Agent难度分级与正解验证死活题生成 Agent 的输出包括题目、正解和变化图。难度分级我用了三个维度棋块大小、眼位空间、外围援兵。棋块越大、眼位空间越小、外围援兵越少难度越高。每个维度分三档组合起来就是 27 种难度我把它映射到 1 到 10 的难度分。正解验证是个麻烦事。LLM 生成的正解不一定正确我试过用 KataGo 做二次验证但 KataGo 对死活题的判断有时候也不准。最后的方案是人工抽检 用户反馈新生成的一百道题我人工做一遍确认正解无误后入库用户做题时如果发现正解有问题可以点“报错”报错超过三次的题目自动下架。5.6 复盘讲解 Agent分段策略与语言风格复盘讲解 Agent 的输出是一段一段的讲解文本。分段策略我试过按手数分、按阶段分、按关键事件分。最后选了按关键事件分布局阶段每 10 手一段中盘战斗每 5 手一段官子阶段每 15 手一段。关键事件包括首次接触战、大龙形成、劫争出现、胜负处。语言风格上我要求 LLM 用“你”而不是“黑方/白方”比如“你这步棋下在星位意图是快速布局但忽略了右边的薄味”。这种第二人称的写法让用户更有代入感实测下来用户对讲解的满意度比第三人称高了 30%。5.7 对话问答 Agent上下文管理与知识边界对话问答 Agent 最大的挑战是上下文管理。用户可能问“这步棋怎么样”但“这步棋”指的是哪一步需要从对话历史里推断。我的做法是滑动窗口 关键信息提取保留最近 10 轮对话同时从每轮对话里提取关键信息手数、坐标、棋块存到一个结构化的上下文对象里。知识边界也很重要。用户问“围棋起源于哪个国家”这种问题 LLM 能答但和棋局无关。我的做法是意图分类先用一个小模型判断用户问题是“棋局相关”还是“知识相关”棋局相关的走对话问答 Agent知识相关的走通用问答通道。这样既保证了专业性又不会让用户觉得“你怎么连这个都不知道”。6. 常见问题与排查技巧实录6.1 Agent 输出格式错误的排查思路格式错误是最常见的问题表现是 JSON 解析失败、字段缺失、类型不对。我的排查步骤是先看原始输出确认是 LLM 的问题还是解析代码的问题如果是 LLM 的问题检查提示词里的输出格式说明是否足够明确如果是解析代码的问题检查 struct 定义和 JSON 字段是否匹配如果都正常检查是不是流式输出的分块边界导致的截断我整理了一个常见格式错误对照表错误表现可能原因解决方法JSON 解析失败LLM 在 JSON 前后加了文字用 extractJSON 提取字段缺失提示词约束不够强在提示词里加“必须包含所有字段”类型不对LLM 把数字写成了字符串在 struct 里用 json.Number数组为空LLM 没理解数组的含义在提示词里给数组示例中文乱码编码问题统一用 UTF-86.2 流式输出中断的处理流式输出中断的原因很多网络抖动、LLM 服务端超时、前端 WebSocket 断开。我的处理策略是断点续传每次流式输出都有一个唯一的 session ID中断后前端用这个 ID 重新请求后端从上次中断的位置继续推送。这个逻辑实现起来不复杂但需要后端把已经生成的内容缓存起来。我用的是 Redis每个 session 存一个列表每生成一块就 push 进去前端重连时先拉取已生成的内容再继续接收新的。6.3 多 Agent 协作时的死锁与超时多 Agent 协作最怕死锁Agent A 等 Agent B 的输出Agent B 等 Agent A 的输出结果谁都动不了。我的预防措施是依赖图 超时每个 Agent 的依赖关系在启动时就确定好形成有向无环图。如果检测到环启动直接报错。每个 Agent 调用都有超时默认 30 秒超时后返回降级结果不阻塞其他 Agent。注意依赖图一定要在代码里显式定义不要靠隐式约定。我早期版本就是靠“大家都知道谁先谁后”来协作结果加了一个新 Agent 后整个流程乱了排查了一整天才发现是依赖顺序错了。6.4 小程序端的性能优化小程序端的性能瓶颈主要在渲染。棋盘有 361 个交叉点每个交叉点都是一个 view渲染起来很卡。我的优化方案是Canvas 渲染 虚拟列表棋盘用 Canvas 画只渲染可视区域讲解文本用虚拟列表只渲染当前视口内的段落。这个优化做完后首屏渲染时间从 2 秒降到了 300 毫秒滚动帧率从 20 帧升到了 55 帧。用户感知很明显之前滑动棋盘会卡顿现在很流畅。7. 一些关于 Agent 开发的个人体会做这个项目最大的感受是Agent 开发的核心不是 LLM而是工程。LLM 只是其中一个组件真正决定项目成败的是编排逻辑、错误处理、性能优化、用户体验。我见过太多人把精力花在调提示词上结果工程一塌糊涂上线就崩。另一个体会是结构化输出是 Agent 落地的关键。没有结构化输出Agent 就是个聊天机器人没法集成到业务流程里。有了结构化输出Agent 才能成为系统的一部分和其他模块协作。我在这个项目里花了大概 40% 的时间在结构化输出的设计和调试上现在看来非常值得。最后分享一个小技巧给每个 Agent 写单元测试。用固定的输入跑 Agent检查输出是否符合预期格式和内容范围。这个测试不能保证输出完全正确但能保证输出格式稳定。每次改提示词后跑一遍测试能避免大部分回归问题。我现在的测试集有 200 多个用例覆盖了各种边界情况改提示词时心里有底。
阅读完成 · 觉得有帮助?
咨询建站