资讯动态

AI编程中数据建模即契约:从分形项目看如何约束代码生成

发布时间:2026/10/21 16:58:23 来源:尧图企业网站定制
分形生成实验做到第六期我决定暂时停一下渲染器优化回到数据建模这件事上。越是和AI协同编程深入越发现一个被严重低估的东西契约。这里说的不是人际契约而是一种你和AI之间对数据结构、字段含义、边界条件达成共识的方式。代码写错了AI会报错模型定错了AI是沉默的——它只会顺着你的错误模型继续生成错误代码而且看上去一切正常。这篇就是我在分形项目里把“数据建模当契约用”的完整记录包括我踩过的坑、最终沉淀下来的模板以及从分形延伸到日常项目的通用打法。1. 第六期实验回头补课不做渲染先收拾数据模型1.1 前五期挖的坑能用但难改先交代一下背景。这个分形生成实验是我个人的一个长期模拟项目前几期分别做了Mandelbrot集合的缩放漫游、Julia集合变体生成、分形树的递归绘制、GLSL着色器调优以及多线程分块渲染。每一期都让AI编程助手参与了不少代码生成单看功能它完成得都不错Mandelbrot能放大、Julia能换种子、渲染速度也提上去了。但问题出在第五期结束之后。我打算把前几期的代码合并成一套可复用的渲染管线结果发现每期生成的模块都有各自的“脾气”。缩放漫游模块自己维护了一套坐标映射把视图中心和缩放系数存成两个浮点数数组分块渲染又另搞了一套矩形区域结构字段名是region里面塞着四个整数到GLSL着色器那边它干脆把所有参数都传成一个扁平的uniform vec4注释里写着“第0位是iteration第1位是zoom”。当时跑单功能都行一旦要串起来就得在各模块之间写大量的适配代码几乎每个调用点都要猜测对方的数据到底长什么样。这就是我在第六期给这个系列定的调子不写新功能先回头把数据模型统一掉。表面上是重构实际上是把之前那些“隐性的、各写各的”数据结构约定全部改成显性的、可检查的契约。1.2 从“生成代码”到“生成结构”的思路转弯很多人在用AI编程的时候注意力都放在“让AI写函数”上给一句需求让AI生成了几百行代码就觉得很爽。但这里有个误区AI生成的代码质量很大程度上取决于你对“结构”的约束。你要求它“写一个分形渲染器”它会自己发挥你要求它“在给定的数据模型上实现一个分形渲染器”它的发挥空间就被限定在了一个安全范围内。这一期我做的第一件事就是把项目里所有跨模块流动的数据抽出来逐个命名、定义字段、定取值范围。这个过程非常像给一个临时的、水平忽高忽低的协作者画施工图我不会让它去决定承重墙在哪我只会告诉它墙的高度和厚度必须是多少剩下的装修它可以自由发挥。如果说前五期教会我的是“AI能写出能跑的代码”那第六期教会我的就是“AI能写出能从数据结构上保持一致性的代码前提是你先给它一份足够清晰的建模”。这期实验本质上就是在验证这个判断。2. “契约”在这件事里到底指什么一份给AI看的边界声明2.1 隐性契约与显性契约写代码的时候我们其实一直在和人、和未来的自己签契约。函数签名是契约类的公共字段是契约API的返回结构也是契约。没有契约的代码不是不能跑而是没法在团队里长期演进——今天的你能读懂昨天的自己三个月后的你未必想读懂。在AI协同编程里这个逻辑被放大了。普通的IDE里代码是静态的你推理数据流靠上下文而AI是生成式的它在每一轮对话里都在“猜”你下一步想要什么结构。如果项目里的数据模型处于“只可意会”的状态AI就会按照它自己见过的通用模板来猜。它猜对的概率比你想象的低得多。这就是隐性契约的风险你和AI各自认为的数据结构根本不在同一张图纸上。2.2 为什么AI编程里数据建模容易被低估被低估有两个原因。一是“入门门槛不高”写一个渲染循环、调一个颜色映射出效果很快很容易让人误以为难点在算法不在数据。二是“数据建模的收益滞后”今天定好的结构可能要等三四个迭代之后才会体现价值而AI编程天然鼓励快速出活人很容易跳过建模直接要代码。我在实际项目里的感受是数据建模在整个AI协同流程里是最不像“编程”的环节却最能决定成败。算法选错可以换渲染思路错了可以推翻唯独数据结构一旦被AI在代码里铺开修改成本是指数级上升的。它可以渗透进十几个函数、跑进缓存逻辑、影响序列化和热更新。等到你想改一个字段名的时候你面对的是AI一遍遍生成过的所有连带调用。2.3 LLM的会话特性决定数据契约必须外置还有一个很现实的技术原因主流AI编程助手的会话记忆是碎片化的。你在对话开头定义了FractalParams有五个字段但几轮之后它在写另一个模块时很可能只记得“有一个参数对象”然后顺手就自己新造了一个RenderSettings字段还都对不上。不是AI笨而是它本质上是在做概率预测你放在对话上下文里的约束会随着距离变远而衰减。所以数据建模在AI协同里的真正作用是把你脑子里的隐性约定变成一段稳定的、存在于项目里的文本让AI每次读到代码时都能看到这份契约而不是依赖它记住上上轮你说过的话。这就是显性化的价值它不依赖AI的“记忆”只依赖AI的“阅读”。3. 我在分形项目里实际写下的那套“契约”长什么样3.1 分形项目的数据结构——先定义坐标与像素的分离我先把整套渲染管线的核心数据流列了出来。分形生成从输入到输出大致经过坐标映射、迭代计算、颜色映射、像素写入四个环节。为了让AI在任何一个环节都不会搞乱数据我把每个环节之间流动的“信封”单独定义成类型。第一组是视口与几何参数。我定义了一个不可变的Viewport它只负责描述“我们想看复平面上的哪一片区域”包含center_real、center_imag、half_width、half_height四个字段分别代表中心点的实部、虚部和半宽高。为什么不用bounding_box因为我发现AI在处理“半宽高”时不容易犯符号错误而处理“左上角右下角”时总是把缩放算反。第二组是颜色映射的控制点。我把它从渲染参数中单独拆出来定义一个ColorMap它的核心是stops数组每个stop包含position和color插值方法只能是线性或样条。这一步的建模决定了后续着色器代码能不能做到“参数与逻辑分离”——颜色怎么插值属于数据不属于算法。第三组是任务描述。RenderTask把一个完整的渲染请求封装起来它引用一个Viewport、一个ColorMap、一个最大迭代次数再加上输出图像的宽度和高度。第四组是像素结果。我不让AI把渲染结果直接做成一个“图像对象”而是用一个带坐标的PixelRecord结构字段是x、y、iterations、rgb。每个像素都是独立的分布式计算和分块渲染都基于这条记录展开。这套建模最核心的意图是把“几何世界”和“屏幕世界”彻底分开凡是描述复平面上坐标的都不允许混入图像宽高凡是描述输出图像的都不允许假设输入坐标。AI如果在此之前自己发挥特别喜欢在渲染函数里用屏幕像素坐标去推迭代初值导致放大之后画面错位。现在这条边界被数据模型直接锁死了。3.2 用不变式锁死AI的自由发挥空间光定义字段还不够因为AI可能给你传一个zoom0或者传一个空的颜色控制点数组然后它的代码在外面就炸了。我后来在契约里加了一块“不变式”部分这是数据建模里最有威力的几条约束。我规定Viewport的half_width和half_height必须同时为正ColorMap至少要有两个控制点RenderTask的max_iterations必须介于1和10000之间PixelRecord的rgb三个通道必须落在0到255闭区间。这些规则看起来简单但放到AI协同里效果非常明显——AI生成代码时会倾向于“不破坏规则”因为你在契约里已经用自然语言明确描述了“违反后会产生未定义行为”。我还把坐标映射的换算关系做成了契约里的公式注释复平面坐标等于center half_size * (pixel / resolution - 0.5)。我知道这个公式写进注释里会让AI生成的代码几乎没有第二种写法这正是我要的该统一的逻辑统一掉该自由的自有不要把算法的自由度浪费在这种容易出错的小地方。3.3 一份可以直接粘进prompt的契约文本我专门维护了一个CONTRACT.md文件放在项目根目录然后又从里面摘出一段精简版每次让AI写新模块时直接粘到prompt里。精简版长这样项目契约用于AI代码生成 1. 所有跨模块数据必须使用以下类型禁止在函数内部新造结构体 - Viewport: center_real:float, center_imag:float, half_width:float0, half_height:float0 - ColorMap: stops:List[ColorStop], interpolation:linear|spline - RenderTask: viewport:Viewport, colormap:ColorMap, max_iterations:int(1..10000), width:int, height:int - PixelRecord: x:int, y:int, iterations:int, rgb:tuple[float,float,float] 2. 复平面坐标换算统一使用z center half_size * (pixel / resolution - 0.5) 3. 渲染函数必须是纯函数只接收RenderTask返回List[PixelRecord]不得读写全局状态。 4. 禁止修改契约中已有字段的含义需要扩展时先讨论字段设计再写实现。这段文本最厉害的地方是第四条。它让AI在准备自己发明新结构的时候至少会先“询问”一下要不要绕开契约而不是直接自作主张。后面我实测下来AI生成时遵守契约的次数明显提升甚至有一两次它主动在注释里标注“此处按契约扩展了新字段”。4. 反面案例没有契约时AI悄悄替我做了多少决定4.1 一次翻车实录动画插值项目里的“结构失序”在认真建立契约之前我曾经让AI写过一个分形漫游动画模块。需求很简单给一组Mandelbrot缩放的关键帧按时间插值把每帧渲染出来。我当时的prompt很随意大概意思就是“帮我写一个分形漫游动画支持关键帧插值能输出图片序列”。AI很兴奋地开始了。它自己设计了一套FractalNavigator类负责存关键帧和当前时间一个Interpolator类负责做补间一个FrameCache类负责缓存已渲染帧还有一个AnimationRecorder负责把帧合成视频。这是典型的AI式过度设计类名精致、结构听上去合理、函数之间还有注释。真正的问题出现在数据格式上。FractalNavigator内部存的关键帧是嵌套字典里面既有浮点数列表又有字符串标记FrameCache缓存的对象直接复用了渲染输出的numpy数组没有定义任何边界最离谱的是Interpolator的插值结果会莫名其妙地和FrameCache的索引绑定在一起因为AI把“当前帧号”存在了FractalNavigator的同一个对象里。我当时没太在意觉得类名都挺一致的能跑就行。4.2 顺着内存暴涨追出来的排查链路跑了大概500帧之后内存开始以肉眼可见的速度膨胀动画帧率也掉得厉害。我先用tracemalloc做了个内存快照发现大量内存堆积在FrameCache里——这个缓存本来应该只保留最近5帧结果它的淘汰逻辑因为字段名写错从来没有触发过淘汰。再往下追问题更深。插值器输出的viewport.zoom是浮点数但FractalNavigator里存的是列表每次插值返回的都是新的列表对象而渲染器拿着这个列表再去做坐标映射出现了一个隐式的类型转换列表被当成数组除了一下中心点坐标全都偏移了几个像素。所以动画看起来在缩放实际在缓慢地漂移。这个漂移不是因为算法错误而是因为“坐标的对象”从没有在数据结构上被显式定义出来AI在每一个交叉口都替我做了一个它认为“理所当然”的决定。这次排错花了我整整一个下午。回过头看问题根源根本不在算法精度而在我没有给AI一份契约我没有说Viewport必须有四个明确命名的浮点字段没有说插值结果必须和缓存输入是同一个类型没有说动画模块之间只允许通过RenderTask传递数据。AI用它的“常识”把这种缺失补齐了而这种补齐害了我。4.3 典型症状对照表经过那一次我总结了一张“没有契约时AI代码会出现的典型症状”对照表现在做代码评审时随手就能用上症状本质原因契约缺失点同一份坐标数据在多个模块里类型不同AI在不同上下文里各自猜测字段类型没有定义跨模块的公共类型缓存、全局变量、对象内部状态纠缠AI为了省事把数据状态藏进类内部没有规定“跨模块数据必须显式流动”修改一个参数导致三个文件行为异常字段含义被AI在不同位置赋予不同解释没有给出字段语义的显式说明运行能过、结果缓慢漂移隐式类型转换或坐标单位混用没有锁死坐标换算公式每次重新生成代码结构都和前次不同AI每次都在重新猜测结构没有把契约作为prompt固定上下文这张表现在成了我和AI协作的标准检查项。凡是新模块跑完先不急着看功能先对表检查结构有没有失控。5. 把契约做成活文档校验器、样例与AI的配合流程5.1 契约不是纸面文章用校验器让AI看到约束契约文本写得好但AI毕竟是概率模型它可能在情绪高昂的时候忽略你写的那句“必须大于0”。所以我又做了一层代码层面的校验器把这些规则变成可执行的断言。我用Python的dataclass实现了这些类型然后在__post_init__里做检查。比如Viewport的校验逻辑from dataclasses import dataclass dataclass(frozenTrue) class Viewport: center_real: float center_imag: float half_width: float half_height: float def __post_init__(self) - None: if self.half_width 0 or self.half_height 0: raise ValueError(Viewport的半宽和半高必须为正数) if not all(map(lambda v: v v, [self.center_real, self.center_imag])): raise ValueError(中心点坐标不能为NaN)这个校验器的存在不是为了防AI是为了防我自己在跟AI协作时过于乐观。只要数据在进渲染函数之前先过一遍校验AI生成出来的调用点如果传了错误结构马上就会暴露。对一个动辄生成上百行代码的AI来说清晰可执行的报错信息比任何口头叮嘱都更有引导力。我还注意到一个细节把校验器写进项目代码之后AI在后续生成中会“读到”这些检查逻辑然后自动规避一些明显违反约束的写法。这等于把契约从prompt扩展到了代码环境里AI的注意力机制会自然被这些断言吸引。5.2 fixture样例与验收方式为了让AI更有效地理解契约我还在项目里建了一个fixtures目录放了三个经典样例一个标准Mandelbrot缩放视图中心位于复平面的某个经典位置一个Julia种子特写坐标围绕某个特殊值一个负空间边界视图专门测试坐标出界时渲染器会不会崩。这些样例不是用来做单元测试的是用来做“契约运行的图像化证明”的。每次让AI写完新功能我都要求它先跑一遍三个fixture样例把输出图片贴回来对照。为什么要这样做因为在跟AI协同的时候文字协议再完善也不如几张参考图稳定。AI在生成渲染代码时如果能对照“这个输入应该出这种图”它调整参数的行为会收敛得非常快。而且样例作为验收标准让“契约是否被遵守”有一个可检查的判据不是说“我觉得可以”而是“渲染结果和fixture参照图一致才算通过”。5.3 把契约放进prompt的顺序与表述模板我后来的口诀是先契约、再样例、后任务。prompt里前两段永远贴着契约文本和相关样例第三段才写具体功能需求。一个典型的prompt格式是请阅读项目契约见下方确保所有代码只使用契约内定义的类型。 参考fixtures目录下的三个样例场景如果你需要验证请先运行它们。 任务在RenderTask到PixelRecord的转换之间实现一个支持双线性插值采样的渲染器实现。 只修改渲染模块不要改动契约类型定义。这个顺序很关键。如果先把任务写成一大段AI很容易陷入对需求的自由解读等到提到契约时它已经决定了自己要用的数据形态。先契约、后任务AI会把“按契约实现”当成前置条件而不是可有可无的参考。我试过对比同一需求不带契约文本的prompt生成的代码结构合理率大概只有一半带契约的prompt基本每次都能保持同一套结构最多在函数命名上有点小差异。这就是“活文档”和“参考文档”的区别。6. 从分形实验到日常项目这套打法的适用边界与操作顺序6.1 什么情况下值得做数据建模因为分形项目的收益太明显我后来把这套“数据建模先行”的方法搬到了几个日常项目里包括一个图像处理Demo、一个跨平台小工具以及一个简单的日志分析系统。实践下来这套打法不是所有场景都值得上但有一个很清晰的判断标准只要AI需要写多个文件、状态流动超过三层、或者后续迭代超过一周数据建模就值得先做。反过来一次性脚本、纯算法改写、两个函数之间的简单转换就完全没必要建什么契约。那种场景AI的自由发挥反而更高效你给它一个输入文件它还你一个输出文件中间结构无所谓。数据建模投入产出比最低的地方就是“中间结果根本没有人看”的场合。还有一类项目建模的收益看起来很虚但实际很扎实当你需要连续多天与AI协作同一个项目时。第一天你制定了数据结构第五天你再让AI添加新功能它看到项目里已经有明确的类型定义和校验器就不会再重起炉灶。这个“长期记忆”不是靠对话历史而是靠代码里的契约本身延续的。6.2 操作顺序模型先行、验证跟进、功能最后我现在操办一个AI协同项目的基本顺序是四步每一期分形实验的迭代严格按这个顺序走效率高过之前随便开聊写数据模型确定所有跨模块流动的类型精确到字段名和类型允许暂时不写实现。写校验器把不变式和取值范围写成代码断言确保结构错误能在入口处暴露。写样例输入至少两个fixture覆盖最典型和最容易出错的输入。让AI实现功能把契约文件、样例说明、任务描述按顺序粘进prompt要求它不能改动契约定义。这个顺序的精髓是“结构先行于行为”。AI在前三步里没有收到任何诱导它自由发挥的信号等它终于开始写功能代码时它的结构选择已经被钉在了非常窄的范围内剩下的只是在填空和组合。它生成的代码会变得异常“听话”。6.3 让AI反提案再拍板的工作方式最后分享一个我后来养成的习惯。每次要新增跨模块的数据结构时我不是自己拍脑袋定好就扔给AI而是让AI先给我一份“结构提案”列出它认为需要的字段、类型和理由然后我来审阅、修改、拍板再把最终版写进契约文件。这个方法妙在两点。第一AI在海量代码语料里见过各种模式它提出的候选结构通常比我想得更全面尤其是边界字段很容易被它补上第二因为最终的契约是我确认过的一旦出了bug责任判定非常清楚——不是我没约束也不是AI瞎猜而是契约本身设计有缺陷修正方向明确。在分形项目的第六期里我就是用这套方式把整个渲染管线的数据流重新过了一遍。现在这个模拟项目终于长成了一个像样的架子渲染算法、颜色方案、动画脚本和调度代码各自独立数据流有迹可循任何模块都能单独替换。接下来我准备在这个基础上继续做第七期的景深效果实验这次不会再让AI替我决定场数据长什么样了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