1. 从"赋饭"这个名字说起:一个猜谜Agent到底在猜什么
第一次看到"赋饭-九天猜猜"这个项目名,我脑子里冒出来的第一个念头是:这名字起得挺有意思。"赋饭"谐音"赋范",数学里赋范空间的概念,暗示这个项目有一套自己的度量标准;而"九天猜猜"又带着点游戏化的轻松感。把这两个词放在一起,基本能判断出这是一个基于大语言模型的猜谜类Agent智能体,核心玩法大概率是"你画我猜"或者"你问我猜"这种交互形式。
猜谜这件事,看起来简单,实际上对Agent的能力要求相当综合。它不像普通的问答机器人,问什么答什么就行。猜谜Agent需要同时具备几个能力:意图理解(从用户的模糊描述中提取关键线索)、知识检索(把线索和候选答案做匹配)、策略决策(决定下一轮问什么问题最有效率)、状态管理(记住已经排除的选项和已经确认的线索)。这四个能力缺一个,猜谜体验就会大打折扣。
我之所以对这个项目感兴趣,是因为它踩中了一个很实际的需求场景:如何让大模型Agent在信息不完整的情况下,通过多轮交互逐步逼近正确答案。这个能力一旦跑通,可以迁移到很多场景——智能客服的故障排查、医疗问诊的初步分诊、教育领域的引导式教学,本质上都是"猜谜"的变体。
这篇文章我会从Agent的架构设计、猜谜策略的实现、多轮对话的状态管理、以及实际开发中踩过的坑这几个角度,把这个项目的核心逻辑拆开来讲。不管你是刚接触Agent开发的新手,还是已经做过几个Demo想找新思路的老手,应该都能从中拿到一些可以直接用的东西。
2. 猜谜Agent的架构选型:为什么我没有一上来就堆框架
2.1 从"最小可行Agent"开始,而不是从LangChain开始
很多开发者做Agent项目的第一反应是先把LangChain或者AutoGPT这类框架搭起来,觉得这样显得"专业"。我一开始也动过这个念头,但很快就放弃了。原因很简单:猜谜这个场景的交互链路其实很短,核心循环就是"接收线索→推理→输出猜测或提问→接收反馈",用框架反而会增加调试的复杂度。
我最终选择的方案是裸调API + 自建状态机。具体来说,整个Agent的核心就是一个循环:
class GuessAgent: def __init__(self, llm_client, knowledge_base): self.llm = llm_client self.kb = knowledge_base self.history = [] # 对话历史 self.candidates = [] # 候选答案池 self.confirmed_clues = [] # 已确认的线索 self.excluded = set() # 已排除的答案 def step(self, user_input): self.history.append({"role": "user", "content": user_input}) # 1. 线索抽取 clues = self.extract_clues(user_input) # 2. 候选池更新 self.update_candidates(clues) # 3. 决策:继续问还是直接猜 action = self.decide_action() # 4. 生成回复 response = self.generate_response(action) self.history.append({"role": "assistant", "content": response}) return response这个结构看起来朴素,但它把推理逻辑和对话管理解耦了。LLM只负责它擅长的事情——理解自然语言、生成自然语言;而候选池的维护、排除逻辑、决策策略这些确定性强的部分,用代码来控制。这样做的好处是,当Agent猜错的时候,我能快速定位是"线索抽取错了"还是"候选池更新逻辑有bug",而不是在一堆框架抽象层里大海捞针。
2.2 知识库的构建:猜谜Agent的"弹药库"
猜谜Agent的猜谜能力上限,很大程度上取决于它的知识库覆盖范围。我一开始想直接用LLM的内部知识来猜,但实测下来发现两个问题:一是LLM对某些垂直领域的知识覆盖不够(比如特定品类的商品、小众的影视作品),二是LLM有时候会"幻觉"出一个不存在的答案,然后一本正经地继续猜。
所以我的做法是自建一个结构化的候选答案库,每个条目包含:
| 字段 | 说明 | 示例 |
|---|---|---|
| 名称 | 答案的标准名称 | 电饭煲 |
| 别名 | 常见别称 | 电锅、饭锅 |
| 类别 | 所属大类 | 厨房电器 |
| 属性标签 | 关键特征 | 加热、煮饭、家用、插电 |
| 易混淆项 | 容易搞混的答案 | 高压锅、蒸锅 |
这个库不需要很大,几千条就能覆盖大部分日常猜谜场景。关键是属性标签要足够细,因为Agent问问题的策略就是基于这些标签来的。比如它知道答案是"厨房电器"类别,就会优先问"是不是用来做饭的",而不是问"是不是交通工具"。
这里有个经验:属性标签的粒度要控制在"能用一个是/否问题区分"的层面。太粗了(比如"家用")区分度不够,太细了(比如"功率800W")又没人会这么描述。
2.3 为什么选择"二十问"作为核心玩法
猜谜的玩法有很多种,我最终选了"二十问"(20 Questions)作为核心机制。这个选择背后有几个考虑:
第一,规则简单,用户零学习成本。用户不需要知道任何规则,只要回答"是"或"不是",或者给一点提示就行。这比让用户画图或者写描述的门槛低得多。
第二,对Agent的策略能力有真实考验。二十问的最优策略是每次问一个能把候选集尽量对半砍的问题,这正好是信息论里"最大信息增益"的直观体现。Agent问得好不好,用户能直接感受到。
第三,有明确的终止条件。二十个问题之内猜出来算赢,猜不出来算输,这个约束让整个交互有张力,不会无限拖下去。
实际实现的时候,我把问题数量限制放宽到了"不严格限制,但超过十五轮还没猜出来就主动认输并揭晓答案"。因为实测发现,用户有时候给的线索很模糊,硬卡二十轮体验反而不好。
3. 让Agent学会"问对问题":猜谜策略的工程实现
3.1 信息增益:猜谜Agent的决策核心
猜谜Agent最核心的能力,是在每一轮选择信息增益最大的问题。这个概念来自信息论,但用大白话解释就是:问一个能把当前候选集尽量对半砍的问题。
假设当前候选池里有100个可能的答案,你问"是不是动物",如果答案是动物,候选池可能缩小到30个;如果不是,缩小到70个。这个问题的信息增益就不如问"是不是有生命",因为后者可能把候选池砍成50/50。
在工程实现上,我用了一个简化版的评分函数:
def score_question(question, candidates): # 基于属性标签估算这个问题能把候选集分成多大的两半 yes_count = sum(1 for c in candidates if matches(c, question)) no_count = len(candidates) - yes_count if yes_count == 0 or no_count == 0: return 0 # 这个问题没有区分度 # 用熵的减少量作为分数 total = len(candidates) p_yes = yes_count / total p_no = no_count / total entropy_before = math.log2(total) entropy_after = -(p_yes * math.log2(p_yes) + p_no * math.log2(p_no)) return entropy_before - entropy_after实际跑的时候,我不会让LLM去算这个分数,而是让LLM生成一批候选问题,然后用代码来打分排序。LLM擅长生成自然的问题表述,代码擅长做确定性计算,各司其职。
3.2 问题生成的Prompt设计:约束比自由更重要
让LLM生成猜谜问题,最容易犯的错误是给太多自由。如果你只是说"请生成一个猜谜问题",它可能会生成"这个东西是不是让人感到幸福"这种没法用是/否回答的问题,或者"它是不是在2015年之后被发明"这种过于具体的问题。
我的Prompt设计遵循几个原则:
- 强制二值化:问题必须能用"是"或"不是"回答,不允许"可能""有时候"这种模糊答案
- 限制问题类型:优先问类别、功能、材质、使用场景,避免问具体年份、品牌、人名
- 给出候选池摘要:把当前候选池的类别分布告诉LLM,让它有针对性地提问
- 要求一次只问一个维度:不允许"它是不是既能吃又能玩"这种复合问题
一个实际用的Prompt模板大概长这样:
你是一个猜谜游戏的提问者。当前候选答案池的类别分布如下: - 厨房电器:35个 - 家具:20个 - 交通工具:15个 - 其他:30个 已经确认的线索:可以用来加热、家用 已经排除的类别:交通工具 请生成3个候选问题,要求: 1. 每个问题只能用"是"或"不是"回答 2. 问题要能有效缩小候选范围 3. 优先问类别和核心功能,不要问具体品牌或年份 4. 一次只问一个维度这个模板跑下来,生成的问题质量明显比自由生成高一个档次。
3.3 当用户回答"不知道"的时候怎么办
实际猜谜过程中,用户经常会给"不知道""可能吧""差不多"这种模糊回答。这时候如果Agent还傻乎乎地按二值逻辑更新候选池,就会出问题。
我的处理策略是引入三值逻辑:是、否、不确定。对于"不确定"的回答,候选池不做硬性过滤,但会给相关属性打一个"弱信号"标记。在后续决策时,弱信号的问题优先级会降低,因为问了也得不到明确答案。
具体实现上,我在候选池里给每个条目维护一个"置信度分数",初始都是1.0。用户回答"是"的时候,匹配该属性的条目分数乘以1.2,不匹配的乘以0.5;回答"不是"的时候反过来;回答"不确定"的时候,所有条目分数不变,但该属性被标记为"低区分度",后续不再优先问。
这个机制跑下来,Agent在面对模糊回答时的鲁棒性好了很多,不会因为一两个"不知道"就把正确答案排除掉。
4. 多轮对话的状态管理:别让Agent"失忆"
4.1 对话历史不是越长越好
猜谜Agent需要记住之前的对话,但把所有历史都塞进Context并不是好主意。一方面Token成本会线性增长,另一方面太长的Context会让LLM的注意力分散,反而容易忽略关键线索。
我的做法是分层记忆:
- 短期记忆:最近3轮对话的原文,保持对话连贯性
- 中期记忆:已确认的线索列表,用结构化格式存储(比如"已确认:可以加热、家用、有盖子")
- 长期记忆:已排除的答案类别和具体条目,只存摘要不存原文
这样每轮调用LLM的时候,实际传入的Context是可控的,大概在500-800 Token左右,既保留了必要信息,又不会让模型"看花眼"。
4.2 线索冲突的检测与处理
多轮对话里最容易出的问题是线索冲突。比如用户先说"是厨房用的",后来说"不是用来做饭的",这两条线索如果都硬性保留,候选池可能直接被清空。
我的处理方式是加一个冲突检测层。每次新增线索的时候,先检查它和已有线索是否矛盾。如果矛盾,不直接更新候选池,而是生成一个澄清问题:"你刚才说它是厨房用的,但又说不是用来做饭的,那它是不是厨房里的收纳用品?"
这个澄清机制看起来简单,但实际体验提升很大。用户会觉得Agent"真的在听",而不是机械地执行指令。
4.3 猜错之后的回滚策略
Agent猜错是常有的事。关键是猜错之后怎么处理。我的策略是不直接回滚,而是把错误答案加入排除集,然后基于当前线索重新生成候选池。
这里有个细节:如果Agent连续猜错两次,说明当前的线索理解可能有问题。这时候我会触发一个"重新校准"流程,让LLM重新审视所有已确认的线索,检查是否有理解偏差。实测下来,这个校准流程能挽回大概30%的"即将失败"的对局。
5. 实测中踩过的坑与调优经验
5.1 坑一:LLM的"过度推理"
早期版本里,我让LLM直接根据对话历史输出下一个猜测。结果发现它经常"过度推理"——用户只是说"是家用的",它就猜"是电饭煲",完全跳过了中间的问询环节。
修复方案:在Prompt里明确约束"在候选池大于10个的时候,不允许直接猜测,必须继续提问"。同时把"猜测"和"提问"做成两个独立的动作,由代码根据候选池大小来决定当前该执行哪个。
5.2 坑二:候选池爆炸
有一次测试的时候,用户给了一个很模糊的线索"是圆的",结果候选池从50个膨胀到了200多个,因为太多东西都是圆的。Agent接下来问了好几个问题都没能把范围缩回来,最后超时失败。
修复方案:给候选池设一个上限(比如100个),超过上限的时候触发"强制分类"流程——先让LLM把候选池按大类分组,然后优先问类别问题,而不是属性问题。这个策略能把候选池快速收敛到可控范围。
5.3 坑三:用户不按套路出牌
猜谜游戏里,用户经常会给出超出预期格式的回答。比如你问"是不是用来做饭的",他回答"算是吧,但也不完全是"。这种回答用简单的关键词匹配根本处理不了。
修复方案:所有用户输入都先经过LLM做一次"意图归一化",把自由文本转成"是/否/不确定/新线索"这四种标准格式之一。这个归一化步骤虽然增加了一次LLM调用,但让后续的逻辑处理简单了很多。
5.4 一个提升体验的小技巧:给Agent加"内心独白"
在调试阶段,我发现Agent的决策过程对用户是完全黑盒的,用户不知道它为什么问这个问题。后来我在每轮回复里加了一句简短的"内心独白",比如"根据你说的'可以加热',我排除了大部分家具,现在重点猜厨房用品"。
这句话不参与实际推理,只是让用户知道Agent的思考方向。加上之后,用户的参与感和耐心明显提升,即使Agent猜得慢一点,用户也愿意继续配合。
6. 从猜谜Agent到通用交互Agent的迁移思路
这个项目做完之后,我最大的感受是:猜谜Agent的很多设计思路可以直接迁移到其他交互场景。
比如智能客服的故障排查,本质上就是"用户描述现象→Agent问诊断问题→逐步缩小故障范围→给出解决方案",和猜谜的逻辑几乎一模一样。区别只是候选池从"答案"变成了"故障原因",属性标签从"能不能加热"变成了"有没有报错代码"。
教育领域的引导式教学也是类似。学生说"这道题不会",Agent不是直接给答案,而是通过一系列问题引导学生自己找到解题思路。这里的"候选池"就是"可能的知识点缺口","问题"就是"诊断性提问"。
甚至医疗问诊的初步分诊也能套用这个框架:患者描述症状→Agent问关键问题→缩小可能的科室范围→建议挂哪个科。当然医疗场景对准确性要求高得多,需要更严格的知识库和更保守的策略,但底层逻辑是相通的。
我在实际迁移的时候,主要改动的部分是知识库的结构和问题生成的Prompt模板,核心的状态管理和决策逻辑基本可以复用。这也是我一开始选择"裸调API+自建状态机"而不是用重框架的原因——逻辑清晰、耦合度低,迁移成本就低。
最后分享一个我在这个项目里反复验证过的经验:Agent的智能程度,不取决于你用了多先进的模型,而取决于你把问题拆解得有多清楚。猜谜这个任务,如果直接扔给LLM说"你来猜",效果很一般;但如果你把"线索抽取""候选池维护""问题评分""决策切换"这些子任务拆开,每个子任务用最合适的方式(LLM或代码)去解决,整体效果会好一个档次。这个思路,我觉得比任何具体的框架或工具都重要。