news 2026/11/3 14:11:12

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
围棋小程序中七个Agent的提示词设计与结构化输出实践

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 棋局解析 Agent:SGF 解析的坑比想象中多

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 解析失败、字段缺失、类型不对。我的排查步骤是:

  1. 先看原始输出,确认是 LLM 的问题还是解析代码的问题
  2. 如果是 LLM 的问题,检查提示词里的输出格式说明是否足够明确
  3. 如果是解析代码的问题,检查 struct 定义和 JSON 字段是否匹配
  4. 如果都正常,检查是不是流式输出的分块边界导致的截断

我整理了一个常见格式错误对照表:

错误表现可能原因解决方法
JSON 解析失败LLM 在 JSON 前后加了文字用 extractJSON 提取
字段缺失提示词约束不够强在提示词里加“必须包含所有字段”
类型不对LLM 把数字写成了字符串在 struct 里用 json.Number
数组为空LLM 没理解数组的含义在提示词里给数组示例
中文乱码编码问题统一用 UTF-8

6.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 多个用例,覆盖了各种边界情况,改提示词时心里有底。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 14:11:03

REA:让AI推理过程可追溯、可验证的运行时解释架构

1. REA不是“反向AI”,而是让AI自己拆解自己的新思路最近在几个技术社区里,看到不少开发者把“REA”误读成“Reverse Engineering AI”的缩写,顺手就往模型逆向、权重提取、黑盒破解的方向去想。我一开始也这么理解,直到参与某高校…

作者头像 李华
网站建设 2026/11/3 14:10:28

iPerf网络性能测试工具使用指南与实战经验

1. iPerf网络性能测试工具概述iPerf是一款开源的网络性能测试工具,专门用于测量TCP和UDP协议的网络带宽、延迟、抖动和数据包丢失等关键指标。作为网络工程师和系统管理员必备的测试工具,它能够帮助我们准确评估网络链路的实际性能表现。我第一次接触iPe…

作者头像 李华
网站建设 2026/11/3 14:09:44

从KV cache共享到批量调度:注意力机制下的隔离问题与口令实验

1. 从“共享状态”说起:一个被忽视的工程陷阱“共享状态,隔离问题”这个说法,第一次听到是在一次内部技术复盘会上。当时团队在排查一个推理服务的性能抖动问题,现象很诡异:单条请求跑得好好的,一旦并发上来…

作者头像 李华
网站建设 2026/11/3 14:09:28

开源软件的发展历程、核心哲学与协作模式

1. 开源运动的起源与哲学开源软件的概念最早可以追溯到上世纪60年代,当时计算机软件通常与硬件捆绑销售,源代码自由共享是行业常态。随着商业软件模式兴起,这种开放文化逐渐式微,直到1983年Richard Stallman发起GNU计划&#xff0…

作者头像 李华
网站建设 2026/11/3 14:08:34

313MB加密包背后的静默上传暗门:Android恶意样本分析实战

在移动安全测试这一行待久了,各种奇怪的样本你都会遇到。前阵子我手头就多了一个313MB的加密包——名字看着是个通用工具类Android应用,体积却大得离谱,而且整体被加固加密处理过,静态扫描基本扫不出东西。这种组合在真实黑产样本…

作者头像 李华
网站建设 2026/11/3 14:06:43

RocketMQ并发消费与重试机制实战解析

1. RocketMQ并发消费与重试机制的核心场景 在分布式消息系统中,消息消费的可靠性保障一直是架构设计的难点。RocketMQ作为阿里巴巴开源的分布式消息中间件,其MessageListenerConcurrently接口正是为解决高并发场景下的消息处理而设计。与顺序消费不同&am…

作者头像 李华