☰
围棋小程序中七个LLM Agent的架构设计与实践
2026/11/3 22:00:36 网站建设 项目流程

1. 项目缘起:为什么要在围棋小程序里塞进七个Agent

做围棋小程序的人多半有过这种体验:用户打开小程序,落子、悔棋、复盘,功能跑通了,但留存始终上不去。问题出在哪?我观察了很久,发现大部分围棋产品的交互是“死”的——用户点一步,界面动一下,仅此而已。没有反馈,没有引导,没有那种“对面坐了个懂棋的人”的感觉。

去年我开始琢磨,能不能用LLM驱动的Agent来填补这个空缺。不是做一个简单的“AI对手”,而是把围棋对弈过程中涉及的不同角色拆开,每个角色交给一个独立的Agent去处理。这个想法落地之后,就有了现在这套架构:一个uni-app写的小程序前端,后端用Golang的trpc-agent-go框架编排七个Agent,每个Agent有自己的场景定位、提示词策略和结构化输出格式。

七个Agent分别覆盖:对弈陪练、棋局解说、复盘分析、定式推荐、死活题生成、棋力评估、闲聊陪伴。听起来多,但拆开之后每个Agent的职责非常清晰,提示词好写,输出也好控制。这篇文章就把这套东西从头到尾拆一遍,包括我踩过的坑和最后跑通的方案。

适合谁看?如果你正在做小程序+LLM的应用,或者对Agent编排、结构化输出感兴趣,这篇应该能给你省不少时间。如果你只是好奇围棋产品怎么和AI结合,也能看到一些实际的设计取舍。

2. 整体架构设计:uni-app前端加trpc-agent-go后端

2.1 为什么选uni-app做前端

围棋小程序的用户场景很明确:碎片时间下盘棋、看个复盘、做几道死活题。这种场景下,原生App太重,H5又太轻,uni-app刚好卡在中间。一套代码可以编译到微信小程序、H5和App,对于我这种一个人维护的项目来说,省掉的适配成本非常可观。

具体到围棋场景,uni-app有几个点特别顺手。Canvas绘图能力足够画棋盘和棋子,而且在小程序端的性能表现比我想象中好。我试过用纯DOM画棋盘,19路棋盘361个交叉点,落子时的重绘会有明显卡顿。换成Canvas之后,配合脏矩形重绘,落子响应基本在16ms以内,肉眼看不到延迟。

另一个考虑是状态管理。围棋对弈的状态不算复杂,但涉及棋盘、手数、提子、禁着点这些数据,用Vuex或者Pinia管理起来很清晰。uni-app对这两者都支持,我最后选了Pinia,因为它的TypeScript支持更好,和后端的类型定义能对上。

提示:uni-app的Canvas在小程序端和H5端的API有细微差异,建议用条件编译处理。我是在#ifdef MP-WEIXIN里用小程序原生Canvas API,#ifdef H5里用标准Canvas API,避免了不少兼容性问题。

2.2 后端为什么用Golang和trpc-agent-go

后端选Golang,理由很直接:并发处理能力强,部署简单,单二进制文件扔到服务器就能跑。围棋Agent的调用涉及大量LLM请求,每个请求的响应时间从几百毫秒到几秒不等,用Golang的goroutine来处理并发调用非常自然。

trpc-agent-go是我在对比了几个Agent框架之后选的。它的核心优势在于Agent编排的抽象做得干净——每个Agent是一个独立的执行单元,有明确的输入输出定义,Agent之间的调用链可以通过配置来编排,不需要写一堆胶水代码。而且它原生支持结构化输出,这对围棋场景特别重要,因为棋盘状态、走法建议这些数据必须是结构化的,不能是一段自由文本。

框架的另一个好处是内置了重试和降级机制。LLM调用偶尔会超时或者返回格式不对,trpc-agent-go可以配置自动重试,重试失败后走降级逻辑。我在实际运行中把重试次数设为2次,降级策略是返回一个预设的兜底响应,保证用户不会看到空白或者报错。

2.3 七个Agent的职责划分

七个Agent不是拍脑袋定的,而是按照围棋对弈的完整流程拆出来的。下面这张表是我最终确定的职责划分:

