ModelCraft:01-如何调研和闭环
从 Duck talking、自主验证 demo、区分 Mock 边界到沉淀高质量测试,讨论技术调研如何形成闭环。

ModelCraft:01-如何调研和闭环
背景
上一章,我们讲解了什么是数据模型,以及数据模型有哪些功能。 接下来,就逐步讲解如何实现一个数据模型。
其实这节课可能大家会觉得有点水,但是实际参与工作中非常重要的一个环节。如何明白你要做什么事以及如何调研?
换句话说,老板给你讲了一个事情,希望你去负责,然后调研,然后得到一个结论,然后大家租户做出决定。这是工作中你成为一个主程必要的一个能力。
这篇文章我其实只想讲 4 件事:
- 如何理解问题:先用 Duck talking 把问题说清楚
- 如何验证 demo:不要只听 AI 讲,要自己上手改一改
- 如何学会 Mock:知道哪些必须真实,哪些可以先假设
- AI 时代最大的资产:高质量测试用例
后面 GraphQL、runtime、meta、SQL 这些内容,都是用来承载这 4 件事的案例,而不是为了炫技。
1. 如何理解问题:Duck talking 自我澄清
这里我简单的说一个我们要做数据模型。很多人对这个数据模型的概念还不清楚,它究竟是个什么东西呢?
有这个疑问是非常正常的,在工作中,我们会接触大量这样有可能之前从未出现在之前知识体系内的事情。这个时候,就需要我们拥有一种能力,就是理解事物的能力。我觉得这个能力是区分老手和菜鸟的关键能力。
我一般如何做呢?我经常采用的是Duck talking的方法。即自己问自己。给自己预设一些问题,然后尝试回答它,在回答的过程中,大脑逐渐明白这件事情。

比如:
Q:数据模型最核心的能力是什么? A:提供一套协议和组件交互,让组件拥有标准的,规范的后端crud接口适配,保证在开发新组件的过程中,仅依赖协议即可完成。 更好的结果是,面对开发者直接调用协议,在不通过工单,以及技术支持的情况下也能很好的使用,在AI时代,通过一个skill,或者其他方式,就能让AI掌握。
Q:如何设计标准的,规范化的协议,为什么不能用开源既有实现? A:首先开源没有。因为这里要做的事情首先是提供动态的,强类型的协议。比如restful动态,但是不是强类型。比如Odata,强大但是过于灵活, 但是安全性有问题,需要明确允许哪些 $expand、最大深度, 而且生态不友好。然后自然而然的就落到了Graphql上。
Q:为什么是Graphql? A:这一步其实需要去读Grapqhl文档,自己最好写1-3个demo,去深刻体会。注意不管你怎样调研,你始终是带着自己的品味的,所以你的目标是1.走广度,即看下这个技术的宽度是多少,即生态,社区活跃度,多少人认可。2.走广度,亲自写demo, 明白哪些具体能力是干什么,然后是哪些官方在使用,这些官方背书决定了技术的上限。
Graphql 1. 强类型语言。 2. 天然的自解释性。 3. github以及facebook的背书。 4. post接口 + query,mutation 接口名的方式天然适合做鉴权。5 设计之初就是解决overfecth问题完美适配select语法等等
在确立Graphql作为协议的底层后,接下来就是去调研使用哪些开源库?
因为我现在是Go的脑残粉,所以技术选型无脑拥抱了Go。
2. 如何验证 demo:不只是让 AI 讲给你听,还要自己上手改一改
Helloworld 的思想 VS AI
我想,但凡是学过编程语言的同学。 都知道Helloworld的思想, 其本质原理就是把一个复杂问题拆解成简单问题。 然后利用TDD的思想,红绿红,一步一步从简单到复杂,然后完成一个功能,从而解决复杂问题。 有人会问,AI来了, 还需要这种思想么? 在我个人的观点看来, AI会不代表你会。即有时候我比较难以理解的是,很多人会有AI出现了后的知识无用论。先不说AI幻觉的问题,难道以后所有复杂问题抛给AI解决么?真的存在这样一种银弹么?
AI时代,虽然说你可以不用写一个真的Helloworld了。但是官方给的Demo你要具备能改的能力。很多人现在依赖AI到什么程度呢,当我交给我的实习生去了解某个事情的任务后。他直接给我了AI总结的一个文档。但是当我去对一些地方提问的时候,他又现场提问AI。额,我们可以把工作交给AI,但是不能把脑子交给AI!!!
数据模型的核心模块是runtime
数据模型的核心就是:如何把一个meta信息暴露成动态的graphql,然后一条grahpql 变成sql真正执行
对应的模块是:https://github.com/luker1228/modelcraft/tree/master/modelcraft-backend/internal/domain/modelruntime
他是数据模型的最高宪法,是一等公民,是因为这点醋,才包的这个项目的饺子。
入口是: /data/home/lukemxjia/modelcraft/modelcraft-backend/internal/app/modelruntime/graphql_app.go
案例:用 GraphQL runtime 调研来验证最小 demo
调研开源库
我当时主要看了 3 个 Go 里最常见的 GraphQL 方案:
github.com/99designs/gqlgengithub.com/graphql-go/graphqlgithub.com/graph-gophers/graphql-go
它们看起来都叫 GraphQL for Go,但设计哲学其实完全不一样。

