Skip to content

第 13 章 · 工程严谨性

pixiu 锚点:db/engine.py(双引擎)、docs/lab/Q1(CI)、docs/lab/Q2(空壳识别) 关键数据:ruff 债务 415→0、GitHub Actions 45s 全绿 83 passed

开篇:不性感,但保命

前面十二章,讲的都是 agent 工程的"性感"部分——prompt、工具、循环、eval、熔断。这一章讲不性感的:架构、CI、lint、状态管理、配置分层

但恰恰是这些不性感的东西,决定了一个 agent 是"玩具"还是"产品"。pixiu 在这些事上吃过亏、也补过课,这一章把这些底线讲清楚。它们不难,但容易被忽视,而忽视的代价是——你的 agent 会在某个深夜以一种你完全料不到的方式挂掉。

双引擎架构:让数据分层落地

先讲架构。pixiu 是个有"共享模式"的产品——我自己维护一个中央市场库(所有股票数据),朋友连上来只读共用市场库 + 各自的私有库(持仓、报告)。这个需求,催生了 pixiu 最硬核的一个架构设计:双引擎路由

实现的核心在 db/engine.py,就一行关键的:

python
def get_session_factory(config):
    market_eng = get_market_engine(config)   # 中央市场库
    user_eng = get_user_engine(config)       # 用户私有库
    sf = sessionmaker(binds={
        MarketBase: market_eng,   # MarketBase 的模型 → 中央库
        UserBase: user_eng,       # UserBase 的模型 → 用户库
    })
    return sf

精妙在哪?每条 SQL 语句,按模型所属的基类(MarketBase / UserBase),自动路由到对应的数据库引擎。 你写 session.query(StockDaily),它自动查中央库;写 session.query(Position),自动查用户库。业务代码完全不用关心"这个表在哪个库",路由是自动的。

而且它有个优雅的退化:单人模式(没配 market_database)下,两个引擎指向同一个库,行为和单库完全一致。也就是说,同一套代码,零配置是单机,配一下就变共享——不需要改业务代码。

这个设计还有一个细节:双层缓存(engine 按 url 缓存、session factory 按 engine 对缓存)。因为创建 engine 很贵(要建连接池),不能每次请求都建。这些都是工程上不起眼但必须做对的事。

双引擎是"架构随需求演进"的范本——从单机到共享,不是推倒重来,而是在一开始就把"市场数据 vs 用户数据"的分层想清楚(两套声明基类),后面加共享模式只是接上第二个引擎。

CI:单人项目也值得有(Q1)

接下来讲一件很多人觉得"没必要"的事——单人项目的 CI

pixiu 原来没有 CI,连 lint 都没跑过。pyproject.toml 里声明了 ruff,但环境里根本没装——典型的"配了不跑"。我专门做了个课题 Q1 验证单人 CI 的价值,结果很打脸(打的是"单人不需要 CI"的脸):

第一步,装 ruff,跑一遍——415 条历史债务。这个数字说明,"靠人自觉写规范"是不靠谱的,债务会无声无息地累积。

第二步,清债,415 → 0。清的过程还顺手抓出几个真 bug(下面讲)。

第三步,加 GitHub remote + Actions。45 秒跑完,83 项测试全绿。

Q1 的结论是:单人 CI 的价值,不在"防止别人搞坏",而在"防止自己搞坏 + 强制执行规范 + 抓静默失败"。 没有它,"配了不跑""lint 形同虚设"这些问题永远潜伏。

静默失败是最危险的失败(Q1-1)

CI 抓出的 bug 里,最典型的一个是 Q1-1,值得单独讲。

pixiu 的 web.py 里有段代码,藏着一个 except: pass(裸捕获、静默吞掉)。它掩盖了一个事实:代码里用到了一个未定义的变量 config。因为 except: passNameError 也吞了,这段"补名称"的功能永远在静默失败——不报错、不告警,但功能就是死的。

这种 bug 最可怕:它不让你知道它坏了。 显式报错你早修了;静默失败会让你以为功能在跑,直到某天发现"咦这个功能怎么一直没生效"。

修法是改用 get_config(),而这个 bug 是 CI 的 ruff F821(未定义变量)规则抓出来的。这就是 CI 的价值——它能在你不知道的地方,把静默失败揪出来。

所以这一章最想强调的一条工程纪律是:永远不要 except: pass 你要么处理异常,要么记日志,要么上抛,但绝不能假装它没发生。静默失败是工程债务里最阴险的一种。