Agent名称触发场景核心职责输出类型
对弈陪练用户落子后生成对手走法结构化走法坐标
棋局解说每走一步后用自然语言解释当前局面文本+关键点标注
复盘分析对局结束后分析胜负关键手结构化报告
定式推荐开局阶段推荐当前局面的定式走法序列+说明
死活题生成用户请求练习根据棋力生成死活题棋盘状态+答案
棋力评估对局结束后评估用户棋力水平评分+段位估算
闲聊陪伴用户空闲时围棋相关闲聊自然语言文本

这个划分的关键在于每个Agent的输入输出边界非常清晰。对弈陪练只负责生成走法,不负责解释;棋局解说只负责解释,不负责走法。这样每个Agent的提示词可以写得很聚焦,LLM的输出质量也更容易控制。

3. 七个Agent的场景拆解与提示词设计

3.1 对弈陪练Agent:让LLM学会“下棋”

这是七个Agent里最难做的一个。LLM本质上是一个语言模型,让它生成合法的围棋走法,需要大量的提示词工程和输出约束。

我的做法是不给LLM看完整棋盘。19路棋盘有361个交叉点,如果把整个棋盘状态用文本描述给LLM,token消耗巨大,而且LLM很容易在长文本中迷失。我的方案是只给LLM看局部棋盘——以当前落子位置为中心,取一个9x9的区域,加上全局的形势判断信息(比如双方实地大致对比、当前手数、是否进入收官阶段)。

提示词的核心结构是这样的:

你是一个围棋对弈Agent,当前执黑/白。 棋盘局部状态(9x9区域,X表示黑子,O表示白子,.表示空点): ......... ..X.O.... ...X..... ......... 当前手数:45 形势判断:黑棋实地领先约5目,白棋外势较厚 请生成你的下一手走法,输出格式为JSON: {"move": "D4", "reason": "简要说明"}

这个提示词的关键在于形势判断信息。LLM不懂围棋,但它能理解“实地领先5目”这种描述,并据此调整走法策略。我试过不给形势判断,LLM的走法会非常随机,经常下出明显亏损的棋。

输出格式用JSON约束,配合trpc-agent-go的结构化输出校验,如果LLM返回的坐标不合法(比如落在已有棋子的位置),框架会自动重试。实测下来,第一次调用合法率大概在85%左右,重试一次后能到97%以上。

注意:LLM生成的走法偶尔会“看起来合理但实际上是禁着点”。我的处理方式是在后端加一层合法性校验,如果走法非法,直接走降级逻辑——用一个简单的蒙特卡洛树搜索生成一个合法走法。这个降级逻辑虽然棋力一般,但至少保证对局能继续。

3.2 棋局解说Agent:把局面翻译成人话

棋局解说Agent的定位是“懂棋的朋友在旁边帮你分析”。它的输入是对弈陪练Agent生成的走法和当前棋盘状态,输出是一段自然语言解说。

这个Agent的提示词设计有一个核心原则:说人话,不说术语。我试过让LLM用专业术语解说,结果用户反馈“看不懂”。后来改成用生活化类比,比如“这步棋就像在对方家门口插了一面旗子,虽然暂时没威胁,但对方得时刻提防着”,用户接受度明显提高。

提示词示例:

你是一个围棋解说Agent,用通俗易懂的语言解释当前走法。 当前走法:黑棋D4 棋盘状态:...(局部棋盘) 请用2-3句话解释这步棋的意图,避免使用专业术语,用生活化类比。 输出格式:{"commentary": "解说文本", "key_point": "关键点坐标"}

解说Agent的响应速度要求比对弈陪练高,因为用户落子后期待即时反馈。我把这个Agent的LLM调用超时设为3秒,超过3秒直接返回一个简短的通用解说,比如“这步棋很有想法,让我们看看接下来怎么发展”。

3.3 复盘分析Agent:找出胜负关键手

复盘分析Agent是在对局结束后触发的,它的任务是分析整盘棋的胜负关键手。这个Agent的输入是完整的对局记录(每一步的坐标和手数),输出是一份结构化报告。

这里有一个技术难点:完整对局记录可能很长,一盘19路棋局可能有200-300手,全部塞给LLM会超出上下文窗口。我的解决方案是分段分析+汇总。先把对局按50手一段切分,每段交给LLM分析,找出该段的关键手,然后再把各段的关键手汇总,让LLM做最终判断。

提示词的核心是引导LLM关注胜率变化。我会在输入中附带每一步的胜率估算(这个数据来自一个轻量级的围棋AI引擎,不是LLM生成的),LLM只需要找出胜率变化最大的几步,然后解释原因。

