搜索/为多服务应用添加后端代码和合适的测试

为多服务应用添加后端代码和合适的测试

当 AI 智能体要修改一个拆分为多个服务、带后台工作线程的应用时使用。它会确认环境和正在运行的服务,把数据库代码和任务代码放到规定位置,按项目约定定义作业和错误,挑选一种合适的测试,并在写代码前先做计划。

要使用这个AI规则
## 开始工作前,准备好环境并确认服务正在运行 - 后端依赖位于仓库根目录下一个由锁文件管理的虚拟环境中。如果它尚不存在,请依据冻结的锁文件创建它,并在运行任何东西之前激活它。 - 测试所需的凭据和密钥位于仓库的本地环境文件中。请从那里读取,而不要自行编造取值或索要新的取值。 - 假定所有应用服务都已在运行,但要验证而非轻信:检查日志目录,确认相关服务仍在输出日志条目。 - 通过运行中的容器、使用项目文档记载的客户端调用方式访问数据库,而不要使用你自己临时拼凑的连接方式。 - 通过浏览器自动化操作 Web 应用时,使用端到端测试环境所创建的账号登录。如果该账号不存在,请通过注册页面注册它,因为第一个注册的账号会成为管理员。 ## 把数据库操作集中在一层,测试调用经由前端发出 - 把每一个数据库操作都放进指定的数据访问目录中。不要从其他任何地方执行查询,这样对表结构的了解就集中在同一层,并且可以在同一个地方接受评审。 - 在手动或脚本化测试中调用后端时,始终经由前端来源地址,而不要直接访问后端端口。绕过前端的请求同时也绕过了真实流量所经过的代理、会话和身份认证行为,因此得到的结果不具有代表性。 ## 把新任务放进匹配的后台工作线程池 异步工作被拆分到多个专用工作线程池中,而不是集中在一个通用队列里。请把新的工作放进特性与之匹配的那个池。 - 一个主池负责协调系统级操作:资源删除、同步、清理裁剪以及周期性检查。 - 抓取池从外部来源拉取数据,并为每一批次派生一个处理任务,同时带有看门狗监控,以便检测出停滞的数据源。 - 处理池运行数据摄取流水线:持久化记录、将其切分为分块、进行富化、生成嵌入向量、写入搜索索引并更新元数据。 - 轻量池以高并发处理短小的操作。重型池以低并发处理资源密集型的操作。 - 监控池是单线程的,因为监控不需要并行度,它收集队列深度、内存和健康度指标。 - 调度池掌管周期性触发器,从以秒计的间隔检查到按小时执行的清理,并为每个定时任务解析出对应的租户。 各个池共有的特性:使用线程池而非进程以获得稳定性、按租户隔离、优先级队列、心跳与存活检测、自动重试与故障恢复、用于进程间协调的快速共享存储,以及用于持久化任务状态的关系型数据库。 ## 为每个后台任务设置过期时间,并在任务内自行实现超时 - 使用框架中与具体应用无关的装饰器来声明任务,而不要把它们绑定到某个具体的应用实例上,这样导入一个任务时就绝不会连带触发应用的构建。 - 把任务模块放在与其版本或层级相对应的指定任务目录下。 - 绝不要在没有过期时间的情况下将任务入队。无论任务是由调度发出还是由另一个任务发出,都要提供过期时间。提交没有过期时间就入队的代码是不可接受的,因为这会让队列无限制地增长。 - 由于任务在线程池中执行,框架自带的时间限制功能会被静默禁用。请在任务内部自行实现超时逻辑。 - 工作进程不会在代码变更时重新加载。修改工作进程代码后,请让维护者在测试前重启工作进程,并且不要假定新代码已经生效。 ## 添加端点,并用项目自己的错误类型抛出错误 - 添加端点时,直接为处理函数标注类型,而不要单独声明一个响应模型字段。 - 抛出项目自有的错误类型,而不是框架的通用 HTTP 异常;并且绝不要硬编码状态码,也不要从框架中导入状态码常量。 - 一个全局处理器会把项目错误转换成统一的标准响应体,其中携带一个错误码和一段详情。这消除了样板代码,并让整个后端的错误处理保持一致。 - 错误码来自一个中央注册表。如果需要新的类别,先把它加入注册表;绝不要在调用处凭空编造一个错误码。 - 当转发上游故障、且其状态码来自上游响应时,请通过该错误类型的状态码覆盖参数把状态码透传出去,而不要因此放弃使用该错误类型。 ## 保持严格类型,只写长期有用的注释 - 在提出变更之前,安装并对所有文件运行仓库的提交前钩子。 - 在后端和前端都保持严格的类型标注。不带类型的逃生通道不是可以接受的捷径。 - 保持注释简短,只写那些长期仍然成立的信息。不要写叙述本次变更过程的注释,例如刚刚新增了什么、删除了什么或重构了什么;变更一旦合入,这些注释立刻就变成了噪音。 ## 手写数据库迁移,并同时应用到默认模式和私有模式 - 用迁移工具生成版本文件,然后在它创建出来的文件里手写迁移主体。不要依赖自动生成来表达意图。 - 多租户部署除了默认的模式配置外,还需要针对私有模式配置应用该迁移;在创建版本文件时同样存在这一区分。 - 把迁移当作本次受评审变更的一部分,而不是事后补做的附加工作。 ## 少用模拟,挑选一种能证明改动的测试 一共存在四个层级。相比大量使用模拟,优先选择保真度更高的层级,并挑选那个真正能证明该行为的层级。 - 单元测试假定没有任何服务可以访问,并模拟与外部世界的每一次交互。只为那些逻辑本身值得单独固定下来的、复杂且独立的模块编写单元测试。 - 依赖支撑的单元测试假定外部依赖正在运行——数据库、缓存、对象存储、搜索索引、对外网络——而应用容器并未运行。函数被直接调用,并且部分组件仍可以被模拟。使用它们来尽量减少模拟,同时保留控制不稳定行为的能力,或者验证某个内部调用是否以特定参数发生过——这是纯粹的集成测试做不到的。 - 集成测试针对真实部署运行,不模拟任何东西。优先使用这类测试;只有在确实需要模拟或需要内部验证时,才退而使用依赖支撑的单元测试。 - 浏览器驱动的测试在完整部署之上再加上 Web 服务器。当某个行为需要前端与后端之间大量协同时,使用这类测试。 不要过度测试。一次变更通常只需要一种测试,而不是四种全都要。 ## 编写集成测试时,复用现有的测试夹具和辅助函数 - 编写集成测试之前,先阅读根测试配置以了解可用的测试夹具(fixture),并阅读共享工具目录以了解可用的辅助函数。 - 相比用原始 HTTP 库直接调用端点,优先使用这些工具里的管理器辅助函数;相比手工调用这些辅助函数,又优先使用测试夹具。在已经存在管理员测试夹具的情况下手动创建管理员,会重复搭建过程,并与测试套件的其余部分产生偏离。 - 测试是按目录级别并行执行的,因此同一目录内的文件之间不得依赖共享的可变状态。 - 当测试发起真实的模型调用时,请使用各家提供方的低价高速档位模型,而不是旗舰模型。测试成本会随着套件规模而增长,而所要验证的内容并不取决于模型能力的强弱。 - 照搬同一类别中现有典范测试的写法结构,而不要另创一套新结构。 ## 推测失败原因之前,先读各服务的日志 - 编写集成测试或手动驱动运行中的系统时,请阅读日志目录中按服务划分的调试日志文件。每个服务都会把它的输出实时写到那里。 - 在对某个故障形成推测之前,先读日志。在读日志之前,某个无关的原因(比如某个服务并未运行)看起来和真正的缺陷一模一样。 ## 不把 API 密钥提交到仓库,并保护新的端点 - 绝不要把 API 密钥或机密信息提交到仓库中。 - 通过加密的凭据存储来保存集成凭据,绝不要放在明文配置里。 - 添加功能时遵循既有的基于角色的访问控制模式。新的端点默认不会继承任何保护。 - 在边界处使用模式模型(schema model)校验输入。 - 使用参数化查询,使注入在结构上就不可能发生;绝不要用字符串拼接来组装查询。 ## 为每次 AI 模型调用打上已注册的操作名标签以便追踪 对语言模型、嵌入模型、重排序器、图像生成器、语音组件或意图分类器的每一次调用,都必须开启一个生成(generation)跨度,并用中央流程注册表中的取值为其打标签。请使用与调用路径相匹配的封装:一种用于经由模型抽象层发起的调用,另一种用于绕过该抽象层的、直连提供方或直接的 HTTP 调用。 - 在为一个新操作埋点之前,先向注册表添加一个新的取值。绝不要把原始字符串作为标签传入。 - 标签命名的是操作本身,比如重排序或图像编辑,而不是提供方。提供方信息属于模型配置的元数据。 - 自动回退机制会为那些到达抽象层却没有显式跨度的调用发出未打标签的哨兵值。这些哨兵值在仪表盘中可见,意味着埋点缺失。请修复调用处,而不要依赖这个回退机制。 ## 先研究代码,再写不含时间表和代码的实施计划 先调研,再动笔。首先探索代码库中的相关部分;基于臆测写出的计划比没有计划更糟糕。 一份计划至少包含以下要素: - 要解决的问题:这次变更打算做到什么。 - 重要提示:调研过程中发现的、对实施有影响的事项。 - 实施策略:让这次变更得以落地的高层方案。 - 测试:哪些测试将验证行为的正确性——要经过刻意选择并保持精简。 不要包含时间表章节,也不要包含对回滚步骤的论述。不要在计划中编写代码;保持在高层次,不过引用具体的文件或函数是有用的。这只是一份最低限度的清单,因此在有帮助时可以增加更多内容。 ## 在子项目中工作前,先读该子项目自己的规则 - 根目录的指引涵盖共通事项。每个子项目在自己的代码旁保留各自的标准文档,该文档管辖它自己的目录树。 - 在某个区域开展工作之前,先阅读该作用域的文档。不要假定某个客户端的规则也适用于另一个客户端:一个没有文档对象模型的移动端客户端,其样式基元的间距取值是绝对值而非相对的步进刻度,并且拥有自己的路由和视图基元,它所遵循的规则与 Web 客户端不同。 - 更广泛的工程实践记载在仓库的贡献指南中。除本文档之外,也要阅读并遵循它。