我把一套完整的 Python Agent 运行时塞进手机 APK:依赖闭包、Rust 轮子和三个真机坑

手机侧此前是一份只用标准库的薄内核,和 PC 上游双份维护迟早漂移;把完整内核装进去、又不改上游一行,得真机验证才敢拍板。这篇记从可行性 spike 到真机验收的攻坚,以及真机上炸出来的坑。

TL;DR

  • 做了什么 :把完整的 Python Agent 内核(fastapi / uvicorn / openai-agents 那一套真身,非裁剪版)打进 APK,在手机里以环回 HTTP 服务跑起来,零上游改动。
  • 结果 :真机验收通过------环回 chat 全链路跑通(BYOK deepseek-v4.1-flash 真实应答 + SSE 推流)、task 模式 agent 循环真实运行、工具面 20 个(9 个手机 GUI 原语 + 上游 core)。Phase 0 可行性 spike 全程 4.4s。
  • 代价 :依赖闭包攻坚------5 个自编 Rust wheel + 1 个社区现成 wheel + 2 个语义替身 + 官方 Android 仓 2 个原生包。
  • 目前还没做完(诚实列) :手机 GateUI 缺位,Phase 1 以 security.mode=full_access 顶;上游 utils/__init__ 的 Windows 专属 eager import 仍靠 ctypes.windll 桩顶着(这是全平台可移植性 bug,Linux/macOS 同炸,待上游 3 行惰性化落地后撤);仅单台 arm64 真机验收,未做跨设备推广。

导言:四层

  1. 做了什么 :一个操作手机的 Agent,把完整的 Python Agent 内核真身直接打进 APK,在手机本地环回 HTTP 跑起来------循环、工具、记忆、编排全用上游的,不搞手机专属副本。
  1. 问题:薄内核要和 PC 上游双份维护、慢慢漂移;能不能不 fork、直接上完整内核、且不改上游一行?
  1. 答案:能。真机验收通过,代价是上面那串依赖闭包攻坚。
  1. 定性一句 :这不是「手机跑大模型」,是「手机跑完整 Agent 运行时 (循环 + 工具 + 记忆 + 编排)」;而且零上游改动是关键约束------上游改一行,手机侧构建期自动跟上,永远没有「忘同步」。

1. 目标与闸门:先证明能成

手机侧此前是一份标准库薄内核(hachimi_kernel,只用标准库)。它的代价是和 PC 上游双份维护、慢慢漂移。这次的目标很明确:把上游完整内核真身打进 APK,零上游改动、单一真源------构建期从上游仓拷源码进 APK 的 python 源集,上游改了即生效。

但这事有几个事先不敢打包票的点,所以先设了一道可行性闸门(Phase 0 spike),不过就回炉:

  • 第一道闸也是最硬的:pydantic-core 这种二进制 wheel,Chaquopy 官方仓没有 Android 版,要么重打平台标签、要么自编;
  • 上游 utils 的导入雷 :包导入期 eager import Windows 专属模块(ctypes.windll + psutil),Android 上 import utils 会直接炸------而且这是全平台可移植性 bug,Linux/macOS 同炸;
  • 还得证明 :完整 SDK 导入链 + 起 uvicorn + 环回 /health 能在真机跑通。

结果:Phase 0 五道闸全过、无桩运行 ,全程 4.4s 。GATE_A pydantic-core 2.13.3 / core 2.46.3 的 Rust 扩展真机加载;GATE_C 完整上游导入链真实 SDK 无桩通过(utils + omni_core + router_runtime + chat_runtime + audio_api);GATE_D 环回 uvicorn /health → 200。闸门过了,才敢进 Phase 1 动真格。

顺带一个架构决定:本机内核 = 环回远程(127.0.0.1:<port>),遥控 PC 是同一个 client 只换 base_url,Tier 1 / Tier 2 同构。这是把手机当成「一台能跑完整 Python 服务的机器」,不是降级方案。