输出格式:

{ "key_moves": [ {"move_number": 45, "coordinate": "D4", "winrate_change": -15.2, "analysis": "这步棋导致胜率大幅下降..."}, ... ], "summary": "整盘棋的胜负关键在..." }

3.4 定式推荐Agent:开局阶段的走法库

定式推荐Agent只在开局阶段(前30手)触发。它的输入是当前棋盘状态和手数,输出是推荐的定式走法序列。

这个Agent的提示词里我嵌入了一个定式库的摘要。不是把整个定式库塞进去,而是把常见定式的名称和对应的走法序列用简洁的格式列出来,让LLM根据当前局面匹配最合适的定式。

常见定式: - 星位小飞挂:D4-F3-C6 - 三三入侵:D4-C3-D3 - ... 当前局面:...(棋盘状态) 请推荐最合适的定式,输出格式:{"pattern_name": "星位小飞挂", "moves": ["F3", "C6"], "explanation": "..."}

定式推荐Agent的准确率比较依赖定式库的质量。我最初只放了20个常见定式,LLM经常推荐不匹配的定式。后来扩充到80个,覆盖了大部分开局场景,准确率明显提升。

3.5 死活题生成Agent:根据棋力动态出题

死活题生成Agent是我个人最喜欢的一个。它的输入是用户的棋力评估结果(来自棋力评估Agent),输出是一道死活题的棋盘状态和答案。

这个Agent的提示词设计有一个巧妙之处:用模板+参数的方式生成题目。我不让LLM从零生成死活题,而是预置了一批死活题模板,每个模板有难度参数(1-10),LLM根据用户棋力选择合适的模板,然后微调棋盘上的棋子位置。

死活题模板库: - 模板A:角部死活,难度3-5,基础形状... - 模板B:边上死活,难度5-7,基础形状... 用户棋力:业余3级(对应难度4) 请选择合适模板并生成题目,输出格式:{"board": "...", "answer": ["D4", "E5"], "difficulty": 4}

这样做的好处是题目质量可控,不会出现LLM生成的“无解”死活题。我试过让LLM完全自由生成,结果有30%左右的题目要么无解,要么有多个答案,用户体验很差。

3.6 棋力评估Agent:给用户一个靠谱的段位

棋力评估Agent的输入是用户最近的对局记录(胜负、对手棋力、关键手胜率变化),输出是一个棋力评分和估算段位。

这个Agent的提示词里我加了一个评分公式的说明,让LLM按照公式来计算,而不是凭感觉给分:

棋力评分公式: 基础分 = 对手平均棋力 × 0.6 + 胜率 × 40 调整分 = 关键手平均胜率变化 × 0.5 最终评分 = 基础分 + 调整分 段位映射:评分0-20=业余1级,20-40=业余2级,... 请根据以下对局记录计算棋力评分: ...

用公式约束之后,棋力评估的稳定性好了很多。之前LLM给的评分波动很大,同样水平的用户,有时候评业余1级,有时候评业余3级。加了公式之后,波动范围缩小到半个段位以内。

3.7 闲聊陪伴Agent:让小程序有点温度

闲聊陪伴Agent是唯一一个不涉及围棋技术的Agent,它的职责就是在用户空闲时陪聊。输入是用户的聊天消息,输出是自然语言回复。

这个Agent的提示词里我设定了一个人设:一个下了二十年围棋的业余棋手,性格随和,喜欢用围棋比喻生活。比如用户说“今天工作好累”,Agent会回“下棋也是这样,中盘绞杀最耗神,该收官的时候就得收官,别硬撑”。

人设提示词:

你是一个下了二十年围棋的业余棋手,性格随和,喜欢用围棋比喻生活。 用户说:{user_message} 请用1-2句话回复,保持轻松自然的语气。

闲聊Agent的LLM调用参数和其他Agent不同,我调高了temperature(从0.3调到0.8),让回复更有变化。其他Agent的temperature都设在0.2-0.4之间,保证输出的稳定性。

4. 结构化输出的实现细节与踩坑记录

4.1 为什么结构化输出是刚需

围棋场景对数据格式的要求非常严格。棋盘坐标必须是合法的(A-T,跳过I,1-19),走法必须是合法的(不能落在已有棋子的位置),胜率必须是0-100之间的数字。如果LLM返回一段自由文本,后端解析起来会非常痛苦。