这里我特别想补充一点。调研不是让AI给一个结论就完善了。 我见过太多装模作样调研的人,其实很简单,就是“实践是检验真理的唯一标准”,你要真的自己用下demo,手动改一改,哪怕加点日志。 也会让你对问题理解更加深入和具体。 人是无法理解超出自己认知之外的事情的。
1. gqlgen:schema-first,类型安全最强
gqlgen 的思路是先写 .graphql schema,然后自动生成 Go 代码和 resolver 骨架。
优点:
- 类型安全很好,编译期就能发现大量 schema 和代码不一致的问题
- 对静态业务接口很友好,团队协作体验成熟
- 生态完整,工程化能力强,做正式 API 很稳
缺点:
- 太依赖 codegen。schema 一变就要重新生成代码
- 更适合“接口是提前定义好的”场景,不适合我这种运行时动态拼 schema 的需求
- 当 model、field、where input 都是运行时根据 meta 生成时,
gqlgen这套静态生成链路会非常别扭
一句话总结:如果你做的是常规业务 GraphQL API,gqlgen 往往是第一选择;但如果你要做 dynamic GraphQL runtime,它反而太重、太静态。
2. graphql-go:code-first,但运行时最灵活
graphql-go 的思路比较朴素:直接在 Go 代码里构造 Object、Field、Schema,然后执行 graphql.Do()。
优点:
- 运行时拼装 schema 很灵活,特别适合根据 meta 动态生成类型和字段
- 心智模型简单,核心链路清楚:
Schema -> Field -> Resolver -> Execute - 非常贴合我这里的需求:把“模型定义”翻译成 GraphQL schema,再把查询参数翻译成内部查询对象
缺点:
- 类型安全弱很多,很多问题只能在运行时发现
- 手写对象定义比较啰嗦,复杂后容易散
- 工程化配套没有
gqlgen那么强,很多约束需要自己补
一句话总结:它不是最现代的方案,但非常适合做“运行时动态 schema 构建器”。
3. graph-gophers/graphql-go:更偏标准 schema,但动态能力一般
graph-gophers/graphql-go 也是 Go 里很常见的一套实现,它更偏“先有 schema,再绑定 resolver”。
优点:
- API 设计比较干净,整体体验比纯手写 AST/对象层舒服
- 对标准 GraphQL 语义支持不错,适合常规服务端接口
- 比较适合“我有一份稳定 schema,然后把 resolver 绑上去”的模式
缺点:
- 还是更偏静态 schema,不像
graphql-go那样适合在运行时大规模拼装类型 - 做动态 input、动态 relation、动态 filter 时,不如
graphql-go直接 - 对我这个“meta -> runtime schema” 的核心问题帮助没有那么大
一句话总结:它比 graphql-go 更规整,但没有 graphql-go 那么适合做高度动态的 runtime。
最后为什么我会落到 graphql-go
因为我这里真正要解决的问题,不是“给几个固定表写一个 GraphQL API”,而是:
- 模型是动态的
- 字段是动态的
- where / orderBy / relation 也是动态的
- 整个 schema 需要在运行时根据 meta 组装出来
这时最重要的能力不是 codegen,也不是静态类型绑定,而是运行时拼 schema 的自由度。
所以如果把这 3 个库放在一起看:
gqlgen最适合静态、正式、工程化很强的业务 APIgraph-gophers/graphql-go适合偏标准、偏静态 schema 的服务graphql-go最适合我这里这种 dynamic runtime
因此最后选 graphql-go,不是因为它“最先进”,而是因为它最贴合问题本身。
接下来真正关键的,不再是“继续讨论概念”,而是要把问题收缩成一个可以验证的 demo。因为调研如果不能落到一个可运行、可修改、可观察的最小闭环,最后大概率只会停留在嘴上理解。
只看一条链路:select * from xxx limit 10
query { findMany( where: { status: { eq: "done" } } orderBy: [{ createdAt: "desc" }] take: 10 skip: 20 ) { items { id title status } totalCount }}我当时真正想验证的问题只有一个:
在 demo 阶段,怎么先验证“查询多条”这件事能不能用 graphql-go 跑通?
这里先别讲复杂架构,也先别讲动态 SQL 生成。
因为 demo 阶段根本不该追求“大而全”。 这时候要看的就 3 件事:
graphql-go能不能定义一个findMany查询入口- 这个入口能不能拿到参数并命中 resolver
- resolver 能不能返回一个列表结果
这 3 件事一旦成立,就够了。 说明这个库至少能承载“查询多条”这个最小能力。
第 1 步:先用 graphql-go 定义一个最小 schema
graphql-go 是 code-first 的。也就是说,你不用先写 .graphql 文件,而是直接在 Go 里定义类型、字段和 resolver。
所以 demo 阶段完全没必要一上来就搞动态模型。先写一个最小版本就够了,比如:
- 一个
Todo类型 - 一个
findMany查询字段 - 返回值里先有
items
本质上先证明这件事:
GraphQL Query -> graphql-go Schema -> findMany resolver -> 返回列表这个闭环能跑起来,后面才值得继续投时间。
第 2 步:给 findMany 挂一个 resolver
graphql-go 的核心点很简单:每个字段都可以挂一个 Resolve 函数。
所以 findMany 能不能工作,关键根本不在 SQL,而在于:
这条查询,最后能不能稳定命中一个 resolver这个 resolver 在 demo 阶段甚至可以先不查数据库,直接返回假数据。 因为你此时要验证的不是“数据库链路有没有通”,而是:
- GraphQL 字段定义对不对
- 参数结构能不能被解析
- 返回列表结构对不对
最小的 resolver,大概就是这样:
Resolve: func(p graphql.ResolveParams) (any, error) { return map[string]any{ "items": []map[string]any{ {"id": 1, "title": "write article", "status": "done"}, {"id": 2, "title": "record video", "status": "done"}, }, "totalCount": 2, }, nil}这一步一旦成立,就已经证明 graphql-go 能做“查询多条”。
第 3 步:再验证参数能不能传进来
只有返回死数据还不够。 查询多条真正有意义的地方,在于它通常会带筛选、排序、分页。
所以第二个要验证的点是:
where / orderBy / take / skip能不能从 GraphQL 请求里进到 resolver在 graphql-go 里,这些参数最终都会出现在 graphql.ResolveParams 里。
也就是说,当你写下这条查询:
query { findMany( where: { status: { eq: "done" } } orderBy: [{ createdAt: "desc" }] take: 10 skip: 20 ) { items { id title status } totalCount }}resolver 里理论上就能拿到这些参数。
demo 阶段不需要把它们优雅抽象,也不需要马上设计统一查询对象。 你只需要先证明:
func(p graphql.ResolveParams) (any, error) { where := p.Args["where"] orderBy := p.Args["orderBy"] take := p.Args["take"] skip := p.Args["skip"]
_ = where _ = orderBy _ = take _ = skip
...}这里能拿到值,就说明 graphql-go 已经具备承载查询语义的能力。
第 4 步:最后再把 resolver 里的假数据换成真实查询
到这一步,demo 的核心目标其实已经差不多完成了。
因为你已经验证了:
Schema 能定义-> Resolver 能命中-> 参数能传入-> 列表能返回剩下的事情才是:把 resolver 里的假数据替换成真正的数据来源。
这个数据来源一开始甚至也不用复杂。 完全可以分三层逐步验证:
- 先返回内存切片,确认 GraphQL 结构没问题
- 再写死一条
select * from xxx limit 10,确认数据库链路没问题 - 最后再把
where、orderBy、take、skip一点点补进去
也就是说,demo 阶段真正合理的顺序不是:
一上来就设计完整 runtime而是:
先证明 graphql-go 能承接查询多条 -> 再接一个最简单的 SQL -> 再逐步增加查询语义第 5 步:所以 demo 阶段真正要看的,不是“架构优不优雅”
而是下面这个最小闭环有没有成立:
query findMany -> graphql.Do() -> findMany resolver -> 拿到 Args -> 查询数据 -> 返回 items这个闭环成立,就说明 graphql-go 这个库可用。
这个时候,你才有资格继续往下想:
- 如何抽
Input - 如何做统一 SQL Mapper
- 如何支持动态模型
- 如何把 relation、aggregate、cursor pagination 全补上
这些都属于第二阶段的问题。
完成这一步之后,其实“计算”这件事就已经被解决得差不多了。
所谓计算,讲白了就是:给我一条 GraphQL,我能不能命中 resolver,能不能把参数接住,能不能把结果算出来再返回。 只要这个最小闭环成立,说明执行层是通的。
那剩下来的问题就不是计算了,而是存储。
也就是:这些 meta 信息到底怎么存?
但这个问题本身没有想象中玄。 它本质上就是表设计。
而且这里也不需要故意发明新东西。 因为我做的不是数据库本体,而是一层装饰层。 我关心的不是重造 table、column、index 这些概念,而是对这些 meta 信息做增强。
所以最自然的做法,就是直接模仿数据库自己的结构去设计:
- 表,就对应一张 meta table
- 字段,就对应一张 meta column
- 关系,就对应 relation meta
- 再在这些基础信息上补充自己的增强字段
比如展示配置、校验规则、权限标签、关联语义,本质上都是“增强信息”,而不是底层数据库事实本身。
所以存储层这件事,反而不需要讲得太神秘。 它没有 runtime 这边这么多执行细节,本质就是:沿着数据库原有的结构建模,然后在上面加一层增强 metadata。
接下来真正要考虑的问题,不是“怎么存”,而是:怎么触发存储。
因为这些 meta 信息不是凭空来的。 你总得有一个入口,把数据库里的表、字段、索引、关系这些基础事实,同步到你自己的 meta 表里。
这时候要做的事情也不复杂,核心就是暴露一个“同步接口”。
比如你完全可以先设计成这样一种语义:
sync database metadata它的职责很单纯:
- 连接目标数据库
- 扫描数据库里的表结构信息
- 把 table / column / relation 这些事实层数据写入自己的 meta 表
- 对已经存在的记录做更新,对不存在的记录做新增
这里最重要的不是实现细节,而是接口语义要清楚。
也就是说,这个接口不是“帮用户改数据库结构”,而是:
把数据库当前的事实,拉一份出来,同步成系统自己的 metadata。
这个边界一定要守住。 因为我是装饰层,不是数据库迁移工具,也不是 ORM migration 引擎。 我不负责替用户创建表、删除列、改索引。 我只负责识别数据库现在长什么样,然后把它映射成我的 meta 系统。
所以这一段设计接口时,简单描述就够了,不需要展开太多实现。 真正重要的是把职责说清楚:
- 输入是数据库连接和目标库信息
- 过程是扫描数据库 schema
- 输出是同步后的 meta table / meta column / relation metadata
到了这里,存储这件事也就差不多闭环了。
再往后,数据模型的核心工作其实就只剩一条主线:把 findMany 这条最小查询链路扩展出去。
也就是从:
findMany
逐步扩成:
countcreateupdatedeletefindUnique
这里为什么这么设计,其实也没什么特别高深的原因。 我基本就是直接抄 Prisma。
原因很朴素:不是因为我做过什么严密论证,也不是因为它在理论上最优,而是因为 Prisma 足够流行。
既然它流行,就说明这套命名和操作心智,很多开发者已经被教育过了。 那我直接沿用它,成本最低。
换句话说,这里的目标不是发明一套全新的 CRUD 语言,而是尽量站在现成生态上。 用户一看 findMany、findUnique、create、update、delete、count,基本不用解释,就知道大概是什么意思。
所以这里的设计动机其实非常现实:不是追求原创,而是直接复用 Prisma 已经跑出来的用户心智。
本质上这不是重新发明一套新系统,而是在同一套 runtime 思路上,把不同操作一个个补齐。
当这些基础能力补完以后,数据模型这件事的核心其实就已经设计完成了。 后面再往下做的 relation、aggregate、分页、权限,更多是能力增强,而不是架构性质变。
别看这里前面写了这么多,其实思路本身并不复杂。
说到底,无非就是几件事:
- 先验证
graphql-go能不能承接最小查询闭环 - 再把数据库事实同步成自己的 meta 存储
- 再把
findMany这条链路扩成count、create这些基础能力
这套东西在今天这个阶段,代码怎么写反而已经不是最难的问题了。 有 AI 编程在,很多 demo、样板代码、重复劳动,推进速度都会非常快。
真正难的地方,不是“这一行代码该怎么敲”,而是:你脑子里有没有这套完整闭环。
也就是你是否真的想清楚了:
- 先验证什么
- 后抽象什么
- 存储边界在哪
- 同步接口负责什么
- runtime 和 metadata 的分工是什么
这些东西一旦想清楚,代码只是把设计翻译出来。 但如果闭环没想清楚,就算 AI 帮你一天写几千行,也只是更快地把混乱实现出来。
3. 学会 Mock:最重要的不是会不会造假数据,而是知道哪里必须真实
很多人第一次做 demo,最容易犯的错就是两个极端:
- 要么什么都想一次做真,结果被数据库、权限、网络、初始化脚本拖死
- 要么什么都 mock,最后跑出来一个看似可用、其实什么关键问题都没验证到的空壳
所以真正难的,不是“会不会 mock”,而是:你能不能判断出哪些东西是这次调研必须保真的,哪些东西只是为了加快验证速度可以先替代。
在我这个案例里,真正要验证的是 GraphQL 这一层执行语义能不能成立,所以“查询链路”必须尽量真实,而“数据内容”本身完全可以先假。
也就是说,Mock 不是偷懒,而是在保护你的验证目标不被无关细节稀释。
这里继续往前走一步,就会自然落到第四件事:当你已经知道怎么验证、也知道哪些地方该 mock,接下来最值钱的就不再是某一段样板代码,而是你沉淀下来的验证资产。
4. AI 时代最大的资产:高质量测试用例
因为在古法编程时期,我本来就是一个很注重单测的人。 对我来说,写高质量测试一直不是“锦上添花”,而是工程里最值钱的那部分资产之一。
不过这里要说得更准确一点:高质量测试资产,并不是简单的“越早写越好”,而是要尽早开始定义核心语义,但不要过早追求把所有测试一次写满。
为什么呢?因为测试真正有价值,不是因为它出现得早,而是因为它测的是不是系统真正应该成立的语义。
如果你的代码已经写完、甚至已经上线,再回头补单元测试,问题就会变得很明显:
- 这些测试往往测的是“现在这坨代码怎么工作”
- 而不是“这个系统本来应该怎么工作”
- AI 为了保证线上行为不被破坏,也会倾向于迎合现状
- 结果就是把很多历史兼容、偶然行为、脏边界一起固化进测试里
很多兼容,在系统一开始其实根本没有必要。 但如果你太晚补测试,这些本来不该长期存在的东西,就会因为“测试已经这样写了”而被反向合理化。
所以今天真正稀缺的,已经不是“会不会写测试代码”,而是:你能不能在语义还干净、边界还清楚的时候,把最核心的行为先沉淀成测试。
这一点在这里尤其重要。 因为从 graphql 到 sql 的转换,本质上不是一个小函数,而是一整组函数在协同工作。
这里面会不断出现各种边界:
where怎么解析orderBy怎么落take/skip怎么处理- 空条件怎么处理
- 非法字段怎么报错
- 不同组合条件下 SQL 长什么样
这些东西如果只靠人脑记,迟早会漏。 但一旦通过测试把它们固化下来,这条链路就会越来越稳。
而且这里的测试价值,不只是“防止改坏”。 更重要的是,它会把你对 runtime 语义的理解沉淀成可执行资产。
这里还有一个很重要的分寸感:测试用例只有经过多层次验证才真正有用。 也就是说,它不是“脑子里想一个 case 就立刻永久固化”,而是随着你对问题理解越来越深,不断筛掉偶然行为,留下真正该被长期保护的语义。
换句话说,测试测的不只是代码对不对,更是在定义:
- 这个系统允许什么输入
- 遇到什么边界应该怎么表现
- 最终应该生成什么样的 SQL
所以这类测试一旦写好,它就不只是辅助代码,而会成为项目里非常重要的财富。 真正好的测试,不是为了迁就历史实现,而是为了守住那些你确认过、验证过、值得长期保留的系统行为。
后面你改实现、换 SQL builder、重构参数解析,甚至换人维护,都没关系。 只要这批测试还在,核心语义就还在。
回到调研本身:调研不是看资料,而是设计验证路径
最后再补一句,到底什么叫调研该做的事情。
很多时候,我们去调研一门技术,不是为了“多懂一个名词”,也不是为了看完文档以后能复述几段概念。 真正关键的是:这门技术到底能不能解决我的问题。
所以调研一定是带着问题去调研,而不是纸上谈兵。
这里最怕的就是问题说不清,最后调研也只能停留在“这个框架看起来不错”“那个生态好像很成熟”这种空话上。 这些话没有任何工程价值。
真正有效的调研,反而很简单。 还是那句话:先把你的问题表达清楚。
然后把“怎么验证它能不能解决”也说清楚。
而且这个问题一定要具象化。 越具体,调研越有效。
比如我这里的问题不是:
GraphQL 好不好而是:
我能不能通过这门技术,实现动态的 GraphQL这就是一个可以被验证的问题。
围绕这个问题去调研,你就不会跑偏。 你不会再关心那些太泛的评价,而是只看几件事:
- 它能不能动态定义 schema
- 它能不能承接
findMany这种查询入口 - 它能不能把参数传进 resolver
- 它能不能先把最小闭环跑通
只要这些事能做到,就说明这门技术在你的问题上是可用的。
当然,这里面还有一个很重要的点:你要知道哪些东西必须真实,哪些东西可以 mock。
比如我这里,真正要验证的是 GraphQL 这层执行能力,那数据库数据本身就完全可以先 mock。
也就是说:
- schema 定义要真实
- resolver 命中要真实
- 参数传递要真实
- 返回结构要真实
- 但数据内容本身可以先 mock
这样调研才会快,而且不容易被无关细节拖死。
所以说到底,调研这件事真的没那么玄。 它的核心不是“看了多少资料”,而是:
你有没有把自己的问题说清楚,然后把验证路径设计清楚。
这节课的总结
如果是学生时期的我,看这节课讲什么的时候,我会觉得云里雾里。那个时候的我只想看代码,只想看设计模式,只想看数据结构。我感觉这更像编程里的“术”。如今,我今天想讲的更多的是编程的“道”,即如何自己完成一个功能,或者说一个复杂的事情,或者说你想做个东西,你究竟如何开始。
总结:
- 如何理解问题————duck talking自我澄清
- 如何验证demo————不只是AI,还要自己上手改改
- 学会Mock————其实这里最难的是要知道哪些可以Mock,哪些不可以Mock才是最重要的
- AI时代最大的资产————高质量测试用例
如果你对这类数据模型、架构设计、Agentic Engineering 的内容感兴趣,欢迎关注我的公众号:Luke’s AI Hub。