2. 依赖闭包:比预想深得多(攻坚主战场)

把完整内核装进去,第一道闸不是代码,是依赖闭包 。纯 Python 那部分(fastapi / uvicorn / openai / httpx / PyYAML / starlette)走 PyPI 就完事;真正要命的是那些编译型扩展------而它们藏得比静态审计深。

清点方法(先说,免得被当成玄学)

逐层 `grep "^import ^from " + AST 解析顶层 import,区分「模块级硬依赖」和「函数内惰性导入」;每个二进制包问三问:官方 Android wheel 有吗 → Chaquopy 仓有吗 → 社区 / 自建可行吗。**静态审计仍会漏**------utils/json_utils` 的 numpy 是第三处漏网,最终以真机逐轮报错收敛为准。

实测闭包序列(这是最有价值的一段)

复制代码

规律:LLM / Agent 生态 2025--2026 年 Rust 化严重,每个「纯 Python 好像够了」的判断,都要用 pip 实测闭包验证------这是 spike 阶段反复被打脸换来的。

最终落地:

  • 自编 Rust wheel 5 个:jiter 0.18.0、rpds-py 0.30.0、ormsgpack 1.12.2、orjson 3.11.7、uuid-utils 0.14.1(abi3)
  • 社区现成 1 个 :pydantic-core 2.46.3(文件名要从 linux_aarch64 改成 android_24_arm64_v8a,pip 才收)
  • 官方 Android 仓 2 个:psutil 7.1.3、numpy 1.26.2
  • 语义替身 2 个:见下

语义替身:比编 C 扩展省一半力气的正当手段

前提:消费方对 API 的语义要求弱于位级兼容时,纯 Python 替身 + 官方开关是合法解。

  • xxhash 3.6.0 :langgraph 只消费 xxh3_128_hexdigest(pregel 状态版本指纹,只要求确定性 128 位摘要)→ 用 hashlib.blake2b(digest_size=16) 实现,py3-none-any wheel。与 PC 真 xxhash 摘要值不互通------无跨机语义,无影响。
  • zstandard 0.25.0 stub :langsmith 的压缩导入是 try/except ImportError 守卫 + 官方开关 LANGSMITH_DISABLE_RUN_COMPRESSION=true------stub 满足 pip 依赖解析,守卫自然判 False,双保险。

判据(这一步不能省):替身前先 grep 消费方真实用法。langgraph 全仓只有一处 from-import;langsmith 有守卫 + 开关。并确认无跨系统兼容语义。

自编 wheel 的三个关键坑(五步法只说坑,不堆流水)

  1. pyo3 对 android 目标无条件 -lpython3.12 :extension-module feature 压不掉(上游 build.rs 对 android 硬编码)。必须从自家 APK 抽 libpython3.12.so 喂进去,产物 DT_NEEDED 与 Chaquopy 运行库 soname 逐字匹配才 dlopen 命中。
  1. 扩展文件名必须裸 .so :<name>.cpython-312-aarch64-linux-android.so 这种完整标签名 Chaquopy 的 EXTENSION_SUFFIXES 不认(import 报 No module named),改成 <name>.so 即刻可见。
  1. maturin 1.16 在 Windows 有解释器名解析 bug (.exe 后缀),绕行 cargo 直编 + 手工组 wheel(从官方 manylinux aarch64 wheel 抽 python 文件与元数据,换入自编 .so,WHEEL 标签改 cp312-cp312-android_24_arm64_v8a,重算 RECORD)。

边界:以上针对 NDK r27c + Chaquopy 17.2 + 目标 Python 3.12,版本钉死(openai-agents==0.22.2 / openai==3.13.0 / mcp==2.2.0 ...)。换 NDK 或升 Chaquopy 可能要重走一遍。


3. 三个真机踩坑(现象 → 后果 → 怎么修 → 教训)