trpc-agent-go的结构化输出功能解决了这个问题。它通过JSON Schema约束来强制LLM按照指定格式输出。我在每个Agent的配置里都定义了输出Schema,框架会在LLM返回后自动校验,不符合Schema的响应会被拒绝并触发重试。

以对弈陪练Agent为例,它的输出Schema是这样的:

{ "type": "object", "properties": { "move": {"type": "string", "pattern": "^[A-HJ-T][1-9]|1[0-9]$"}, "reason": {"type": "string", "maxLength": 100} }, "required": ["move", "reason"] }

这个Schema里的pattern正则确保了走法坐标的合法性。如果LLM返回了“I5”这种非法坐标(围棋棋盘没有I列),Schema校验会直接失败,框架自动重试。

4.2 提示词里的结构化输出引导

光有Schema约束还不够,提示词里也需要明确告诉LLM输出格式。我的经验是在提示词末尾用单独的段落强调输出格式,并且给一个具体的示例。

比如对弈陪练Agent的提示词末尾:

请严格按照以下JSON格式输出,不要添加任何其他内容: {"move": "D4", "reason": "占据星位,便于向两侧发展"}

这个示例很重要。我试过不给示例,只给Schema描述,LLM的输出格式错误率会高很多。给了示例之后,格式错误率从15%降到了3%左右。

4.3 处理LLM返回的“意外情况”

即使有Schema约束和提示词引导,LLM偶尔还是会返回意外内容。我遇到过的典型情况包括:

  • 返回了多个JSON对象(LLM把思考过程也输出了)
  • JSON字段名拼写错误(比如把“move”写成“moves”)
  • 坐标格式正确但语义错误(比如返回了“A0”,但围棋棋盘没有0路)

针对这些情况,我在trpc-agent-go的重试配置里加了一个自定义校验函数。框架在Schema校验通过后,还会调用这个函数做二次校验。如果二次校验失败,同样触发重试。

func validateMove(resp map[string]interface{}) error { move := resp["move"].(string) if !isValidCoordinate(move) { return fmt.Errorf("invalid coordinate: %s", move) } if isOccupied(move) { return fmt.Errorf("position occupied: %s", move) } return nil }

这个二次校验函数帮我拦截了不少“格式正确但内容错误”的响应。实测下来,加上二次校验之后,对弈陪练Agent的走法合法率从97%提升到了99.5%以上。

4.4 结构化输出的性能开销

结构化输出会带来额外的token消耗和延迟。JSON Schema的描述本身要占token,LLM生成JSON格式的输出也比生成自由文本慢一些。我实测下来,结构化输出的延迟比自由文本高20%-30%。

对于对弈陪练和棋局解说这两个对延迟敏感的Agent,我做了一些优化。对弈陪练Agent的Schema尽量精简,只保留必要的字段。棋局解说Agent的输出Schema里,commentary字段设了maxLength,避免LLM生成过长的解说。

实操心得:如果你的应用对延迟要求很高,可以考虑把结构化输出和自由文本输出分开。关键数据用结构化输出,辅助性的解释用自由文本。我在棋局解说Agent里就是这么做的——key_point字段用结构化输出,commentary字段用自由文本,兼顾了准确性和响应速度。

5. 常见问题与排查技巧实录

5.1 LLM返回的走法不合法怎么办

这是最常见的问题。LLM生成的走法可能落在已有棋子的位置,或者落在禁着点。我的处理策略是三层防护:

第一层是Schema校验,用正则表达式确保坐标格式正确。第二层是自定义校验函数,检查坐标是否在棋盘范围内、是否被占用。第三层是降级逻辑,如果前两层都通过但走法仍然不合法(比如打劫规则),直接调用一个简单的蒙特卡洛树搜索生成合法走法。

三层防护下来,对弈陪练Agent的走法合法率能到99.9%以上。剩下0.1%的情况是LLM返回了完全无法解析的内容,这时候直接走降级逻辑,用户基本感知不到。

5.2 Agent响应超时怎么处理

LLM调用的响应时间波动很大,快的时候几百毫秒,慢的时候十几秒。我给每个Agent都设了超时时间,超时后走降级逻辑。

