ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CangjieMagic jsonable 包深入解析:声明式 JSON 序列化、类型模式与语义约束校验

CangjieMagic jsonable 包深入解析:声明式 JSON 序列化、类型模式与语义约束校验
  • 人工智能
  • 大模型
  • AI Agent
  • RAG
  • 工具调用
  • MCP 服务
  • MCP Clients
  • 多智能体

【免费下载链接】CangjieMagic

基于仓颉编程语言构建的 LLM Agent DSL,其主要特点包括:声明式 DSL、支持 MCP 协议,支持任务智能规划等。

项目地址:https://gitcode.com/Cangjie-TPC/CangjieMagic
点击查看免费下载

magic.jsonable是 CangjieMagic 中负责"数据与 JSON 双向转换、类型模式描述与语义约束校验"的基础包。它通过Jsonable<T>接口、TypeSchema类型模式体系和@jsonable宏,把任意class/struct/enum声明式地变成"可序列化、可反序列化、可校验、可被 LLM 结构化输出解析"的数据类型。读完本文,你将掌握该包全部公开 API 的语义、@jsonable/@field/@alias/@constraint宏的组合用法,以及它在 Agent 结构化输出(chatGet<T>)中的约束修复闭环原理。

一、包定位:LLM 结构化数据的"地基"

在 CangjieMagic 的架构里,magic.jsonable位于最底层,被多个上层模块依赖:

  • Agent 的chatGet<T>(abs_agent.cj)通过T.getTypeSchema()拿到类型模式,要求模型按此输出 JSON,再用T.fromJsonValue解析成强类型对象;
  • Agent Executor 会把outputSchema注入一个answer工具(opertors.cj),引导模型以指定结构作答;
  • 工具参数定义ToolParameter直接以TypeSchema作为参数类型模式(tool_parameter.cj);
  • @agent的 DSL 层在生成 AI 函数声明时,也用getTypeSchema()描述返回值(ai.cj)。

也就是说,只要理解jsonable,就理解了 CangjieMagic 中"如何让模型输出可信的结构化数据"的完整链路。

二、核心 API 全景:六大构件

文档定义的六个公开类型全部位于 src/jsonable/jsonable.cj 与 src/jsonable/type_schema.cj 中,先看一张全景:

类型作用
ToJsonValue将实现类型转换为JsonValue(序列化方向)
FromJsonValue<T>从JsonValue还原出T(反序列化方向)
Jsonable<T>同时继承上述两者,并补充类型模式与语义校验
ConstraintValidation校验结果:Pass或携带失败消息的Fail(String)
JsonableException序列化/反序列化/校验失败时抛出的异常
FieldSchema/TypeSchema描述字段与对象/数组/枚举的结构化类型模式

2.1 序列化接口 ToJsonValue

public interface ToJsonValue { func toJsonValue(): JsonValue }

任何实现了toJsonValue()的类型都可以把自己变成一个JsonValue(JsonString、JsonInt、JsonArray、JsonObject等,来自stdx.encoding.json)。后续可继续调用toJsonString()输出标准 JSON 文本。

2.2 反序列化接口 FromJsonValue

public interface FromJsonValue<T> { static func fromJsonValue(json: JsonValue): T }

注意它是静态方法:由 JSON 值构造一个T。实现方必须处理类型不匹配的情况——通常抛JsonableException。

2.3 综合接口 Jsonable

public interface Jsonable<T> <: FromJsonValue<T> & ToJsonValue { static func getTypeSchema(): TypeSchema func validate(): ConstraintValidation }

Jsonable<T>把三个能力合并在一起:

  • getTypeSchema():返回T的类型模式;
  • validate():对当前值执行语义约束校验,返回Pass或Fail(消息);
  • 继承的fromJsonValue/toJsonValue:完成双向转换。

文档特别强调了三点实现约定(在 jsonable.cj 中均有对应):

  1. @jsonable宏生成的类型会自动实现该接口;
  2. @constraint[...]声明的约束会在validate()中被求值;
  3. fromJsonValue在反序列化后必须调用validate(),校验失败要拒绝该值(抛异常)——这正是"LLM 输出不可信"问题在数据层的兜底。