踩坑段落的价值不在「我踩过」,在把因果链讲完整。下面三个都是真机逐轮报错收敛出来的,不是设计阶段能预判的。

坑 1:SSL_CERT_FILE(本次最深的一坑)

  • 现象 :agent 循环跑通后,LLM 调用 3 连败 APIConnectionError(流式退同步的日志可见);而裸 ssl.create_default_context() 直连同一端点 TLS 握手成功、httpx 也成功(401 = HTTP 正常)。
  • 后果:链路「看起来通、实际调不通」,极迷惑------你以为网络配置对了,其实只有非 SDK 路径通。
  • 根因 :openai SDK 走 httpx2,httpx2 自建 ssl context 走「默认验证路径」------设备上 openssl_cafile=/usr/local/ssl/cert.pem 不存在 → unable to get local issuer certificate。Chaquopy 的 CA(cacert.pem)不在默认路径,httpx2 也不自动用 certifi。
  • 修法 :环境变量 SSL_CERT_FILE=certifi.where()(certifi 在 AssetFinder 有真实文件),prepare() 里设置,先于任何 context 创建。
  • 诊断法 :环回诊断路由 /debug-net 三层自检(裸 ssl / httpx / httpx2 + 显式 CA 对照),一次出全部结论。该路由保留(仅 127.0.0.1 可达)。
  • 教训 :交叉编译 / 定制运行时的 CA 信任链要显式接管,不能假设「系统有默认 CA」。凡是换了 Python 运行时或打包方式,TLS 是第一优先级验证项。

坑 2:CWD / HOME 三件套(顺序敏感)

  • 现象 :import utils 即 OSError: Read-only file system。
  • 后果:内核根本起不来,连报错都来不及看业务层。
  • 根因 :上游 utils/logger import 时 makedirs('./temp')(相对 CWD),而 Android 进程 CWD=/ 只读。
  • 修法 :chdir 必须发生在任何 omni/utils 导入之前 。Kotlin 侧序列:prepare()(零依赖,只动环境)→ bind 工具模块 → start;工具模块顶部再自保一份(import 即 chdir)。HOME 同理指向 App 私有目录。
  • 教训:跨平台移植,先确认「隐式 CWD / HOME 依赖」藏在哪。这类依赖平时在桌面跑得好好的,一上定制运行时就炸,而且炸在最底层、最不显眼。

坑 3:服务就绪冒名

  • 现象 :omni_host 报 kernel up,/health 探活 200,实际端口上没东西。
  • 后果:探活「通过」误导你以为内核起了,真正请求打过去全空。
  • 根因 :bind 失败(端口被残留监听占用)后 uvicorn 静默退出,而 /health 探活打到了还活着的旧监听者 上------冒名顶替。Phase 0 的 spike 自检服务器(同端口)残留也撞过同一类:spike 的 should_exit 异步生效,正式服务立刻 bind 会 EADDRINUSE,探活又打到 spike 上假成功。
  • 修法 :就绪判断用 uvicorn Server.started 标志 + 线程存活检查,探活只作第二确认。验收完成后摘除测试挂钩。
  • 教训:同一端口上先后两个服务交接时,「探活通过」无法区分新旧监听者。就绪信号要来自服务自身的状态机,不能来自「端口有人应答」。

