AI图鉴

AI图鉴首页 › AI 资讯 › 我把一套完整的 Python Agent 运行时塞进手机 APK:依赖闭包、Ru

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

来源:掘金 · 发布时间:11-01 · 由 AI图鉴每小时自动聚合

把完整 Python Agent 内核打进手机 APK 本地跑、零上游改动。记从 spike 到验收的攻坚:依赖闭包、5 个自编 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 真机验收,未做跨设备推广。

导言:四层

- 做了什么:一个操作手机的 Agent,把完整的 Python Agent 内核真身直接打进 APK,在手机本地环回 HTTP 跑起来——循环、工具、记忆、编排全用上游的,不搞手机专属副本。

- 问题:薄内核要和 PC 上游双份维护、慢慢漂移;能不能不 fork、直接上完整内核、且不改上游一行?

- 答案:能。真机验收通过,代价是上面那串依赖闭包攻坚。

- 定性一句:这不是「手机跑大模型」,是「手机跑完整 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 就完事;真正要命的是那些编译型扩展——而它们藏得比静态审计深。

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

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

规律: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 的三个关键坑(五步法只说坑,不堆流水)

- pyo3 对 android 目标无条件 -lpython3.12:extension-module feature 压不掉(上游 build.rs 对 android 硬编码)。必须从自家 APK 抽 libpython3.12.so 喂进去,产物 DT_NEEDED 与 Chaquopy 运行库 soname 逐字匹配才 dlopen 命中。

- 扩展文件名必须裸 .so:<name>.cpython-312-aarch64-linux-android.so 这种完整标签名 Chaquopy 的 EXTENSION_SUFFIXES 不认(import 报 No module named),改成 <name>.so 即刻可见。

- 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

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 604 款 AI 工具分类导航 · 每日 AI 热门项目与动态