1. 先把Codex这个词拆开看它到底指什么很多人第一次听到Codex本地部署脑子里浮现的画面是下载一个叫Codex的模型文件丢进显卡里跑起来然后就能写代码了。这个理解有一半对一半错得离谱。Codex这个词在不同语境下指的东西完全不一样。最早它是某代码托管平台推出的一套代码补全能力背后挂的是特定的大模型后来它演变成一个泛指——凡是能根据自然语言生成代码的服务大家习惯性都叫它Codex。所以当有人说我要本地部署Codex他真正想表达的其实是我要在本地搭一套代码生成服务输入需求描述输出可用的代码。这里有个关键认知必须先建立起来代码生成服务不等于模型本身。模型只是其中一层而且是相对死的一层。真正决定这套服务好不好用的是模型外面包裹的那两层——接口层和编排层。我见过太多人把全部精力砸在选哪个模型上结果服务搭起来之后响应格式乱七八糟、上下文管理一塌糊涂、并发一上来就崩最后得出结论说本地模型不行。其实不是模型不行是架构没搭对。这篇文章要讲的三层架构就是把这套服务拆成三个职责清晰的层模型推理层、服务接口层、业务编排层。三层各管各的事层与层之间通过明确的契约通信。这样拆的好处是你想换模型只动第一层想换接口协议只动第二层想改业务逻辑只动第三层。任何一层出问题排查范围立刻缩小到三分之一。适合读这篇的人有三类一是想在自己机器上跑一套私有代码生成服务的开发者二是团队里被安排去调研本地AI编码方案的技术负责人三是已经跑通了某个模型但服务化之后各种别扭、想搞清楚问题出在哪的人。不管你是哪一类接下来的内容都会从为什么这么分层讲到每一层具体怎么落地中间穿插我实际踩过的坑。提示本文讨论的是本地部署场景下的服务架构不涉及任何网络访问工具或代理配置。所有组件均在你自己的机器或内网环境中运行。2. 为什么是三层而不是两层或四层2.1 两层架构的典型翻车现场最常见的两层做法是模型直接挂一个HTTP接口业务代码直接调这个接口。看起来简单实际上问题一大堆。我最早做的一版就是这样用推理框架起一个服务暴露一个生成接口然后写个脚本往里发prompt。刚开始单条测试没问题一旦要处理稍微复杂的任务——比如根据这个接口文档生成对应的数据模型和CRUD代码——就发现两个致命问题。第一prompt的组装逻辑散落在业务代码各处改一个格式要翻遍整个项目第二模型返回的是纯文本业务侧要自己解析、自己判断生成结果是否完整、自己处理截断这些逻辑和业务逻辑搅在一起代码很快就变成一团乱麻。两层架构的本质问题是它把和模型打交道这件事当成了业务逻辑的一部分。但这两件事的变更频率完全不同。模型可能一个月换一次prompt模板可能一周调一次而业务逻辑相对稳定。把它们耦合在一起等于让稳定的东西跟着不稳定的东西一起改。2.2 三层各自的职责边界三层架构的核心思路是让每一层只关心一件事。模型推理层只负责一件事给定输入文本返回输出文本。它不关心这个文本是代码还是散文不关心调用方是谁不关心业务规则。这一层的产物是一个稳定的、纯粹的文本进文本出的服务。服务接口层负责把推理能力包装成规范的API。它处理的是请求格式校验、参数标准化、超时控制、错误码定义、流式输出的分块传输。这一层让上层调用者不用关心底层用的是哪个推理框架、哪个模型。业务编排层负责真正的代码生成业务。它决定用户的需求怎么拆解成多个prompt、生成结果怎么拼接、多轮生成怎么管理上下文、生成出来的代码怎么做基础校验、失败怎么重试。这一层是真正体现代码生成智能体能力的地方。三层之间的契约非常清晰推理层对接口层暴露生成能力接口层对编排层暴露标准化的生成API编排层对最终用户暴露代码生成任务。2.3 和MVC三层架构的区别在哪有人会问这不就是MVC吗不是。MVC的Controller、Service、DAO是围绕数据组织的核心是数据的增删改查和业务规则。而这里的三层是围绕推理组织的核心是文本的生成和编排。DAO层对应的是数据库而推理层对应的是模型——模型不是数据库它是有状态、有随机性、有上下文长度限制的。这个区别很重要。数据库查询是确定性的同样的SQL永远返回同样的结果模型推理是概率性的同样的输入可能返回不同的输出。所以推理层的设计必须考虑重试、采样参数、结果缓存这些数据库层不需要考虑的东西。把模型当数据库用是新手最容易犯的错。3. 模型推理层选型、量化与显存账3.1 本地推理框架怎么选本地跑模型绕不开推理框架的选择。目前主流的有几类一类是通用推理服务框架一类是轻量级本地运行工具还有一类是面向特定硬件的优化方案。选型的核心考量不是哪个跑分高而是哪个和我的部署环境最匹配。如果你只有一张消费级显卡显存有限那优先考虑支持量化加载、显存占用可控的方案。如果你有服务器级的多卡环境那可以考虑支持张量并行的方案。我个人的经验是先用最轻量的方案把链路跑通再根据瓶颈决定要不要换重型方案。很多人一上来就追求最优性能结果环境配置卡了三天链路还没通。先用一个能跑起来的小模型把三层架构搭通确认接口和编排逻辑没问题再换大模型这样风险最小。3.2 量化等级对代码生成质量的实际影响量化是本地部署绕不开的话题。简单说量化就是把模型权重从高精度比如16位浮点压缩到低精度比如4位整数好处是显存占用大幅下降代价是精度损失。对于代码生成任务量化带来的影响比通用对话更敏感。原因是代码对精确性要求极高——一个变量名拼错、一个括号位置不对整个代码就跑不起来。我在实测中对比过同一模型的不同量化等级结论是量化等级显存占用相对代码语法正确率适用场景16位100%最高显存充足追求质量8位约55%接近16位平衡之选4位约30%明显下降显存紧张简单任务3位及以下约22%下降严重不推荐用于代码生成这个表不是绝对的不同模型对量化的敏感度不一样。但大方向是代码生成任务尽量用8位以上4位是底线再低就别指望生成能直接用的代码了。3.3 显存够不够先算一笔账很多人问我这个显卡能不能跑某个模型其实可以自己算。模型显存占用大致等于参数量 × 每参数字节数 上下文缓存。举个例子一个70亿参数的模型用8位量化每参数1字节权重占约7GB。上下文缓存取决于上下文长度和批大小假设上下文长度4096、批大小1缓存大概1到2GB。再加上推理框架本身的开销总共需要约10GB显存。一张12GB的卡能跑8GB的卡就悬了。如果是4位量化权重降到约3.5GB加上缓存和开销6GB左右8GB的卡就能跑。这就是为什么量化对本地部署这么重要——它直接决定了你的硬件能不能用。注意上面是粗略估算实际占用受推理框架、是否启用KV缓存优化、批处理策略等影响。建议先用小模型实测观察实际显存占用再决定上多大的模型。3.4 推理层的接口设计要点推理层对外暴露的接口我建议保持极简一个生成接口接收prompt和采样参数返回生成文本。不要在这一层做任何业务判断。采样参数至少要暴露这几个温度控制随机性、最大生成长度防止无限生成、停止词控制生成边界。温度对代码生成很关键——温度太高生成的代码天马行空但不可用温度太低生成的代码千篇一律缺乏灵活性。代码生成一般建议温度在0.2到0.5之间具体看任务。还有一个容易被忽略的点推理层要支持流式输出。代码生成往往比较长如果等全部生成完再返回用户等待时间会很长。流式输出可以让用户边生成边看到结果体验好很多。但流式输出对接口层的设计有要求这个后面讲。4. 服务接口层用FastAPI把推理能力包装成规范API4.1 为什么选FastAPI接口层框架的选择我推荐FastAPI理由有三个。第一它对异步的支持非常自然。代码生成是典型的IO密集型任务——大部分时间在等模型推理CPU是闲着的。用异步框架可以让多个请求在等待推理时互相不阻塞并发能力比同步框架高一个量级。第二它自带请求校验和文档生成。你定义好请求体的数据结构它会自动校验入参、自动生成交互式文档。这在调试阶段特别省事不用自己写一堆校验代码。第三它的流式响应支持很成熟。代码生成需要流式输出FastAPI的流式响应机制用起来很顺手。当然如果你团队更熟悉别的框架用别的也行。核心不是框架本身而是接口层要承担的职责。4.2 接口层的目录结构一个清晰的目录结构能让接口层的职责一目了然。我常用的结构是这样的service/ ├── main.py # 应用入口注册路由 ├── routers/ │ ├── generate.py # 代码生成相关路由 │ └── health.py # 健康检查路由 ├── schemas/ │ ├── request.py # 请求体数据结构 │ └── response.py # 响应体数据结构 ├── clients/ │ └── inference.py # 调用推理层的客户端 ├── core/ │ ├── config.py # 配置管理 │ └── errors.py # 错误码定义 └── middleware/ └── logging.py # 请求日志中间件这个结构的关键在于clients目录——它把调用推理层这件事封装成一个客户端类路由层不直接和推理层打交道而是通过这个客户端。这样以后推理层换了实现只要客户端接口不变路由层完全不用动。4.3 请求与响应的数据结构设计接口层的请求体我建议至少包含这几个字段任务描述、目标语言、上下文代码可选、采样参数可选。响应体则包含生成结果、是否完整、耗时、使用的模型标识。这里有个设计决策值得说生成结果要不要在接口层做结构化。我的建议是接口层只做最基础的结构化——把生成文本、状态、元信息分开但不要试图在接口层解析代码结构。代码结构解析是业务编排层的事接口层管太宽会导致职责混乱。流式输出的响应设计要特别注意。流式场景下响应不是一次性返回的而是一块一块推的。每一块应该是一个独立可解析的单元通常用JSON行格式或者特定的分隔符。我踩过的坑是早期用纯文本流式输出结果前端拿到的是连续的字符串没法区分这一块生成完了和还在生成中。后来改成每个块带一个类型标记前端就能正确处理了。4.4 超时、重试与错误码接口层必须处理超时。模型推理可能很慢尤其是长代码生成一次请求几十秒很正常。接口层要设置合理的超时时间并且区分推理超时和连接超时——前者说明模型在跑但没跑完后者说明推理服务可能挂了。重试策略要谨慎。代码生成不是幂等操作重试可能产生不同的结果。我的做法是只在连接失败这种明确的可重试错误上重试推理超时不自动重试而是返回给上层让业务层决定。错误码要定义清楚。至少区分这几类请求格式错误、推理服务不可用、推理超时、生成结果为空、内部错误。每类错误对应不同的HTTP状态码和错误信息方便上层和前端处理。# 错误码定义示例 class ErrorCode: INVALID_REQUEST (E1001, 请求参数不合法) INFERENCE_UNAVAILABLE (E2001, 推理服务不可用) INFERENCE_TIMEOUT (E2002, 推理超时) EMPTY_RESULT (E2003, 生成结果为空) INTERNAL_ERROR (E9999, 内部错误)4.5 并发控制与限流本地推理服务的并发能力是有限的。一张显卡同时只能处理有限的请求请求排队太多会导致每个请求都变慢。接口层必须做并发控制。最简单的做法是用信号量限制同时进行的推理请求数。超过限制的请求要么排队要么直接返回服务繁忙。排队的好处是不丢请求坏处是用户等待时间不可控。我的经验是设置一个合理的队列长度超过就拒绝这样比让所有请求都慢吞吞地排队体验更好。限流还要考虑单用户的频率限制。如果某个用户疯狂发请求会挤占其他人的资源。简单的做法是按IP或按token做频率限制超过阈值就拒绝一段时间。5. 业务编排层让代码生成真正可用5.1 编排层到底在编排什么如果说推理层是发动机接口层是传动轴那编排层就是方向盘和变速箱。它决定这套服务往哪走、怎么走。编排层要处理的事情包括把用户的自然语言需求拆解成模型能理解的prompt、管理多轮生成的上下文、把多次生成的结果拼接成完整代码、对生成结果做基础校验、失败时决定重试还是降级。举个具体例子。用户说帮我写一个用户登录接口包含参数校验和错误处理。这句话对模型来说太笼统了直接丢给模型生成的质量参差不齐。编排层要做的是把它拆成几个子任务先确定技术栈和框架再生成接口定义再生成参数校验逻辑再生成错误处理最后组装。每个子任务用针对性的prompt生成质量会稳定很多。5.2 Prompt模板的管理编排层最核心的资产是prompt模板。这些模板不应该硬编码在代码里而应该独立管理方便调整和版本控制。我习惯把prompt模板放在独立的配置文件或目录里每个模板有明确的用途说明和变量占位符。比如prompts/ ├── generate_function.txt # 生成单个函数 ├── generate_class.txt # 生成类定义 ├── generate_tests.txt # 生成测试代码 ├── refactor.txt # 重构现有代码 └── explain.txt # 解释代码逻辑每个模板文件里用占位符标记可变部分编排层在调用时填充。这样做的好处是调整prompt不用改代码非技术人员也能参与优化不同模板可以独立迭代互不影响。5.3 上下文管理与截断策略代码生成经常需要上下文——比如在这个类的基础上加一个方法模型需要看到现有的类定义。但模型的上下文长度是有限的上下文塞太多会挤占生成空间塞太少模型又缺乏必要信息。编排层要做的上下文管理包括决定哪些上下文必须带上、哪些可以省略、超长时怎么截断。我的策略是优先级排序当前编辑的文件内容优先级最高相关的接口定义次之项目整体结构再次之。超长时从低优先级的开始截断。截断有个技巧不要简单地从中间截断那样会破坏代码结构。更好的做法是按语法单元截断——比如按函数、按类截断保证截断后的上下文仍然是语法完整的。5.4 生成结果的校验与修复模型生成的代码不能直接信任必须校验。编排层要做的校验至少包括语法是否合法、括号是否匹配、引用的变量是否定义、导入是否完整。语法校验可以用对应语言的解析器。比如生成的是Python代码就用Python的ast模块解析一遍解析失败说明语法有问题。这一步能拦掉相当一部分明显错误的生成结果。对于校验失败的结果编排层可以尝试自动修复。最简单的修复是把错误信息连同原代码一起再发给模型让它修正。这个生成-校验-修复的循环可以跑两到三轮大部分语法错误能在这一过程中被修掉。但要注意修复循环要有上限不能无限重试。我见过有人设置成直到校验通过为止结果遇到模型死活修不对的情况请求就卡死了。设置最多三轮三轮还不行就返回给用户附上错误信息。5.5 多轮生成的结果拼接复杂代码往往需要多次生成再拼接。拼接不是简单地把字符串连起来要考虑导入语句要不要合并、命名冲突怎么处理、代码风格是否一致。我的做法是先生成一个骨架确定整体的结构、导入、命名规范然后各个部分在这个骨架的约束下生成。这样拼接时冲突会少很多。拼接完成后再整体过一遍格式化工具统一代码风格。6. 三层之间的通信与联调6.1 层间契约的稳定性三层架构能不能发挥优势关键在于层间契约是否稳定。契约不稳定改一层就要动其他层分层就失去了意义。我的经验是层间契约一旦确定就要像对待公开API一样对待它。推理层对接口层的契约是生成接口的输入输出格式接口层对编排层的契约是生成API的请求响应格式。这些格式的变更要走版本管理不能随意改。具体做法是给契约加版本号。比如推理层的生成接口路径带上版本接口层的API也带版本。新版本上线时旧版本继续保留一段时间给调用方迁移的时间。6.2 联调时最容易出问题的地方联调阶段最容易出问题的地方我总结下来有三个。第一个是编码问题。模型输出的文本编码、接口传输的编码、业务层处理的编码任何一环不一致都会导致乱码。尤其是代码里包含中文注释时编码问题特别容易暴露。统一用UTF-8并且在每一层的边界都显式声明编码。第二个是流式输出的边界处理。流式输出时一个完整的生成结果会被切成多个块。如果切分点正好在一个多字节字符中间或者在一个JSON结构中间接收方解析就会出错。解决办法是在接口层做缓冲保证每个块都是完整的可解析单元。第三个是超时时间的层层传递。编排层调接口层有超时接口层调推理层也有超时。如果编排层的超时比接口层的短那接口层还在等推理编排层已经超时返回了造成资源浪费。超时时间要逐层递减外层比内层留出更多余量。6.3 日志与可观测性三层架构的排查没有日志寸步难行。每一层都要有清晰的日志并且日志要能串联起来——同一个请求在三层里的日志要能通过一个请求ID关联。我的做法是在接口层生成一个请求ID通过请求头传递给推理层编排层也记录这个ID。这样出问题时拿请求ID一搜三层日志全出来了问题出在哪一层一目了然。日志内容要包括请求参数、响应状态、耗时、错误信息。但要注意代码内容可能很长全量记录日志会撑爆磁盘。我的做法是记录代码的长度和摘要完整内容只在调试模式下记录。7. 实际部署中的那些坑7.1 模型加载慢导致的启动超时本地部署时模型加载可能要几分钟。如果接口层启动时就要求推理层可用那接口层会一直启动失败。解决办法是让接口层和推理层解耦启动——接口层先起来推理层在后台加载加载完成前接口层返回服务初始化中。7.2 显存碎片导致的间歇性失败长时间运行后显存可能出现碎片导致原本能跑的任务突然失败。这个问题很隐蔽因为重启服务就好了但跑一段时间又出现。缓解办法是定期重启推理服务或者在推理层做显存整理。更彻底的办法是控制并发数避免显存占用忽高忽低。7.3 生成结果不稳定同样的输入有时候生成质量好有时候差。这通常是采样参数的问题。温度设置过高会导致随机性太大。解决办法是把温度调低并且对关键任务使用固定的随机种子保证结果可复现。7.4 长代码生成被截断模型有最大生成长度限制超过就会被截断。生成一个几百行的文件时很容易触发。解决办法是在编排层做分段生成——把大文件拆成多个部分分别生成再拼接。或者用支持更长上下文的模型。8. 写在最后的一点个人体会这套三层架构我前前后后迭代了好几版最大的体会是本地部署代码生成服务难点从来不在模型本身而在模型外面那两层。模型是现成的下载下来就能跑但怎么把它包装成一个稳定、好用、可维护的服务这才是真正花时间的地方。如果你正准备动手我的建议是先用最小的模型把三层链路跑通哪怕生成的代码质量很差也没关系重点是验证架构。链路通了之后再逐步换更大的模型、优化prompt、完善校验逻辑。这样每一步都有反馈不会卡在某个环节动弹不得。还有一点不要追求一步到位。我见过有人想一次性把并发、缓存、限流、监控全做上结果哪个都没做好。先把核心链路做扎实其他的按需加。服务是长出来的不是设计出来的。