空壳识别:功能不是写了就算数(Q2)

讲完 CI,讲一个比 CI 更需要主动性的方法论——空壳识别(课题 Q2)。

pixiu 巡检时,我列了一份"待核实清单":

  • scheduler 是否真被启动?(定义完整,但 start() 调没调用?)
  • alert → 钉钉链路是否真通?
  • conversation 持久化是否真接入 agent 循环?
  • report_qc 是否在所有报告路径启用?

这些功能的共同点是:代码读着像那么回事,但调用链可能是断的。 写了一个 ConversationStore,但 agent 循环没调它,对话功能就是个空壳。这种"伪连接"不会报错,所以 CI 抓不到——只能靠人(或 agent)系统核实调用链是否闭合

pixiu 用的是"系统识别法"——逐个追调用链。核实结果:conversation 连续性其实已通(Q2-1 降级)、report_qc 已全覆盖(Q2-2 关闭),但也确认了哪些是真接通的、哪些需要补。这个方法的价值不在找出多少空壳,在于建立"功能要核实才算数"的纪律。

你做自己的 agent 时,值得建一份待核实清单:你以为在跑的功能(记忆、通知、持久化、质检),逐个验证调用链闭合。这是 CI 之外的、针对"逻辑空壳"的第二道防线。

状态持久化:别让设置重启就丢(W5-2)

一个具体的小坑,标注 W5-2:pixiu 的 toggle_pipeline(开关某条定时管线)原来只改内存里的一个 dict,重启服务后状态 reset——用户关掉的管线又自己启用了。

这种 bug 的特征是:单测测不出来(单测里改完就读,不会重启),只有真实部署才发现。修法是加 _load/_save_enabled_state,把开关状态持久化到 data/scheduler_state.json,启动加载、toggle 写入。

教训是:任何"用户改了就期望保留"的状态,都要持久化。 内存里的状态是临时的,重启即失。配置、开关、偏好这类,都得落到文件或 DB。

死代码:该删就删(W1-7)

最后讲一个工程卫生细节,W1-7memory.py 里有个 save_summary() 函数,全代码库没有任何地方调用它。更糟的是,read_recent() 还在调用"读取 save_summary 该写的文件"——但 save 是死的,read 永远读空。整条路径是个 no-op,早被 DB 版的记忆(第 5 章 _build_memory_context)取代了,但死代码还留着,误导后来人。

修法:删 save_summaryread_* 留注记说明"恒返回空,待清理"。死代码不只是占地方,它会制造"这条路径还在工作"的错觉。 定期清理,是工程卫生的基本功。

配置三层:东西各归其位

pixiu 的配置分三层,是个值得借鉴的组织方式:

放什么位置
.env基础设施(DB 连接、密钥、端口)gitignored
config.yaml业务调参(策略、cron、模型)gitignored,UI 可编辑
config.defaults.yaml业务调参模板进 git

原则是:密钥和基础设施走 .env(只读),业务参数走 yaml(可调、有默认模板),模板进 git 让别人能复现。 而且 env 优先级高于 yaml——被 env 覆盖的 yaml 字段,UI 里自动转只读,避免"两处都能改、改了不一致"。这种"各归其位 + 明确优先级"的配置分层,能省掉无数"为什么我改了不生效"的坑。

这一章的工具:工程严谨自检

  • [ ] 你的架构有没有为"未来的演进"留分层?(像双引擎那样,单人能用、共享也能用)
  • [ ] 你有 CI 吗?哪怕单人?(没有的话,415 条债务正在无声累积)
  • [ ] 你代码里有 except: pass 吗?(有就改成处理/日志/上抛)
  • [ ] 你以为"在跑"的功能,核实过调用链闭合吗?(空壳识别)
  • [ ] 用户改的状态,重启会丢吗?(该持久化的都持久化)
  • [ ] 有没有死代码在制造"还在工作"的错觉?

小结

工程严谨性,全是些不性感的事——双引擎分层、CI、lint、空壳识别、状态持久化、配置分层、死代码清理。但它们是 agent 从"demo"走向"产品"的底座。

pixiu 的经验是:这些事不能靠"以后再说",它们会以"静默失败""空转一整夜""重启丢配置"的形式,在你最没防备的时候咬你一口。早做这些"无聊"的事,是省下未来无数个深夜的最高 ROI 投资。

下一章

第 14 章 · 驾驭曲线 —— 你和 agent 的协作,处在哪个阶段?