另外源码中注释了一处平台限制(jsonable.cj):由于 cjc 0.53.x 存在 bug,Jsonable暂时无法提供fromJsonStr(String)便捷静态方法,反序列化需先JsonValue.fromStr(...)再走fromJsonValue。

2.4 校验结果 ConstraintValidation

public enum ConstraintValidation { | Pass | Fail(String) }

Pass表示校验通过;Fail携带一条人类可读的失败消息(例如Constraint violation in ConstrainedData.number: number > 0),该消息会原样出现在后续的修复提示语中。

2.5 异常类型 JsonableException

public class JsonableException <: Exception { public init(msg: String) { super(msg) } }

所有 JSON 转换失败统一抛此异常,例如:

  • Convert to Int error. Value: ...(类型不匹配)
  • Get JSON object error.(非对象)
  • Get JSON object field \score` error`(必填字段缺失)
  • Constraint violation in ...(约束校验失败)

三、FieldSchema 与 TypeSchema:类型模式的完整描述

3.1 struct FieldSchema

public struct FieldSchema { public FieldSchema ( public let name: String, public let description: String, public let typeSchema: TypeSchema, public let required!: Bool = true, public let constraints!: Array<String> = [] ) { } }

一个字段包含五个要素(type_schema.cj):

  • name:JSON 中的字段名(可能不同于仓颉成员名,如@alias之后);
  • description:人类可读的字段说明(由@field注入),会写入 schema 供 LLM 参考;
  • typeSchema:字段值的类型模式;
  • required:是否必填,默认 true;Option<T>字段会自动置为 false;
  • constraints:挂在字段上的语义约束表达式字符串数组(由@constraint注入)。

3.2 enum TypeSchema

public enum TypeSchema <: ToJsonValue & ToString { | Str | Int | Float | Boolean | Arr(TypeSchema) | Enum(Array<String>) | Obj(Array<FieldSchema>) }

七种模式完整覆盖了 JSON 的基本类型(type_schema.cj):

变体含义序列化结果
Str字符串{"type": "string"}
Int整数{"type": "integer"}
Float浮点数{"type": "number"}
Boolean布尔{"type": "boolean"}
Arr(TypeSchema)数组,元素为指定模式{"type": "array", "items": ...}
Enum(Array<String>)枚举,限定可选字符串{"type": "string", "enum": [...]}
Obj(Array<FieldSchema>)对象,含字段清单{"type": "object", "properties": ..., "required": [...]}

3.3 关键实现:toJsonValue 生成 JSON Schema

TypeSchema的toJsonValue()实现(type_schema.cj)将自身递归转换为近似 JSON Schema 的结构,这正是喂给 LLM 的"输出格式说明书":

  • 对象模式会为每个字段生成properties条目,并合并description与constraints两个扩展键(当它们非空时);
  • 只有必填字段(field.required == true)进入required数组;
  • 若对象没有字段(如TypeSchema.Obj([])),则不输出properties/required键。

toString()直接复用toJsonValue().toJsonString(),因此TypeSchema也可以被当作字符串直接拼进提示语。

四、@jsonable 宏:从声明到实现的代码变换

@jsonable是 CangjieMagic DSL 层(宏包magic.dsl)提供的声明式入口(jsonable.cj),支持class、struct和穷举enum(jsonable.cj),还支持dump属性用于打印生成代码调试:

@jsonable[dump: true] class Foo { ... }

4.1 宏生成了什么

transformJsonableType(jsonable.cj)把:

@jsonable class F { let a: String let b: Int64 }

变换为等价于:

class F <: Jsonable<F> { let a: String let b: Int64 public init(...) { ... } // 全字段构造器 static func getTypeSchema(): TypeSchema { ... } // 收集 FieldSchema,返回 TypeSchema.Obj static func fromJsonValue(json: JsonValue): F { ... } // 逐字段反序列化 + validate func toJsonValue(): JsonValue { ... } // 逐字段序列化 func validate(): ConstraintValidation { ... } // 逐约束求值 }

各生成方法的实现要点:

  • 构造器(L215-L246):为每个成员变量生成带!的命名参数;有默认初始化表达式的成员参数带默认值,可选参数排在最后。
  • getTypeSchema(L248-L285):为每个字段构造FieldSchema,其中required = !isOption(fieldType),并把@field描述、@alias别名、@constraint约束字符串都填入。
  • fromJsonValue(L287-L355):按三种情况取字段值——有默认值则缺失时用默认值;Option<T>缺失时用JsonNull();其余必填字段缺失时抛JsonableException(消息带字段的 JSON 名,别名也会出现在消息里)。构造完成后立刻value.validate(),失败则抛JsonableException。
  • toJsonValue(L357-L393):Option<T>字段若序列化为JsonNull则跳过不输出;其余字段全部写入。
  • validate(L395-L414):按字段声明顺序、字段内约束声明顺序逐个求值,一旦某个约束为假立即返回Fail,消息格式为Constraint violation in <类型>.<字段>: <约束表达式>——所以"返回第一个失败的约束"是有意设计(测试 testConstraintValidateReturnsFirstFailureInDeclarationOrder 专门验证了这一点)。

4.2 枚举的自动支持

@jsonable也支持穷举枚举(L416-L507):

  • getTypeSchema()返回TypeSchema.Enum([ctorName, ...]);
  • fromJsonValue要求输入必须是 JSON 字符串且匹配某个构造器名,否则抛异常;
  • toJsonValue把枚举值映射回其名字符串;
  • 带类型参数的构造器与 ellipsis 枚举会被宏拒绝(抛DslException)。

五、配套宏:@field、@alias、@constraint

三个字段级宏都通过assertParentContext("jsonable")强制只能用在@jsonable类型内部。

5.1 @field:给字段写说明

field.cj 接受一个字符串字面量作为字段的人类可读描述,会进入FieldSchema.description,最终写入 schema 供 LLM 理解字段含义(支持多段字符串,拼接时以换行连接):

@jsonable class MyDateTime { @field["Year of the foundation"] let year: Int64 }

5.2 @alias:JSON 字段别名

alias.cj 把仓颉成员名映射为 JSON 中实际使用的键名。别名同时作用于序列化、反序列化和 schema 输出:

@jsonable class AliasedOptionalData { @alias["user_name"] let name: String }

测试 testAliasAndOptionalRoundTrip 证实:序列化结果含user_name而不含name;反序列化时也按user_name取键;schema 中required列表输出的是别名。

5.3 @constraint:语义约束

constraint.cj 接收一个布尔表达式,支持任意合法的仓颉表达式,包括字段间引用(this指向当前对象):

@jsonable class ConstrainedData { @constraint[number > 0] @constraint[number < limit] let number: Int64 let limit: Int64 @constraint[name.size < 10] @constraint[this.alias.size < this.name.size] let name: String let alias: String }

同一字段可以叠加多条约束,validate()按声明顺序求值并返回第一个失败项。测试 testConstraintValidate 验证了合法值返回Pass、非法值返回Fail("Constraint violation in ConstrainedData.number: number > 0");testConstraintSchema 还验证了约束表达式会原样写入 schema 的constraints键,供 LLM 在生成时提前规避。

六、内置类型适配:开箱即用的 Jsonable

magic.jsonable为常用类型提供了现成的Jsonable扩展(jsonable.cj),无需任何额外声明:

类型类型模式反序列化规则备注
StringStrJSON 字符串;null视为""L52-L70
Int64IntJSON 整数;null视为0L72-L90
Float64FloatJSON 浮点或整数;null视为0.0L92-L110
BoolBooleanJSON 布尔;null视为falseL112-L130
Array<T>Arr(T的schema)JSON 数组,逐元素转换L132-L159
ArrayList<T>Arr(T的schema)同上L163-L193
Option<T>与T相同JSONnull→None,否则Some(T)L197-L226
HashMap<String, T>无(抛异常)JSON 对象,键值转换L228-L261
JsonObjectObj([])直接透传L263-L279

几个值得注意的设计:

  1. Option<T>的类型模式就是T本身,因为"可选"是通过required=false表达的(见 3.1),而不是引入新的模式变体;
  2. HashMap<String, T>拒绝提供 schema(getTypeSchema()直接throw JsonableException("HashMap has no type schema")),因为它字段不确定、无法用固定 schema 描述——但它仍可序列化/反序列化为 JSON 对象;
  3. 所有内置类型的validate()都返回Pass,语义约束只对@jsonable声明的类型生效。

七、实战:chatGet 与约束修复闭环

jsonable 在 Agent 中最典型的应用是chatGet<T>(abs_agent.cj):

public func chatGet<T>(request: AgentRequest): Option<T> where T <: Jsonable<T> { request.outputSchema = T.getTypeSchema() // 1. 注入类型模式 ... }

调用流程如下:

  1. 将T.getTypeSchema()设为request.outputSchema,Executor 据此注入answer工具、要求模型按 schema 输出;
  2. 模型返回文本后,用ParserUtils.extractFirstValue<T>解析成T;
  3. 若解析失败(ParserException),把Output does not follow the type schema: ${schema}追加进对话并让模型重试修复;
  4. 若fromJsonValue内部约束校验失败(抛JsonableException),把Output violates semantic constraints. Validation error: ${ex.message}追加进对话重试修复;
  5. 重试次数受Config.outputRepairRetryNumber控制,默认 3(config.cj);耗尽后chatGet返回None,而不是抛出异常。

仓库中的 jsonable_constraint_verifier 示例完整演示了这一闭环:

@jsonable private class ScoreCard { @constraint[score > 0] @constraint[score <= maxScore] let score: Int64 let maxScore: Int64 } let scoreCard = agent.chatGet<ScoreCard>("generate a score card").getOrThrow()

该示例覆盖了"约束失败触发修复并记录校验提示""非 JSON 输出触发 schema 修复""混合修复""必填字段缺失修复""重试耗尽返回 None""上游模型失败原样抛出"等场景(main.cj),并可通过LLM_API_KEY、LLM_MODEL、LLM_BASE_URL环境变量对接真实模型跑端到端验证(L289-L452)。配套的 json_output_test.cj 还用chatGet<MyDateTime>实测了模型返回结构化年份数据。

八、使用建议与注意事项

  1. 把业务数据模型声明为@jsonable,即可同时获得序列化、类型模式与约束校验,无需手写样板代码;
  2. 用@field写清字段语义、用@constraint表达跨字段业务规则,两者都会进入 schema,让 LLM 在生成阶段就尽量合规,减少修复轮次;
  3. 用Option<T>表达可选字段,序列化时None会被省略,反序列化时缺失或显式null都会还原为None;
  4. HashMap可作容器但不能作chatGet<T>的返回类型(无 schema);需要结构化输出时请定义@jsonable类;
  5. 重试次数可调:Config.outputRepairRetryNumber默认 3,对强约束场景可适当调大,对延迟敏感场景可调小;
  6. 涉及枚举时确保是穷举枚举,否则@jsonable宏会拒绝编译。

九、总结

magic.jsonable以"接口 + 枚举模式 + 宏变换"三层结构,把 JSON 序列化、类型模式生成与语义约束校验统一到Jsonable<T>一个契约之下。它既是chatGet<T>结构化输出的数据契约来源,也是 LLM 输出可靠性兜底的第一道防线:schema 指导生成、约束拦截非法值、修复闭环让模型自纠。理解这一层,你就能在 CangjieMagic 中轻松构建"模型输出 → 强类型可信数据"的完整管线。

延伸阅读:包级 API 总览见 package_docs/jsonable.md;DSL 宏定义见 package_docs/dsl.md;完整测试见 jsonable_test.cj;端到端示例见 jsonable_constraint_verifier。

  • 人工智能
  • 大模型
  • AI Agent
  • RAG
  • 工具调用
  • MCP 服务
  • MCP Clients
  • 多智能体

【免费下载链接】CangjieMagic

基于仓颉编程语言构建的 LLM Agent DSL,其主要特点包括:声明式 DSL、支持 MCP 协议,支持任务智能规划等。

项目地址:https://gitcode.com/Cangjie-TPC/CangjieMagic
点击查看免费下载

相关推荐

上一篇:魔兽争霸3终极兼容性修复指南:告别卡顿乱码,解锁现代游戏体验
下一篇:ComfyUI-VideoHelperSuite终极指南:3步构建AI视频工作流

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进