Agent超时时间降级策略
对弈陪练5秒蒙特卡洛树搜索生成走法
棋局解说3秒返回通用解说模板
复盘分析15秒返回简版复盘报告
定式推荐3秒返回最常见的定式
死活题生成8秒返回预设的固定题目
棋力评估10秒返回基于胜率的粗略估算
闲聊陪伴5秒返回预设的闲聊回复

超时时间的设定需要根据实际网络环境和LLM服务商的响应速度来调整。我最初把对弈陪练的超时设为3秒,结果在晚高峰时段经常触发降级。后来调到5秒,降级率从12%降到了3%左右。

5.3 七个Agent的调用成本怎么控制

七个Agent如果每个都调用LLM,成本会很高。我的优化策略是按需触发。不是每个Agent在每次对局中都会触发:

  • 对弈陪练:每次用户落子后触发,频率最高
  • 棋局解说:每3手触发一次,不是每手都触发
  • 复盘分析:对局结束后触发一次
  • 定式推荐:只在开局前30手触发
  • 死活题生成:用户主动请求时触发
  • 棋力评估:对局结束后触发一次
  • 闲聊陪伴:用户主动发消息时触发

这样下来,一盘200手的对局,对弈陪练调用约200次,棋局解说约67次,其他Agent加起来不超过10次。总调用次数控制在300次以内,成本可控。

另外,对弈陪练Agent我用了缓存机制。相同的棋盘局部状态,如果之前已经请求过,直接返回缓存结果。围棋的局部状态重复率其实不低,缓存命中率大概在15%左右,进一步降低了成本。

5.4 小程序端的性能优化

uni-app编译到小程序端之后,性能瓶颈主要在Canvas绘图和网络请求。Canvas绘图我用了脏矩形重绘,只重绘发生变化的区域,而不是整个棋盘。网络请求方面,我把Agent的调用改成了流式返回,用户落子后先显示一个“思考中”的动画,Agent返回后再更新界面。

还有一个细节是请求合并。用户落子后,对弈陪练和棋局解说两个Agent都需要触发。我把这两个请求合并成一个批量请求,后端并行调用两个Agent,然后一起返回。这样减少了小程序端的网络往返次数,响应速度更快。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
Agent返回空响应LLM服务超时查看后端日志中的超时记录调整超时时间,增加重试次数
走法坐标非法LLM输出格式错误检查Schema校验日志优化提示词,增加示例
解说内容重复temperature过低查看LLM调用参数适当调高temperature
复盘报告不完整对局记录过长检查分段逻辑调整分段大小,增加汇总步骤
死活题无解模板选择错误检查模板难度参数增加模板库,优化匹配逻辑
棋力评估波动大评分公式未生效检查提示词中的公式说明强化公式约束,增加示例
闲聊回复生硬人设提示词不够具体检查人设描述增加性格细节和示例对话

6. 一些实操心得和后续扩展方向

这套七个Agent的架构跑了大半年,整体稳定性还不错。有几个心得值得单独拎出来说。

第一,Agent的粒度不是越细越好。我最初想把对弈陪练拆成“布局Agent”“中盘Agent”“收官Agent”三个,后来发现拆得太细之后,每个Agent的提示词都要重复写一遍棋盘状态描述,token消耗反而增加了。最后合并成一个对弈陪练Agent,通过提示词里的“当前阶段”参数来区分策略,效果更好。

第二,结构化输出的Schema要尽量宽松。我一开始把Schema写得很严格,比如reason字段要求不超过50个字。结果LLM经常超出,触发重试,浪费了不少token。后来把限制放宽到100个字,重试率明显下降。Schema的目的是保证数据可用,不是追求完美。

第三,降级逻辑比想象中重要。LLM服务不可能100%可用,网络波动、服务商限流都会导致调用失败。如果没有降级逻辑,用户看到的就是报错或者空白。我现在的降级逻辑虽然简单,但至少保证了用户体验的连续性。

后续我打算在这几个方向继续折腾:一是把死活题生成Agent的模板库扩充到500个以上,覆盖更多棋力段位;二是给复盘分析Agent加上“对比分析”功能,让用户可以选择两个不同时期的对局进行对比;三是探索用更小的模型来处理对弈陪练Agent的走法生成,进一步降低成本。

这套东西说到底就是一个思路:把复杂问题拆成多个简单问题,每个问题交给一个专门的Agent去处理。围棋是这样,其他领域应该也差不多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询