前言3:AI持有契约,Schema In AI,Not Service
Created By RV, and licensed with Creative Commons "CC BY-NC-ND 4.0"
来自《面向AI Programming的软件工程范式》
这一章节,其实是理论书《面向AI Programming的软件工程范式》的一篇杂言。
虽是杂言,却完整解释了为什么要在服务层完全抛弃Schema,为什么要选择非编译型的动态语言。
快速扫一遍,我们更容易放下强类型、伪强类型、静态类型等面向人类程序员的执念。
Schema在AI,类型检查,既可编码时由AI生成校验,也可运行时由AI动态注入。
以下,是正文。
########假装是分割线########
很多事情,在其起始,驱动我们起而行之的,往往只是一种直觉,一种信念。
看到一个可行、可信的目标,保证其不会沦为空谈,能将其落实在地,只有一种力量:要相信,相信的力量。
“No Schema”易
最早,在面向AI Programming的服务调用中,提出“No Schema: Schema In AI, Not Service”时,这仅仅是一种信念。相信其可行,相信其是“面向AI Programming”,相信其会为AI Coding带来极大的便利。
要实现No Schema,从RPC架构中拆掉Schema,保证Not In Service,不是难事。
通过KreeX-RPC: 重构面向AI Programming服务调用范式,讲述理论的同时,也完成了Kree4JS整个框架的创建:
“No Schema、异构通信网格、Serverless对等服务、传输与服务分离、透明RPC本地-远程语法一致、单调用粒度按需全链路跟踪、原生服务治理、Select-Reduce策略的服务集群……”
破立,只在数月之间。
“Schema In AI”难
Service层没有Schema,可以。
但是,整个体系,在其架构中,不可能没有Schema,所谓:没有规矩,不成方圆。
不论是“杀人者死,伤人及盗抵罪”式的约法三章,还是现代洋洋洒洒的严密严谨的法典,规矩的形式会变,但是“要有规矩”,这个本质不会变。
从Service层拆掉Schema,只是因为Service没必要持有Schema,AI持有Schema会更好。
Schema不会消失,只会形式变化,位置转移。
但,AI怎样持有,以何种形式、何种格式持有Schema,则是一个令人头痛的难题。
不过,虽然难,还是有优雅解的。
一. AI Native:在AI层定义Schema,在AI层持有Schema
从0到1,是破冰开路之旅,这总是最难的。
在没有现实可见、物理存在的证据之前,没人愿意在一个Idea上投入资源,除了迷之自信者,还有先行者。
以何种形式实现“Schema In AI”这个难题,其火花式的创意,来自A2UI的“UI Component Catalog”。
这个问题上,我,不是先行者,只是迷之自信者。
相信,被Inspired,然后借其道,用其意。
A2UI,将AI可用的UI组件编目,为AI生成UI划定明确的可用组件清单,这是UI Schema。
这是,“道”。止步于“UI Component Catalog”,过于可惜了。
一个APP,有组件、有布局、有样式、有服务、有数据。
同一个“道”,泛化到不同的范围即可。
然后,我们得到了DataModel Catalog + Service Catalog,这就是“Schema In AI”,这就是AI持有,约束AI,同时钳制了所有Service的唯一契约。
AI Native是指,整个体系脱离了AI则会坍塌和崩溃。
开发期,编制Schema,为Service和DataModel编目,这是一个人类主导 + AI辅助的过程。
意图、创意,来自人类,繁琐的实现,AI来编写,人类review修正。
一份Schema,由AI进行多语言适配,由AI实现多服务框架适配。
运行期,如何根据Schema校验服务调用,有两种方法:
AI Coding时,根据Schema在服务内部注入校验代码。
服务框架提供动态AOP拦截机制,运行时AI Agent根据Schema动态注入拦截代码。
无论如何,脱离AI提供的智能化、自动化能力后,Schema的编制、动态校验、动态维护、动态分发机制都会崩溃。
不是人类没“知识”能力,是人类没“数量、动态匹配”能力。
二. Schema散在Service,实质是无Schema
当Schema只存在集中于中央的一份时,才是“真正的真相只有一个”。
这个道理很简单:各处封建,那是春秋战国;车同轨,书同文,才是一个大一统的帝国。
允许每个服务提供者,自下而上,定义自己的服务Schema,最终的结局则是“人人都有,人人都无”。
局部的有序,最终,带来全局的无序。
当Schema散在Service中时,每个Service的技术方案不同、schema格式不同、同类型多语义、同语义多类型……
对于身处中央的AI来讲:
- Coding时,要逐接口学习,逐schema理解,逐Service特异处理,麻烦无穷。
- 运行时,Service版本演进,Schema与实现脱节,动态漂移,惊喜处处惊心。
不论是人工编程,还是AI编程,最不需要的就是“平权”。放弃集中式的Schema编制与编目,是自取其乱。
Schema散落在Service,则是事实上的:
任一Service都有自身Schema,任一Service都无全局Schema,居中调度者全无Schema。
何必。
三. Schema Catalog:为AI编制同一的Schema定义目录
集中式的Schema Hub不解决本质问题
提供一个集中式的Schema Hub,允许各个Service自下而上定义自己的Schema,然后向Schema Hub注册,从而实现某种对Schema的集中管理,这不是我们的期望中的解决方案,它也解决不了任何的实质问题。
只要是“自下而上”,它就不是一种适合AI编程的解决方法。
自上而下,才既是短期的霸道,也是长久的王道。
编制Schema,使用Schema约束AI的代码生成,使用Schema定义服务间的通信契约,使用Schema动态校验服务调用,使用Schema定义AI Agent与AI Agent的通信契约。
Schema本身才是真相,而Service实现,则是转身即可能消失、随时重生刷新、临时动态的产物。
探索期,理清意图,以Service Catalog订立契约
Schema应该是在“探索期”被制定的,Schema本身就是探索期的可见成果之一。
一个工程、一个功能模块、一个feature开始之初,我们往往并不能清晰地看到它落地的形式是什么。
个人,我的习惯是,只讲意图,只讲“Intents”。
我的MonoRepo下,一般有一个Intents目录,用来存放各种Intent碎片。
有了想法,不归类、不分层,只要求AI立即记录,并综合分析已有的各个Intent,是否重复、是否交叠、是否冲突。
然后,要求AI针对Intent,给出设计草案,按照预置的C4规则,进行逐层拆分、细化。
C4的L4层,实际所形成,就是数据模型代码、类层次划分、API方法签名定义。
剩下的事情,则简单多了。
定下Service Catalog的Schema,要求AI,基于C4的L4代码层UML定义,转写出Service Catalog实例。
例如,greeting.hello(someone:string):string在ServiceCatalog中,长这个样子:

基于这个Schema:
- AI生成的调用代码签名,可被约束、可被校验
- 服务调用被AOP拦截后,实际调用参数、返回值可被校验
Kree4X RPC + kree4UI配合使用,从理论到实现,好像没遇到什么不可逾越的障碍。
整个过程,唯一不可或缺的是AI。
没有AI的辅助,每一步都要人来细致、严谨、繁琐地操作,这基本是一个理论可行,而实际不可能的任务。
开发期,基于Service Schema收敛AI生成代码的边界
编制了Service Catalog后,自然就为AI Coding时划定了明确的边界。
在为AI划定边界:编制Catalog白名单,UI Component、AI Design、Service、DataModel中,我们展开讨论过这个问题。
此处可以一笔带过了。
简单地提一下:
- 生成式UI中,Service Catalog划定了AI生成的UI Component实例中,各个组件可调用的Server端服务的白名单
- AI Coding中,Service Catalog提供AI可调用的各种Service的白名单边界,以及调用时详细的方法签名信息,方法功能说明。
而获得这一切能力,付出不过是:
- 编制提示词,指导AI方法论是什么
- 提供校验器,校验AI的输出是否违反了Schema约束
- Review时,要求AI遵循Schema约束,检查上一个Agent的输出。
运行时,使用Service Schema强制校验服务调用
运行时,使用Service Schema校验服务调用是否违例,实际很简单。
前边提过,存在两种流派。
AI Coding时,根据Schema在服务内部注入校验代码。
这个属于力大砖飞的解法,Coding时,在服务、方法的入口处,写入Schema校验代码。
Raw代码,根据schema信息,根据语言不同,编制不同的校验代码。
function hello(someone) {
if (typeof someone !== 'string') {
throw new Error()
}
……
}
Schema修订一次,涉及的源码修改,漫山遍野。
人类做,既繁琐,又容易出错。
此种场景,则是AI所擅长的小粒度、批量、短程任务,由AI处理,又快又好。
力大了,砖确实能飞的。
AOP拦截机制,注入拦截代码,根据Schema动态校验
各种主流的服务框架,基本都有AOP式的拦截器、或者中间件机制的,允许按照一定的规则拦截特定服务,执行特定的业务逻辑。
eg. 常见的登录校验、令牌检查、权限检查……
Schema是一种规则,使用AOP拦截注入检查具体的服务调用,这是一种技巧式的解法。
以Kreejs的拦截器体系为例:
// 初始化CatalogManager
const catalogManager = CatalogManager.addService('./kree4ui-demo-service-catalog.json')
// Kree4js服务拦截器
class ServiceInterceptor {
/**
* 调用后拦截,校验返回值
*/
afterCall (ctx, result, cluster, methodName, params, options) {
const { name: serviceName } = cluster
if (!result.ok) {
return
}
// 获取校验器
const validator = catalogManager.service(serviceName)
// 校验方法调用返回值
const validateResult = validator.validateReturn(methodName, result.value)
if (!validateResult.ok) { // 校验失败,返回错误
return { done: true, error: validateResult.error}
}
}
/**
* 调用前拦截,校验入参
*/
beforeCall (ctx, cluster, methodName, params, options) {
const { name: serviceName } = cluster
// 获取校验器
const validator = catalogManager.service(serviceName)
// 校验方法调用入参
const validateResult = validator.validateCall(methodName, params)
if (!validateResult.ok) { // 校验失败,返回错误
return { done: true, error: validateResult.error}
}
}
}
如何选择
何时选择力大砖飞的Raw校验,何时选择基于规则的AOP式校验,其实并没有所谓的“必须怎样”的原则。
传统的人工时代,我们对于修改源代码,有一种近乎于“禁忌式”的思维定式:大量改代码,是不对的。
而在AI时代,最终代码会沦为一种廉价的临时中间级产物(努力中……),AI Coding,何必在意?
如果,你的系统规模庞大,或者演变迅速、Schema不稳定、代码也不稳定,基于AOP式的规则动态注入,则会提供更好的开发体验,和运维便利。
最差,最差,AOP式拦截,可以省Token,不是么?😊
AI-2-AI,使用Service Schema定义AI Agents通信语言
AI时代,对于RPC式的服务,我有一种近乎偏执式的偏爱。
因为:RPC服务,自带语义。
名字就是力量!
方法名,就指示了自身存在的意图;参数名,就指明了参数存在的目的。
配合Service Schema,提供类型约束,签名约束,及针对AI的Instruction信息描述,它天然对AI友好。
在此方面,HTTP几乎处处都是反派。
使用Service Schema,订立A2A,Agent To Agent的通信语义,与我而言,则是一种最自然的选择。
“Agent To Agent需要通信语义契约”,这是本质需求。
而如何实现,则是个人偏好了。
我的选择,是基于RPC的Service Schema声明。
严格讲是基于Kree4X RPC的Service Schema。
- Kree4X服务与通信分离。一个服务实现,并不关心底层的通信机制。
- Kree4X通信层异构协议组网,HTTP、TCP、UDP、WebSocket……,各种节点可以无缝组网,这大幅消解了HTTP所谓的“普适性”
- Kree4X节点动态发现,节点间动态协商,动态直连
- kree4原生支持服务集群,Select-Reduce模型,动态选择服务节点,动态合并处理结果
最重要的是,架构统一。
不论是Web客户端与服务端、微服务节点与微服务节点间、还是客户端与AI、服务端与AI、AI与AI间,所有参与者都使用统一、简单的透明RPC语义式通信。
挺好。
四. Schema In AI,强类型语言是累赘
工程上,我是实用主义者。
什么场景,适合什么语言,就选择什么语言。试图金手指式的一种语言走天下,这是自找麻烦。
而AI时代,在大规模的AI Coding后,一个强烈的断言式的感觉越来越觉得理所当然:
“非必要,不要强类型。”
某些场景,不得不为之,那就只能用之。
有任何可以替代的可能,就果断把强类型语言扔到一边,有Javascript,有Python可用,就不要碰任何真强类型、伪强类型语言。
最讨厌“引战”,此处也无任何引战之意。
一家之言,不喜勿扰。
AI时代,变是唯一的不变,动态性第一
源代码,最终会成为没人多看一眼的黑盒,最多是灰盒。
Rule 1: 软件成为灰盒,甚至黑盒,我们旗帜鲜明地表达过这个观点。
意图是根本,Schema是契约,而代码则是派生,只是唯一真相的一种阶段性投影。
重要么?很重要。
真的重要么?其实不重要。因为,AI持有明确严格的Service Schema及详细的C4级设计分解时,我们可以拿Token换。
源代码,很廉价。
LLM能力够强,人类或者审查Agent的Review能力跟得上的话,它的质量也能很可靠。
所以,如果一个场景下,多种语言都可以满足要求的情况下,AI优先。
AI First,是一种态度,整个体系给AI让路,AI怎么便利,我们就怎么做事。
AI生成式情景下,静态编译式语言天然处于劣势
动态生成的场景下,任何静态编译式的语言,其实都不适合AI处理。
“动态生成”,是一种生产环境状态。此时“静态编译”才可交付,意味着需要把开发期的基础设施带入到生产环境,这一点极其荒谬。
太重、过重,低效,这一点上,应该是基本的共识。
何必呢,又不是没有别的选择。
强类型静态语法检查:阻止人类语法错误,对于AI得失已失衡
即便不是AI编程时期,个人也对类型体操深恶痛绝。
Javascript + JSDoc为IDE提供类型提示已足够,所以当Svelte 团队正式把 Svelte 核心代码库从 TypeScript 迁移到 JavaScript + JSDoc,我所能做的只有会心的一笑。
静态的类型检查,是一种针对人类编程的特异化设计。
对于AI,则是彼之蜜糖我之毒药。
强类型,最大的价值,在于人类编写源码时,IDE即可即时给出反馈,防止人类程序员各种低级的语法错误。
而在AI时代,这成为了一种时代的化石。类似于,智能手表时代的机械表,精致、精美的小众选择与艺术品,对大众来讲,被毫不犹豫地抛弃。
AI不需要强类型,AI需要的是Service Schema的语义提示。
AI编写代码,编码期需要的是语义。
调试运行期,AI不靠强类型兜底,靠的是运行时试错,靠的是完备的测试用例约束。
强类型运行态类型检查:有用,但不够,Schema定义约束不仅是类型
回到现实,从交付的角度来讲,强类型既多余,又不够。
类型检查,与Service Schema机制重复。况且类似TS之类的伪强类型,在运行期毫无类型。
而Service Schema所能提供的语义式约束,例如最大值、最小值、取值空间、匹配模式、多参数校验规则,没有任何是强类型语言所能提供的。
既然开发期,人类不在编码,既然在运行态,还是要引入校验代码,那么,何苦来着?
使用前述的“运行时,使用Service Schema强制校验服务调用”,不香么?
BTW:
Kree4X RPC + Kree4UI结合使用后,涌现出各种奇奇怪怪,但是架构优美的能力。
越来越体会到《技术的本质》书中所言的“组合进化”的乐趣。😁