直接从实战场景聊起吧。我前阵子接了一个内部智能体项目任务听起来很简单给用户提交的内容做一次准确度评估。一开始我直接让大模型“自由发挥”输出一段话来说明它怎么判断。结果很快就被打脸了——我拿到了几十种不同风格的“判断文本”有的写一大段有的只写一个词有的还把结论藏在第二段第三行。为了把这个自由文本消化进下游系统我不得不写一堆脆弱的解析逻辑正则、关键词匹配、JSON提取全上还是防不住漏判和误判。后来我彻底换了个思路不让它生成文本只让它做判断。所有输出收敛到严格定义好的结构里用类型安全的方式把事情办扎实。这篇文章就记录我这次“TypeSafe Agent Skill”接入的全过程包括为什么这么选、怎么做、中间踩了哪些坑希望对正在折腾Agent落地的你有参考价值。我花了大概两天半重构这个能力核心改动只有一件事把“让模型写作文”改成“让模型做选择题填结构化字段”。就这么一个简单的转换后续下游的解析代码从一百多行缩到了不到十行线上误判率也好了不止一个量级。这个经验让我觉得特别值得展开说说——因为它不是某个项目的偏方而是所有想用LLM做判断、分类、审核、风险评估的人都绕不开的设计问题。1. 项目思路拆解判断型任务为什么不能用“文本生成”思路来做首先要明白一个前提大语言模型本质上是文本生成器它按概率预测下一个token。但你跟它说“请判断这段内容是否合规”如果你不约束输出格式它给出的“判断”本质上仍然是一段文本。文本就有无限种合法的写法这对程序来说是个灾难。1.1 需求定位我要的不是“一段话”而是一个可执行的结论在项目最开始需求方说得很含糊“让AI帮忙判断一下提交内容的有效性。”第一个版本我照着字面意思做让模型输出判断结果文本类似“这是一段可用的内容因为它的格式正确不过有些细节还需要补充。”表面上看这件事完成了但仔细想就会发现下游根本没法直接用这句话。我需要的是有效还是无效若无效属于哪一类置信度多少。这些是程序可以消费的数据点而不是给人类看的一整句评语。做工程的人应该都知道一句话——接口的返回值决定了一个系统的气质。如果你把LLM当一个“文本接口”来用那你整个下游都是在跟不可控字符串搏斗。而如果你把它当成一个“结构化决策接口”来用你就有机会获得和普通API一样稳定的返回。1.2 设计取舍用约束代替自由用类型代替文本我这次的核心设计原则可以归纳成三句话判断结果必须是枚举值不能是自由字符串。有效、无效、需人工复核这几种状态写死模型只能从中选一个。辅助字段必须结构化。比如不合规的原因分类也定义成枚举置信度用0到1的数值唯一允许自由文本的字段只有“备注”并且明确标注它不是程序逻辑依赖的一部分。校验层必须做两次。模型输出的JSON先按定义好的Schema解析解析完再对枚举字段做合法性检查两项都过才算一次成功的判断。这套思路执行下来效果是立竿见影的。最明显的变化是原来一周内我在日志里能翻出七种不同格式的“判断结果”重构之后所有返回值的结构完全一致日志直接可以拿去做统计展示几乎零成本。1.3 为什么选择“Skill”的封装方式这个判断能力不是孤立存在的它要被多个上游场景调用。有的场景是内容审核有的场景是问卷答案质量评估有的场景是用户意图分类。我不希望每个场景都在自己的代码里写一段提示词、然后各自解析返回值这样迟早会失控。所以我把它封装成一个独立的Agent Skill。Skill在这套架构里不只是一个提示词模板它包含明确的输入Schema明确的输出Schema预设的系统提示词后处理与校验逻辑一段给Agent看的“技能说明”什么时候该调用它、传入什么参数、拿到什么结果。这样做的直接好处是调用方不用关心这个判断是怎么实现的他们只需要按照输入Schema传参然后从输出Schema里取字段。整个判断逻辑的升级、提示词调优、校验规则变更都可以收敛在这个Skill内部不会影响上游调用方。2. TypeSafe Skill 的核心设计从Schema到代码的双重保障TypeSafe这个词这几年在工程圈很热但在LLM应用里真正做到的不多。大多数项目所谓的结构化输出只是让模型返回一个JSON字符串然后json.loads()一下取不取得到字段全靠运气。TypeSafe意味着从运行时数据到代码类型是一一对应的今天我要展开说的就是这个层面的设计。2.1 输入Schema设计把问题的边界预先画清楚判断型Skill的输入不复杂核心字段如下from pydantic import BaseModel, Field from typing import Literal, Optional class JudgmentInput(BaseModel): content: str Field(..., description待判断的内容最长不超过2000字) scenario: Literal[content_review, answer_eval, intent_cls] Field( ..., description业务场景标识影响系统提示词的选用 ) require_reason: bool Field( True, description是否要求模型输出结构化判定理由 )你可能注意到我特意加了scenario这个字段。同一个Skill要支撑多个场景最简单粗暴的做法是把所有提示词塞在一起让模型自己理解但这样效果很差。更好的做法是显式声明当前是哪个场景然后Skill内部根据这个字段切换不同的提示词子模板。这背后的逻辑是给模型的信息越精准模型的判断越稳定。你给它一个明确的场景标签比让它从一段冗长的说明里自行推断任务背景要可靠得多。这就是输入侧类型安全的意义——它不只是给程序看的也是给模型看的。2.2 输出Schema设计把判断结果收敛成不可逃逸的结构输出Schema是整个重构的核心我最终定稿的版本是这样的class JudgmentOutput(BaseModel): verdict: Literal[valid, invalid, need_review] Field( ..., description最终判断结论三选一 ) invalid_type: Optional[Literal[spam, low_quality, off_topic, format_error, other]] Field( None, description仅当verdict为invalid时必须指定否则为null ) confidence: float Field( ..., ge0.0, le1.0, description模型对判断结论的置信度0到1之间 ) structured_reason: dict Field( ..., description结构化判定理由包含具体违规点或问题定位 ) brief_note: str Field( , description面向人工复核的简短备注程序侧不应依赖此字段 )我重点解释几个设计上的细节第一verdict必须是Literal类型。这是类型安全最直接的体现它从Schema层面限制了模型的“表达自由”。模型只能在这三个值里做选择不存在第四个选项。如果你让它返回自由字符串那么“invalid”“Invalid”“INVALID”“无效”都会出现程序侧就得写一堆归一化逻辑。第二invalid_type是有条件必填的。我用Optional修饰它但在业务规则上要求当verdictinvalid时这个字段必须被填充。虽然Pydantic的Schema本身表达不了条件约束但我之后会在校验层补一道程序级检查。这个“Schema 程序逻辑”的双重保障就是TypeSafe能在工程上落地的关键。第三structured_reason是一个dict而不是自由文本。很多人会问为什么判断理由不直接用字符串呢我一开始也这么写后来发现一个问题理由中蕴含的“违规点定位”对下游分析极有价值。比如内容审核场景里我希望知道到底是哪一段话触发了“低质”判断。如果给一段自由文本程序无法直接定位如果定义成结构化字段比如{offensive_terms: [词1, 词2], offending_snippet: 原句摘录}程序就能直接提取并做展示或统计。这也回答了一个核心问题TypeSafe并不等于不要自然语言而是把自然语言限制在它应该存在的位置。对于需要程序消费的字段用严格类型对于需要人看的字段用文本。泾渭分明。2.3 校验层实现宁可拒绝不可带病通过有了Schema之后还不能直接信任模型的输出。因为模型是通过文本方式输出JSON的就算你要求“只输出JSON”它也可能给出来一个结构合法但语义非法的结果。比如枚举值拼错了一个字母程序如果直接用就会出问题。所以我在校验层写了这么一段逻辑from pydantic import ValidationError def parse_judgment(raw_text: str) - JudgmentOutput: # 先暴力抽取JSON片段 try: json_str extract_json_fragment(raw_text) data json.loads(json_str) except Exception: raise ValueError(模型输出中未找到合法JSON片段) # 再走Pydantic校验类型不匹配直接抛异常 try: obj JudgmentOutput(**data) except ValidationError as e: raise ValueError(f模型输出Schema校验失败: {e}) # 最后补业务级条件校验 if obj.verdict invalid and obj.invalid_type is None: raise ValueError(verdict为invalid时必须指定invalid_type) if obj.verdict ! invalid and obj.invalid_type is not None: raise ValueError(verdict不为invalid时invalid_type必须为null) return obj两层校验缺一不可。第一层Pydantic解决“类型对不对”第二层程序逻辑解决“业务规则符不符合”。我踩过的一个真实案例是模型返回了{verdict: invalid, invalid_type: null}Pydantic校验是能过的因为invalid_type本来就是Optional字段但业务上这个数据是残缺的。没有第二层校验这条脏数据就会流到下游而且极难排查。3. 实操过程一个TypeSafe Agent Skill的完整接入实录理论说得再漂亮不落地都是空谈。下面记录我第一次完整接入这个Skill的实操流程包括每一步的具体操作、关键代码和当时踩到的问题。3.1 第一步注册Skill梳理它的调用契约接入Agent之前首先要做的不是写提示词而是把“调用契约”定义清楚。我使用的是自定义的Skill注册机制核心思路是给Agent一个函数级的抽象让Agent可以像调用普通工具一样调用这个判断能力。agent_skill.register( nametype_safe_judgment, description对给定内容做结构化判断返回类型安全的判定结论。适用于内容审核、答案评估、意图分类等只做判断不生成文本的场景。, input_schemaJudgmentInput, output_schemaJudgmentOutput, ) def type_safe_judgment(req: JudgmentInput) - JudgmentOutput: prompt build_judgment_prompt(req) raw_text llm_chat_completion(prompt, json_modeTrue) return parse_judgment(raw_text)这段代码最关键的在于对Agent侧暴露的是函数签名而不是自然语言模板。Agent在决定是否调用这个Skill时只看description和输入输出Schema。它不需要知道内部用了什么提示词也不需要知道返回结果是怎么解析出来的。它只需要知道传入JudgmentInput拿到JudgmentOutput。这种设计带来的工程收益非常直接单元测试可以mock整个Skill而不用调真实模型如果将来判断逻辑从“调大模型”换成“调规则模型混合”调用方无感知多个Agent可以安全地复用同一个Skill不担心各改各的提示词导致行为漂移。3.2 第二步设计系统提示词引导模型“选”而不是“写”TypeSafe Skill的提示词设计和传统的聊天式提示词有明显区别。传统方式会说“请判断这段内容是否有效”然后期望模型给出回答我的做法是告诉模型你的输出将被程序解析严禁输出与Schema无关的内容。实际使用的系统提示词结构大概是这样的你是结构化内容判断引擎。你的职责是根据输入内容和场景输出一个符合JSON Schema的判断结果。 你必须严格遵守以下规则 1. verdict字段只能取valid、invalid、need_review中的一个不允许出现其他值。 2. 当verdict为invalid时invalid_type必须取枚举值之一不得留空。 3. confidence必须是0到1之间的浮点数1.0表示完全确定0.0表示完全不确定。 4. structured_reason必须是JSON对象描述判断依据禁止使用自由文本段落。 5. 禁止在JSON之外输出任何解释、前言、后记。 6. 如果不确定请选择need_review不要强行给出valid或invalid。你会发现我把“如果不确定就选need_review”写进了提示词。这是非常重要的一步因为我观察到一个现象语言模型在被迫二选一时即使信息不足也会硬选一个。这在实际业务里是很危险的——本来该人工复核的内容被机器自动放行了。增加need_review这个中间态相当于给模型一个“认怂”的出口这恰恰是提升判断可靠性的关键设计。另外我还在提示词里做了一次对比性的示例引导。给模型看两个极端样例一个是“返回了自由文本程序无法解析”另一个是“返回了标准JSON程序秒级消费”。这种对比通常在五轮内就能让模型稳定输出正确格式。总之一句话提示词的作用不是“说服它写”而是“约束它选”。3.3 第三步接入Agent完成工具注册与路由Skill定义好了之后接入Agent这一步相对机械。我这里用的是比较通用的Agent工具注册机制——Agent有一个工具列表每个工具都有名字、描述、参数说明、返回值说明。大模型根据用户请求内容决定调用哪个工具然后把工具返回值作为生成回复的上下文。接入时有几个容易踩坑的地方坑一给Agent的Skill描述不能太“范”。我一开始写的描述是“对内容进行判断”Agent经常在该调用的时候不调用不该调用的时候乱调用。后来改成“当需要判定用户提交的内容是否有效、是否合规、属于哪一类意图时调用此Skill”精确了很多。这其实印证了一个经验Agent的工具调用决策质量直接取决于工具描述的准确性模糊的描述是智能体幻觉的主要来源之一。坑二输出Schema里的中文枚举值在Agent侧容易错乱。我早期版本直接用了中文枚举值比如有效、无效、需复核结果Agent在决定路由时偶尔会把中文值当作实际输出写进回复。后来我改成英文枚举值作为实际数据中文只作为description里的解释性文本。这样既保证了程序侧的类型安全也不影响模型对业务含义的理解。# Agent注册示例 agent.register_tool( nametype_safe_judgment, description当需要判定用户提交的内容是否有效、是否合规、属于哪一类意图时调用此Skill。, parametersJudgmentInput.model_json_schema(), returnsJudgmentOutput.model_json_schema(), )3.4 第四步完善日志埋点与可观测性这一点值得单独拿出来讲。TypeSafe Skill的接入带来最大的好处其实是可观测性大幅提升。因为返回值是结构化数据我可以在日志里直接把verdict、confidence、invalid_type作为字段输出。这样在监控面板上我可以实时看到三种结论的分布比例、平均置信度、各业务场景的误判率。我在日志埋点上做了这样一件事给每一次判断都生成一个唯一的judgment_id并把原始模型输出、解析后的结构化结果、校验是否通过、耗时、token消耗全部关联到这一个ID下。这样做的实际价值是——当你想要复盘某一次误判时你可以用judgment_id把所有现场数据捞出来而不是大海捞针地在日志里找“某一段疑似不正常的文本”。这在A/B测试提示词时尤其好用一比就知道新的提示词到底改没改善判断质量。4. 工程选型实录为什么是TypeSafe而不是其他方案做这类结构化判断市面上其实不止一种实现路径。我在动手之前认真评估了三种主流方案这里把对比过程完整写出来供你在选型时参考。4.1 方案A自由文本生成 程序解析初始方案这个方案是大多数人第一反应的做法让模型自由输出然后用提示词或程序从文本里解析出结论。优点实现成本最低不需要任何额外约束提示词写起来最简单模型不容易因为格式约束而“发挥失常”缺点解析逻辑极其脆弱一个标点符号变化都可能破坏解析逻辑结论类型不可枚举下游统计困难模型输出风格变化会导致长期的维护噩梦我在项目一开始就是这套方案实测下来的感受是demo演示没问题一上生产就各种打补丁。你会陷入“模型换了个措辞 - 解析逻辑漏判 - 用户在日志里发现异常 - 你加班修解析逻辑”的循环。最致命的是你永远无法保证这周修完下周不会再出同类问题。4.2 方案B提示词强制JSON输出 基础解析这套方案往前走了一步要求模型输出JSON然后程序用json.loads()解析。优点比纯自由文本可解析性大幅提升实现成本依然较低只多了一句“以JSON格式输出”缺点没有Schema约束程序端拿到了JSON也不清楚字段类型是否可靠模型可能输出多余字段、漏掉必需字段、把枚举值写成自由文本一旦上游改了JSON结构下游解析代码必须同步改容易出错事实上这个方案只解决了“可解析”的问题没解决“可靠”的问题。它和生产级的TypeSafe之间隔着一条鸿沟。4.3 方案CTypeSafe结构化输出 Pydantic校验最终方案这是我现在最终落地的方案核心思路是用Schema约束模型输出边界用双重校验确保数据质量。优点输出类型与程序代码一一对应编译器能帮你提前发现错误枚举字段杜绝了“同义不同值”的脏数据问题下游代码只依赖结构不依赖任何文案措辞稳定性极高日志与监控天然结构化统计与复盘成本极低缺点需要多写一些Schema定义代码提示词设计需要更精细要引导模型严格遵循结构对模型指令遵循能力有一定要求部分较弱模型可能会在结构输出上出状况最终选择这个方案的核心理由其实只有一条它能把你从“无限可能性”里解放出来让你的系统重新变得可预期。工程最怕的东西就是不确定性LLM本身已经带来了足够多的不确定性如果连返回结构都不可控这个系统基本上无法交付给下游使用。4.4 三种方案对比速查表我整理了一张表方便你直接拿去参考对比维度自由文本生成强制JSON输出TypeSafe结构化输出开发成本极低低中解析稳定性差中高枚举值控制不可控半可控完全可控下游消费成本高中极低可观测性差中好依赖模型能力低中较高长期维护成本高中低如果你正在犹豫选哪种方案我给你的建议是先想清楚这个技能的输出值会不会被程序消费。如果会直接上TypeSafe不要走弯路。如果只是给一个临时脚本用、没有长期维护压力那方案B也未尝不可。但一旦涉及生产环境、多团队协作或长期迭代TypeSafe几乎是必选项。5. 实操中遇到的坑与排查心得接入过程当然不是一帆风顺的。我整理了自己实际踩过、也帮周围同行排查过的几类问题每个都附上了应对思路。5.1 模型“硬编”枚举值置信度永远满格这是我在压测时发现的第一个问题。某些业务场景下模型好像“学坏了”不管输入什么内容verdict都是validconfidence都是0.99。表面上看返回完全合规、类型完全安全但实际判断质量等于零。排查思路是这样的我先把日志里的confidence分布拉出来看发现大量聚集在0.99附近这明显不正常。接着抽样检查了valid的比例远高于人工标注的基线。最后定位到问题出在提示词示例上——我在示例中举的“正确”样例几乎都是valid的模型学到的不是“判断规则”而是“回答valid”。修正方法很直接重构了示例样本让三种结论在示例中均衡出现在提示词里显式加了“请勿无理由给出valid所有判断必须基于输入内容的实际证据”对confidence低于0.6但verdict为valid的案例在监控里标黄人工抽检这里面最值得说的是监控习惯。如果你不把confidence分布当做一个关键指标来盯这类“模型偷懒”问题可能隐藏很久直到业务方反馈误判漏判。有了结构化的日志这一步排查才可以做得如此快速。5.2 JSON片段紧贴文本直接json.loads()失败尽管提示词要求“只输出JSON”但模型偶尔还是会多带一点前后缀比如“根据判断结果”或者“json表示。这导致json.loads()直接抛异常。我排查时的第一个策略不是改解析代码而是去看哪一种前后缀出现得最频繁。用一周日志统计下来发现95%的前缀都是固定的两三种模式。于是我先写了一个清洗函数把这些常见前后缀去掉再解析。这个方案的坏处是——如果模型升级或提示词调整后出现新的前后缀清洗函数就会失效。后来我换成了更稳健的方案不按“去掉前后缀”的思路而是按“提取第一个{到最后一个}”的暴力提取方案。这样无论前面写了什么废话只要最终输出里有一块合法的JSON对象就能提取出来解析。def extract_json_fragment(raw_text: str) - str: start raw_text.find({) end raw_text.rfind(}) if start -1 or end -1 or start end: raise ValueError(文本中不存在JSON对象) return raw_text[start:end1]这个方案的容忍度高得多配合Pydantic校验实际效果很稳定。如果你也遇到类似问题我建议一步到位直接上这种提取式方案不要在清洗逻辑上花太多时间。5.3 图文混杂输入导致判断质量骤降我们的场景里有些输入不纯是文本可能包含图片或特殊符号。模型对这些混杂内容的判断质量明显下降特别是当图片承载了关键信息时。因为我们的核心诉求是“类型安全的判断”所以做的第一个调整是在输入Schema里增加一个字段显式标注是否有非文本内容has_multimodal_content: bool Field( False, description输入内容是否包含图片等非文本元素 )当这个字段为True时提示词会切换到一个更保守的版本明确提示模型“如果你无法完整理解非文本内容请给出need_review结论不要强行判断”。实际运行下来这个策略大幅压低了“盲猜”造成的高置信度误判。这里也印证了一个大原则类型安全的输出必须以输入侧的充分显式化为前提。输入Schema里定义得越清楚模型行为就越可控。你不告诉模型“这是多模态内容”它就会默认按纯文本处理然后自信地给出一个错误判断。5.4 Skill返回正常但Agent最终回复里出现“幻觉值”这个问题比较隐蔽。Skill本身返回的JudgmentOutput完全合法但Agent在拿到Skill的返回值后会基于它生成给用户的最终回复。在这个过程中Agent偶尔会“记错”工具返回里的字段值比如把verdictinvalid说成valid或者自己编造一个invalid_type值。这个问题的根因不在Skill而在Agent的“自由生成”环节。它会把工具返回当成一份资料、然后再组织语言这个过程中它可能篡改事实。我的处理方式有两个层面第一层在Agent的系统提示词中加一条强约束“工具返回的结构化字段是客观事实最终回复必须严格引用禁止修改或编造任何字段值。”第二层在Agent和用户之间增加一个“事实校验层”。该层拿到Agent生成的最终回复后提取其中的关键结论与Skill返回的JudgmentOutput做一致性比对。哪怕只是简单的字符串匹配比如“Agent文本中包含‘有效’但JudgmentOutput.verdict为invalid”都能拦下一大批幻觉问题。这里想分享的心得是TypeSafe的价值不应该止步于Skill内部而应该延伸到Agent生成最终文本的环节。你只有把“判断结果”的权威数据源握在结构化字段里才有可能反查和约束Agent的自由文本输出。没有这个权威数据源你连介入的抓手都没有。6. 一点来自实战的优化经验接入稳定之后我又做了几轮针对性的优化。这些优化不是必须的但如果你想把判断质量再往上推一档可以照着试试。6.1 置信度分桶策略我观察到原始置信度虽然是连续的但模型的置信度并非严格校准的——有时0.7和0.9之间没有本质区别。因此我对置信度做了分桶处理在输出Schema里同时保留confidence原始值和一个衍生字段confidence_level: Literal[low, medium, high] Field( ..., description置信度分桶low0.60.6medium0.85high0.85 )这个分桶字段不是模型直接生成的而是在后处理阶段基于confidence计算出来并写入最终返回。这样下游业务就可以用confidence_level来做粗粒度的策略分流而不用自己定阈值。同时有效降低了业务侧对“连续数值”的不当依赖——很多业务方拿到0.72这个数容易误以为是经过校准的概率实际上它只是一个排序参考。6.2 一轮离线评测集固化为了保证每次调整提示词后判断质量不滑坡我花了一个下午整理了二百条带人工标注评测集覆盖正常内容、低质内容、垃圾内容、偏题内容、边界模糊内容等类型。评测脚本的逻辑很简单跑完一批测试统计准确率、召回率、以及“关键误判率”把invalid判成valid。每次改动提示词或Schema之后先在评测集上跑一遍数值达标才允许合入主线。这一步的价值怎么说都不为过——你如果不把判断质量量化那么“优化提示词”就成了一场玄学博弈。有了评测集每次改动是变好还是变坏一目了然。这也是TypeSafe Skill能持续迭代而不退化的根基。6.3 小样本批量测试技巧开发调试过程中最烦的就是跑一次调一次大模型接口又慢又费token。我后来做了一个小技巧在本地调试时用“离线JSON文件”模拟模型返回再走一遍完整的提示词构建、JSON提取、Schema校验、业务校验链路。这样大部分代码逻辑问题都可以在白盒情况下快速定位只有真正需要验证提示词效果时才调用大模型接口。这会显著压缩调试周期。我原先改一次提示词要跑三分钟真实接口现在迭代校验逻辑一分钟能跑十几个用例。调试效率上了一个大台阶强烈推荐给同样在封装Agent Skill的开发者。最后说两句心里话这次TypeSafe Agent Skill的接入让我对“LLM应用工程化”这件事有了更具体的理解。很多人觉得大模型应用就是提示词工程写几句漂亮的prompt就完事了。但在真实工程环境里你真正需要的是把LLM的输出关进一个类型安全的笼子里——让该确定的东西100%确定让该灵活的东西只在指定区域灵活。至少对我来说这个思路帮我把整个项目的返工成本降到最低也让“AI判断”这件事从一个玄学问题变成了一个工程问题。万物皆可结构化哪怕本质是文本生成器的大模型只要你用对了约束方式它也能成为一个稳定的结构化决策引擎。这个内容后续还可以往两个方向扩展一是把“判断理由”从结构化字段进一步升级为一个独立的可校验证据链让每一次判断都有据可查二是把置信度分桶与人工复核系统做联动实现真正的“低置信度人工兜底”。如果你也在做类似的Agent能力封装希望这份实录能帮你少走一些我走过的弯路。