4. 工具面:@function_tool 直接注册(AssetFinder 虚拟路径)

  • 现象 :Chaquopy 把 python 源码打进 assets/chaquopy/app.imy(zip),import 正常,但真实文件系统不存在 ------任何「扫描目录」的上游逻辑(插件 loader 的 plugins/ 目录扫描、Path(__file__).parent.glob)在设备上静默落空。
  • 后果:上游插件 loader 在手机上扫不到任何插件,工具面为空。
  • 修法 :工具注册改走 @function_tool import 即注册(TOOL_REGISTRY 进程内表),不依赖 loader 目录扫描;数据文件走 certifi.where() 等运行时解析的真实路径。
  • 数据 :手机工具面 20 个------9 个手机 GUI 原语(observe / wait / tap_by_id / tap_xy / type_text / gesture / screenshot / launch_app / press)+ 上游 core 工具。PC 专属工具(如 shell_exec)在手机上会注册但执行失败------失败是结构化的(回 agent 自主换路),不炸循环;后续按「工具面 = 注册 ∩ 通道门」纪律收窄。
  • 教训 :定制打包器改变了「文件在磁盘上」这个隐含前提。凡是依赖 __file__ 相对路径或目录扫描的逻辑,移植时都要逐处排查。

5. 边界(诚实写,不夸大)

  • 机型 / 样本量:单台 arm64 真机(与系列前文同设备)、Android,N=1 验收;不做跨设备推广。
  • Tier 边界:Phase 1 以本机环回(Tier 1)为主;Tier 2 遥控 PC 复用同一 wire contract;mini_loop 兜底待 Phase 2 退役。
  • GateUI 缺位 :手机审批面未做,Phase 1 以 security.mode=full_access 显式全权顶上,审批面后续独立做。
  • 上游可移植性 bug 暂用桩 :utils/__init__ 包导入期 eager import window_finder(模块级 ctypes.windll + psutil)------Android 上 import utils 即炸,目前用 ctypes.windll = SimpleNamespace(user32=None) 桩顶着。这是全平台 bug(Linux/macOS 同炸),正解是上游 3 行惰性化,落地后撤桩。
  • 网络是独立环境事实 :同一 BYOK 端点 PC 可达 ≠ 手机可达(某端点手机 TCP 443 间歇超时,需重试 / 切设备可达的 provider)。另:gitclaw 网关非流式 响应把 OpenAI 载荷包在 {"data":{...}} 里,流式则标准 SSE 无包裹------设置页非流式测试因此误报「响应不是 OpenAI 兼容格式」,而内核走流式不受影响。测试桩与正式链路传输方式不同(流 vs 非流),结论可能相反,验证端点必须流式与非流式各测一次。

写在最后

把一整套 Agent 运行时装进手机、靠构建期同步保单一真源,工程上最重的不是写代码,是上面这几层依赖与运行时的攻坚------每一层都是真机逐轮报错收敛出来的,不是设计阶段能预判全的。

按层的症状 → 根因 → 解法 → 沉淀规律,完整记录在项目仓库的 doc/full_kernel_hosting_pitfalls_*.md,换机换端点照其最小验收序复跑即可。

项目地址:github.com/momo-null/hachifish

相关推荐
呆呆敲代码的小Y4 小时前
【实战精选面试题】C# 面试题详细解答,按难易程度排序
android·java·游戏·c#·游戏开发·unity面试题·c#面试题
思想觉悟5 小时前
SEAndroid实战
android·linux·c++
思想觉悟6 小时前
Aosp开发磨刀石之ADB速查
android·c++
应用市场8 小时前
用本机跑的开源音乐模型,给安卓小游戏做 20 秒无缝循环背景音乐
android·python·游戏开发·soundpool·音频处理
企业数字化笔记8 小时前
表单发布前怎么验收?版本引用、字段兼容和历史数据回归
android·数据挖掘·回归
生信分析笔记8 小时前
Nature 正刊 GPN-Star 基因组语言模型
android
渣渣盟8 小时前
Flink 状态管理深度剖析:从 KeyedState 到 RocksDB,状态如何存储、恢复与扩缩容
android·大数据·flink
avi91118 小时前
电影开源Jevdo - 源码介绍(vue版)没有android 和 没有wpf
android·wpf·框架·数据爬虫·meego·电影管理
OxYGC8 小时前
[工程] PHP2Shell 漏洞解析:两条 PHP 内核 RCE,为什么应用层的防护全部失效
android·开发语言·php