带过一个面向多语言用户的AI Agent项目最让我头疼的不是多轮对话也不是工具调用而是幽默。用户抛来一句“你这回答比我家Wi-Fi还快就是信号不太好”Agent接不住这个梗冷冰冰回了一句“感谢您的反馈我们会继续优化网络速度”隔着屏幕都是尴尬。为了根治这个问题我们启动了一套多语言幽默理解与生成系统的研发目标很直接让Agent能识别“用户在开玩笑”能判断当前适不适合接梗也知道怎么回一个至少不冷场的笑话。这篇文章我把从立项到上线的完整过程梳理一遍包括语料怎么建、理解模型怎么搭、生成模块怎么迭代、Agent怎么集成以及评测阶段踩过的坑。适合正在做对话AI、Agent产品或者对幽默计算、情感计算感兴趣的工程师参考。我会尽量写实操层面的东西不堆理论。1. 为什么幽默是自然语言理解里的“地狱难度”先拆问题再动工1.1 幽默到底难在哪语言层、语用层、文化层的三层拆解幽默让人工智障翻车大多数时候可以归结为一个底层机制预期违背。听众先建立某个心理预期然后在关键位置被突然推翻这个落差带来笑意。AI要做的是双份工作——既要知道“正常的语义走向”是什么又要知道“哪里出现了反常”。拆开来看幽默至少落在四个层面。第一是语言层比如谐音、双关、字形变化。中文里“压力山大”被谐音成“鸭梨山大”就是典型的音形碰撞英文里“Time flies like an arrow; fruit flies like a banana”同时用到fly和like的多义性这种幽默直接长在语言结构上脱离语言就不存在。第二是语义层常见手法是类别切换和歧义利用一个词在句中被强行换到另一个语义域。第三是语用层包括讽刺、反语、自嘲、调侃这一层最难因为字面和真实意图经常完全相反。第四是文化层某个段子好笑是因为听众共享一段经历、一个网络事件、一种生活场景换一群人就成了天书。这也是为什么“你为什么不笑”对模型来说是个非常难以回答的问题。它不理解“这是什么梗”更不理解“为什么这里是梗”。所以我们一开始就定了个原则幽默理解不等于幽默检测。光判断“文本是不是笑话”远远不够Agent还需要定位笑点识别幽默类型最后还要判断“在这个对话场景里该不该接这个梗”。这是一个面向行动的完整链路不是一个分类器能对付的。1.2 多语言让难度翻倍但解题框架可以收敛刚启动的时候团队差点掉进“做一个通用幽默引擎”的大坑里。通用幽默生成目前还属于研究题不解决工程问题。我们很快把范围收敛成了一个更可执行的框架三个能力加一个库。三个能力分别是检测当前文本是否含幽默识别幽默类型并定位笑点根据对话语境决定是否接梗、用什么风格接。一个库是多语言文化常识与流行梗知识库。为什么不用“让模型自己学会一切”原因有二。第一幽默语料在整个NLP生态里本来就稀缺多语言更是少得可怜纯靠端到端模型从碎数据里学出稳定的幽默感不现实。第二产品要的是“得体”而不是“爆笑”。一个Agent每天面对大量严肃请求幽默只是辅助调节气氛。优先保证不翻车比偶尔惊艳重要得多。这个定位决定了后面所有技术选型。2. 语料与标注这项工程80%的精力都耗在这里2.1 多语言幽默数据从哪来什么样的数据能用数据是整个项目最先卡脖子的地方。通用NLP任务有大把开源语料但“幽默”是个窄题材公开资源零散且质量参差。我们最终采取了“多源混合”的路子。中文主要来自网络笑话合集的公开页面、相声片段转写、社交平台上带幽默标签的公开文本以及谐音梗评论区按热度做的筛选英文来自公开的joke文本集、单口喜剧转写素材、社交媒体的pun标签内容日文来自漫才转写、网上的駄洒落合集、讨论串里典型“装傻吐槽”结构的对话。韩文、泰语等更大依赖字幕组和综艺节目的公开转写资源。这些数据不能直接进训练集必须过两关。第一关是可搬运性优先用开放许可或我们自行转写的资源避免版权隐患。第二关是安全合规这一步要严格做去掉攻击性玩笑、地域歧视、人身攻击类内容。我们建了一个负向过滤清单规则先粗筛一遍再用模型做一遍语义过滤。注意这个清洗不能省幽默生成领域最容易踩的就是价值观红线宁可让语料库小一点也不能留隐患。最后的量级是中文11万条英文8万条日文5万条泰语、阿拉伯语这类低资源语言各1万条左右。后续还做了数据增强基于已有真笑话让模型做“同构改写”——保留情节结构和笑点机制替换人物、场景、物品扩展出大量变体。中英文在这种增强下收益很明显低资源语言因为基础量太小增强后依然不够用。2.2 标注规范怎么做笑点位置为什么值得单独标有了原始文本下一步是标注。我们设计了多维标注格式每条数据包含五个字段is_humor二分类是否幽默humor_type多标签文字游戏/双关、情境幽默、讽刺反语、荒诞、自嘲、文化梗punchline_start / punchline_end笑点起止位置culture_dependence0到2代表完全无需背景、需要轻度文化背景、高度依赖特定文化和语言结构offensiveness0到5超过2分的样本不进入训练集这里最想强调的是笑点定位字段。一开始我也觉得标个“是否幽默”就够了但做到后面发现笑点位置的价值远超预期。它不仅给理解模型提供了可解释性还直接服务生成模块——生成时可以复用“先铺垫后反转”的结构而不是凭空瞎编。标注一致性的情况可以给大家做个参考。3名标注员独立标注二分类的Kappa约0.78质量不错幽默类型多标签的Kappa约0.62勉强可用但笑点位置刚开始只有0.5左右非常主观。后来我们改了方案让标注员先选句子再选词并且允许标“没有清晰笑点”这个选项Kappa才升到0.6附近。这算是一条非常实用的经验笑点边界标注要允许“无解”别硬逼标注员给每个句子找笑点。2.3 跨语言对齐直译是陷阱复述才是正路多语言幽默系统最折腾人的是怎么把不同语言的幽默语料“对齐”。开始时我们找“平行幽默语料”——同一笑话的多语言版本找了一圈能用的只有几十条而且绝大多数是传统英语笑话的机械翻译直译后笑点完全消失。后来换了个思路用跨语言复述来构建对齐。具体做法是让精通双语的人看一条中文幽默不要求直译而是“用英文重新讲一个同类型、能让英文用户笑出来的段子”。这样建出来的不是句子级平行语料而是“幽默类型对齐”语料。它保留了笑点机制在不同语言里的等价映射这对训练跨语言理解模型很有帮助。为什么机器翻译在这里行不通因为双关的笑点长在原文的音形层上。中文谐音梗“我今天有点‘鸭’力山大”直译成英文只能得到“I feel duck pressure today”英文母语者完全get不到。翻译能转述语义但转述不了发音碰撞和拼写游戏。想靠通用翻译搞定幽默对齐等于让翻译器去翻译一个它根本看不见的维度。在表示层面我们还用多语言编码模型做了软对齐把相同幽默类型、相似笑点机制的样本在向量空间里拉近作为训练数据的一部分。这块不用刻意追求精确映射能让模型对不同语言的“同类幽默”产生共享表达就够了。3. 幽默理解模块不仅要判断“好不好笑”还要定位“哪里好笑”3.1 多任务共享编码器检测、分类、定位三个头各司其职理解模块的整体架构是基于多语言预训练编码器在上面挂三个任务头幽默检测头做二分类类型识别头做多标签分类笑点定位头对每个token做二分类。三个任务共享同一个编码器这是刻意设计的因为检测需要知道“哪里有笑点”定位需要知道“整体是不是幽默”类型判断又依赖两者的中间信息。共享编码器让三个任务互相提供归纳偏置比各自训练三个模型稳定得多。训练时我们用多任务加权损失检测、类型、定位的权重大概是1.0比0.7比0.8。梯度直接累加在共享层上。实际训练下来检测和类型任务收敛正常笑点定位任务稍慢但加上token级监督后整个编码器对幽默关键位置的注意力明显更集中。这里的另一个细节是上下文窗口。幽默极依赖对话前文所以我们处理对话时会把Agent自己的上一轮回复、用户的上一轮输入和当前文本拼在一起。缺少上下文时讽刺的误判率会明显上升。尤其是“反语”单看一句话几乎无法判断但结合前文往往一目了然。部署时我们把这个多任务模型蒸馏成小模型分语言部署在边缘环境单次推理控制在100毫秒以下。这是为了给后面的生成模块留出时间预算。3.2 “预期违背”信号把笑点当作信息熵增来做辅助监督只靠分类模型能判断“是不是笑话”但解释不了“为什么这里好笑”。为了让模型学到“笑点位置”这个关键结构我们加了一个辅助信号surprise score也就是每个token在上下文里的“意外程度”。具体做法是用一个掩码语言模型把序列中第i个token遮住基于左右上下文预测它的概率取对数概率作为该位置的惊讶值。实际出现的词如果概率很低说明模型对这里有预期违背。再对整条序列做z-score归一化超过1.5个标准差的token就标记为“候选笑点”。举个例子英文双关句“I used to be a baker, but I couldnt make enough dough.”——dough这个词同时承担“面团”和“钱”两层意思模型看到前半句时大概率预期的是“面团”的延续但后半句实际指向“钱”这个位置就会冒出高surprise值。我们把这个信号作为额外的token级特征输入到笑点定位头F1提升很明显。注意surprise只对语言层和某些语义层的幽默效果好对“情境型幽默”几乎不起作用。情境幽默靠的是外部常识反转比如“老板说周六要加班我说不行因为那天我的床需要我陪”单看语言本身没有什么意外。所以surprise只能当辅助特征不能替代完整的语义建模。3.3 讽刺与双关两类最难啃的特殊幽默怎么单独处理讽刺在多语言场景下是重灾区。字面情感和真实情感极性相反模型很容易被表面文本带跑。英文里我们识别积极/消极形容词计算其情感极性与语境预期极性之间的差异再把标点、表情符号这类副语言信号加进去。中文网络语境更特殊像“呵呵”“真是棒棒哒”这类反语标记得单独维护一张语用标记词表并且要在对话级模型里判断不能只看词典硬匹配。双关处理则要引入音形信息。英文可以用音标词典类资源枚举同音词和同形异义词作为候选中文用拼音库把同音字候选枚举出来计算候选词与上下文是否形成语义冲突。这些候选信息编码成token级特征一并送进encoder。实测对中文谐音梗的检测帮助最大F1能到0.76。这个模块的最终效果是英文幽默检测F1约0.83中文约0.79日文约0.72泰语约0.6。讽刺任务在中文上的精确率偏低因为反语太依赖对话语气这属于先天数据问题只能靠语境窗口和语用标记慢慢补。4. 幽默生成模块三代方案复盘从检索到受控生成4.1 初代方案的教训检索式为什么快但尴尬最早做生成时图省事直接上了检索式方案向量召回最相似的段子替换专名和实体当作Agent的回复。上线一测稳定性的确好安全也可控但两大问题立刻暴露。第一是重复率高。用户第一次听到“你这人真有意思像我家路由器总爱自动重启”还觉得新鲜第二次听到一模一样的逻辑就开始皱眉。第二是语境脱节。检索来的段子套不进用户刚说的具体内容经常出现“用户聊堵车Agent回一个关于猫的冷笑话”这种场面。检索式只能当冷启动方案或者低资源语言的兜底不能作为主力生成器。4.2 从端到端到受控生成为什么必须“先找笑点结构再包装语言”第二版我们试过直接用预训练语言模型端到端生成条件前缀里塞入幽默类型、主题、语言等控制信息。效果比检索式灵活但翻车记录也很刺激生成到一半结构断裂、把前文笑话复制后跑飞、偶尔冒出不合适内容而且在中文上生成英式幽默翻译腔的情况特别多。折腾几轮后我们把方案收敛为两阶段受控生成。第一阶段是结构规划器。它根据用户输入、对话场景、幽默目标和闲聊状态先输出一个小型JSON方案包含幽默类型比如文字游戏、荒诞、自嘲、主题词、转折词、是否需要双关候选以及目标语言和字数。这个阶段不直接生成句子只决定“用什么笑点结构”。第二阶段是文本填充器。它把规划器输出的JSON扩展成完整文本。这里有趣的约束来了——如果是英文pun生成器会被约束到音标相似性层必须使用同音词集合里的词如果是中文谐音梗生成器要优先从拼音库抽候选词强制包含到句子里。为什么把“结构”和“文本”拆开因为幽默的核心在结构而不是辞藻。一句“前半句建立预期后半句反转”的模板在任何语言里都说得通。先定结构再选语言外壳比让模型直接从零吐出一段笑话稳定得多也更容易控制类型不走偏。4.3 生成后的自省回路用理解模块拦住烂梗这一步是整套系统里我个人觉得最有价值的设计。生成器输出一段话后不直接回给用户而是先走一遍理解模块过两个关卡。第一关是幽默自检把生成文本丢进幽默检测和类型分类如果置信度低于阈值就重新生成。最多重试两次仍然不合格就放弃讲笑话退回普通回复。这套机制本质上是让Agent当自己的审稿人。第二关是安全过滤。开放生成模型经常在不该冒犯的地方冒犯所以我们上了“三色名单”机制完全禁止词直接拦高风险类别对群体的攻击性内容以及在情感敏感场景里的不当玩笑触发后降级为无害回复轻微擦边的样本进入人工抽查。实际效果举例用户说“我的猫把我的键盘当床了”系统判定适合轻松接梗规划器选择了“荒谬反转拟声双关”结构生成的中文回复是“它可能是在你打字的时候伸爪帮你‘加班’”英文版本则生成“Your cat is just adding ‘paw-sitive’ productivity to your workflow.”。这类段子不算爆笑但在对话里不冷场、不冒犯对Agent来说就达标了。还要强调一点接梗频率必须控制。同一个对话里连续两轮都抛梗用户会觉得AI在抖机灵。我们在Agent记忆里写了“本对话已接梗次数”超过一次就强制走严肃回复分支。5. Agent集成与产品化幽默不幽默要看场景脸色5.1 理解-路由-生成-过滤Agent内部的完整决策链路当幽默模块被塞进真实Agent后不能简单地“检测到笑话就回复笑话”。我们最终设计了一条四段式链路用户输入先进理解模块做幽默检测、类型识别、笑点定位然后进入意图路由层判断当前对话到底适合哪种行为。路由输出四档跟随用户玩笑继续调侃、适度接梗、不接梗但委婉回应、直接严肃回答。只有前两档才会调用生成模块生成完再过安全过滤最终输出给用户。为什么要单独设计“是否该接梗”这一层因为用户可能在宣泄情绪时说反话。比如“我辛辛苦苦干了一年老板今天告诉我项目被砍了我可真开心啊”——这里绝不能当幽默处理。我们在理解模块里加了一个“负面情绪讽刺”联合判别子任务一旦判定用户处于悲伤或愤怒状态幽默生成模块整体关闭。安全设计必须前置不能指望生成模型临场判断。工程实现上理解模块和生成模块是完全解耦的。理解模块蒸馏成小模型分语言部署保证低延迟生成模块集中调用大模型能力做异步处理。为了让等待不那么煎熬回复采用流式输出用户能看到打字过程也在客观上给了生成模块更多的计算时间。5.2 低资源语言的保底方案翻译桥接与检索兜底对于泰语、阿拉伯语这类数据量捉襟见肘的语言我们用了和主流语言不同的策略。第一步是翻译桥接用户输入先翻译成英文在英文侧做幽默检测和类型识别。第二步就讲究了检测到幽默后不能直接把英文笑话翻译回目标语言那样笑点早就碎光了。我们采用“检索文化改写”的保险方案英文侧检索出最接近的段子翻译回目标语言再做本地化改写——把文化实体替换成本地人熟悉的常见对象。比如面向泰语用户笑话里的“隔壁客户”会改写成“堵车高峰期卖果汁的小贩”这类本地生活场景。做这个需要为目标语言维护一个小型本地化实体清单工作量不大但效果立竿见影。最终这类语言的接梗成功率不如主流语言但翻车率显著降低。这里的产品取舍很明确宁可让低资源语言的Agent偶尔显得“没听懂你的笑话”也不要回一个翻译腔满天飞的句子那种尴尬比不接梗更伤人。5.3 监控指标怎么定接梗成功率比“好不好笑”更贴近业务上线后监控什么一开始团队吵过一轮。有人说要盯生成文本的趣味分有人说要盯自定义冒犯指数。最后我们拍板业务核心指标是接梗成功率用户说了一句带幽默的话系统是否判定为幽默并给出一个相关且恰当的回应通过线上抽样人工评估。中文和英文最终能做到约75%低资源语言大约50%。这个数字直接反映了理解模块的召回和生成模块的贴合度。另一个重要指标是用户继续对话率Agent接梗后用户愿意接着聊的比例。对比普通严肃回复接梗后继续对话率高出6个百分点说明幽默确实在和用户拉近距离。负面反馈率也要重点盯。幽默最容易引发用户不适初期负面反馈一度到8%后面通过更保守的路由策略和安全过滤收敛到了3%左右。我们每天还会人工复盘所有被安全过滤器拦截的样本避免过滤过度误伤正常对话。我还总结了一个经验公式Agent适合接梗的概率 检测置信度 × 场景适配度 × 风格匹配度。三者相乘低于某个阈值就放弃接梗。这个公式在实际调参中非常好用比单纯调模型阈值直观得多。6. 评测体系与实际效果BLEU在这个项目里几乎失效6.1 为什么常规文本指标在幽默任务上不可信做这套系统之前我们走了不少弯路其中最大的弯路就是试图用常规文本生成指标评估幽默质量。BLEU本质上是n-gram重叠度它在“参考答案只有一种表达方式”的任务上合理但幽默的天性就是求新求异。同样的笑点参考答案是“你可真是漏网之鱼”模型说“你从网里跑了属鱼的吧”意思相同n-gram重叠极低BLEU会给出一个很低的分数。但在真实对话里这两个回答的幽默效果可能不相上下。按BLEU调参只会逼着模型去背参考答案反而杀死创意。困惑度这类指标同样不靠谱。语言模型眼里最“安全”的句子往往是无聊的套话困惑度降到最低的那一刻也是幽默感归零的那一刻。所以我们定了一个原则自动指标只用于回归测试防止模型升级后某类幽默能力断崖式下滑最终决策只看人工评测和线上真实反馈。6.2 多维度评测设计以及上线后的真实数字理解端的评测相对简单幽默检测用F1、精确率、召回率、AUC笑点定位用IOU需要预测区间和标注区间有重叠才算命中。上面提到的中文0.79、英文0.83、日文0.72、泰语0.6就是这个体系的数字。生成端就麻烦多了。我们建了一套五维人工评测趣味度1到5、自然度1到5、相关性1到5、文化适配度1到3、安全合规是否通过。每个样本至少两个人打分出现较大分歧就第三人仲裁。最终第三代系统的成绩是趣味度平均3.6自然度3.8相关性3.5文化适配度中英文约2.4到2.5日文2.2泰语1.9。和前两代对比更直观初代检索式的趣味度只有2.1第二代端到端生成稍微好一点到了2.9但安全违规率让人头疼到第三代受控生成加自省回路才拉到3.6。我们还在线上做了配对偏好测试同一输入一边是系统的幽默回复另一边是“安全但无趣”的普通回复用户盲选更喜欢哪个。第三代系统在英文和中文上胜率到了61%低资源语言只有46%。这个46%说明在低资源语言上幽默回复和普通回复已经拉不开差距了只能靠继续灌数据和优化本地化改写来补。评测过程还有一个老大难问题众包标注员之间的主观差异。不同人对“好笑”的感知差距极大刚开始Kappa只有0.55。后来我们要求标注员先做20条一致性测试不过关的不能参与正式标注整体一致性才升到0.6附近。幽默评测想完全客观是不可能的能做的就是尽量让标准对齐到群体偏好的中位数上。6.3 三个典型失败案例从翻车样本里捞出的经验案例一发生在日文漫才结构上。用户的输入是典型的“装傻加吐槽”双段结构系统没识别出第二段“从前面的装傻里猛踩刹车”这个反转直接把整段话当成了普通叙述没有触发幽默分支。根因是漫才语料太少标注时又把前文铺垫和吐槽点混在了一条样本里模型根本学不到跨句转折。修复方案是给标注规范增加“结构跨度”字段专门标出铺垫来自前文的哪一句训练时给这类样本更高权重。案例二是中文谐音梗翻译出口翻车。用户说“我今天有点‘鸭’力山大”系统规划器选的是中文双关方案但在英文对话里直接生成了按字面翻译的“duck pressure”式表达英文用户完全笑不出来。根因是中英双关根本不存在可映射关系。修复办法是放弃直译为每种语言单独维护自己的同音词与双关素材库英文就改用英文常见的overload、dough这类同音双关素材重新构造段子。案例三是反语误伤。一个用户抱怨天气说“这天气可太棒了出门五分钟回来就能洗个热水澡”系统当成讽刺幽默回了个冷笑话用户负面反馈。修复方式是把“可太棒了”这类高频反讽短语加入贬义语境检测词表并让路由层变得更保守——一旦负面情绪分偏高幽默分支直接关闭。这个案例后来成了我们安全策略的标杆宁可无趣不能冒犯。做完这个项目我最大的体会是幽默对Agent来说不是一个可以单独优化的“任务”而是一套系统性的行为约束。理解要先行安全要兜底生成只是最后那一下“临门一脚”。技术上最值得投入的其实是语料和评测体系而不是反复换更大的模型。模型再大也补不上标注规范里缺的那一维“笑点位置”。最后分享一个小技巧Agent要记录“本轮是否已经接过梗”。真实用户对连续抖机灵的容忍度比我们想象中低得多一个对话里接一次梗是幽默接三次就变成骚扰了。这个分寸感恰恰是幽默系统里最难用模型参数表达、却又最影响体验的东西。