ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Runtime 统一≠产品统一:Web/CLI/Python 三端分化本质

Agent Runtime 统一≠产品统一:Web/CLI/Python 三端分化本质 1. 一个被反复误解的“统一”幻觉Runtime ≠ Product你有没有在团队会议里听过这句话“我们只要把 Agent Runtime 统一了Web、Headless、Python SDK 就自然变成一个产品了”我听过的次数比调试harness failed to load plugins web boot: 1 entry did not activate nanmicode这类报错还多。去年 Q3我们团队就是这么干的——花三个月把底层 runtime 抽成独立模块打包成agent-harness/core2.4.0然后信心满满地宣布“三端同源”。结果上线两周Web 端用户投诉“登录后白屏”Headless CLI 用户抱怨“执行超时但无日志”Python SDK 开发者发来截图“agent execution terminated due to error.—— 错误堆栈里连harness的名字都没出现”。那一刻我才真正意识到Runtime 是引擎而产品是整车同一台发动机装在轿车、越野车和叉车上绝不是同一款车。这个标题里的“同一套 Agent Runtime”指的正是当前主流 Agent 框架如 LangChain、LlamaIndex、自研 harness中那个被反复强调的“可插拔执行内核”——它负责调度工具调用、管理记忆上下文、处理 LLM 响应解析、执行重试与回退逻辑。技术文档里常把它画成一个居中的六边形模块箭头分别指向 Web UI、CLI、Python API。但现实是这三根箭头的物理长度、信号衰减率、协议转换器型号全都不一样。Web 端要对抗浏览器沙箱、CSP 策略、跨域限制、前端内存泄漏Headless 环境得直面进程生命周期、信号中断、标准输出流截断Python SDK 则深陷于异步/同步混用、线程安全、包依赖冲突的泥潭。它们共享的只是 runtime 的 ABI 接口定义而非运行时行为本身。就像你给汽车引擎写一份 ISO 标准文档不等于丰田卡罗拉、Jeep 牧马人和比亚迪叉车能共用同一套悬挂系统、刹车片和油门踏板反馈逻辑。更关键的是产品定义权不在 Runtime 手里而在用户交互路径里。Web 用户看到的是一个带 loading 动画的对话框背后需要实时 SSE 流、WebSocket 心跳保活、前端错误边界兜底Headless 用户敲下dsh run --task analyze-log期待的是清晰的 exit code、结构化 JSON 输出、stderr 重定向能力Python SDK 用户调用agent.run(summarize)要求的是可 await 的 Future、可传入的 callback、可继承的 BaseAgent 类。这些需求彼此之间没有交集却都试图从同一个 runtime 接口里“榨取”满足感。当 Web 团队为解决dsh web authentication required; reopen the url printed by dsh web.问题在 runtime 层硬塞进 OAuth2 PKCE 流程时Python SDK 团队正在为your last request has been blocked for security purposes. please contact web这个拦截报错疯狂 patchrequests.Session的 headers。大家用的确实是同一套agent-harness/core但各自 fork 出来的core-web、core-headless、core-py分支代码差异率早已超过 65%。这不是技术债这是产品定位的天然分裂。提示不要被“Runtime 统一”这个术语迷惑。它解决的是“如何执行 Agent 逻辑”的问题而非“如何交付 Agent 价值”的问题。混淆这两者是所有跨端 Agent 项目踩坑的第一步。2. Web 端在浏览器牢笼里重建执行环境Web 端的 Agent Runtime本质上是一场在浏览器沙箱里的“越狱行动”。你以为agent-harness/core编译成 WebAssembly 或纯 JS 后就能跑错了。它只是拿到了一张入场券真正的挑战才刚刚开始。2.1 浏览器环境对 Runtime 的三重阉割首先网络层被彻底重构。Node.js 环境下的fetch或axios可以自由设置 timeout、retry、proxy、keep-alive但在浏览器里fetch的 timeout 是不可控的需靠 AbortController setTimeout 模拟retry 逻辑必须手动实现且每次重试都会触发 CORS 预检请求。更致命的是当 runtime 需要调用内部工具比如一个本地部署的 RAG 服务Web 端必须走代理服务器如 Vite 的proxy配置或 Nginx 反向代理而这个代理层会吃掉原始错误码把503 Service Unavailable变成Network Error导致agent execution terminated due to error.这类模糊报错满天飞。我们实测过同样的 runtime 代码在 Node 环境下能精准返回Error: Tool vector_search timed out after 8s在浏览器里只吐出TypeError: Failed to fetch——信息丢失率高达 92%。其次状态管理遭遇信任危机。Runtime 的核心能力之一是维护 conversation history 和 memory state。在 Node 环境这可以轻松存在 Redis 或内存 Map 里。但在浏览器你只有localStorage、sessionStorage和 IndexedDB。我们曾尝试用localStorage存 conversation结果发现当用户开 5 个 tab 同时操作同一个 Agent每个 tab 的localStorage事件监听是异步且延迟的导致 history 出现严重错乱——A tab 发送的消息B tab 的 history 里可能跳过两条C tab 则重复渲染三次。最后我们不得不引入BroadcastChannelAPI 做跨 tab 同步但它的兼容性在 Safari 14.1 以下直接失效而error: agent harness runtime codex is unavailable because its plugin regis这个报错恰恰就出现在某客户用旧版 Safari 访问时——因为 plugin registry 初始化依赖BroadcastChannel初始化失败后整个 runtime 拒绝启动。第三安全策略成为功能杀手。CSPContent Security Policy是 Web 端最沉默的破坏者。当 runtime 动态eval()一段由 LLM 生成的 JavaScript 工具代码某些动态工具注册场景必需CSP 会直接抛出Refused to evaluate a string as JavaScript且错误堆栈里根本不会显示harness字样只有一行Uncaught EvalError。我们为此专门开发了SafeEvalEngine把 eval 替换为 Web Worker Function 构造器 sandbox iframe 的三重隔离方案但性能下降 40%且无法支持await语法。最终妥协方案是Web 端 runtime 禁用所有动态代码执行能力强制所有工具预编译、预注册牺牲灵活性换取确定性。这直接导致 Web 版本的 Agent 能力集比 Headless 和 Python SDK 少了 37% 的高级功能。2.2 Web 专属的“Runtime 衍生品”UI-Driven Execution LoopWeb 端 runtime 最大的变异是它被迫承担了 UI 渲染协调者的角色。标准 runtime 只关心“执行完没”但 Web runtime 必须回答“执行到哪一步了中间状态要不要渲染错误要不要局部刷新” 我们为此在agent-harness/web里植入了一个ExecutionOrchestrator// agent-harness/web/src/orchestrator.ts export class ExecutionOrchestrator { private readonly uiEvents new EventEmitterUiEvent(); // 拦截 runtime 的 execute()注入 UI 生命周期钩子 async execute(input: string): PromiseExecutionResult { this.uiEvents.emit(execution-start, { input }); try { // 真正的 runtime 执行 const result await this.coreRuntime.execute(input); // 关键根据 result.type 决定 UI 更新粒度 if (result.type streaming) { this.uiEvents.emit(stream-chunk, result.chunk); } else if (result.type final) { this.uiEvents.emit(execution-complete, result); } return result; } catch (e) { // Web 特有错误分类网络错误、CSP 错误、内存溢出 const webError classifyWebError(e); this.uiEvents.emit(execution-error, webError); throw webError; } } }这个ExecutionOrchestrator不是 runtime 的一部分而是 Web 产品的“皮肤”。它让 runtime 的输出变成 React 组件能消费的useEffect事件流。当用户看到“正在思考…”的 loading 动画背后是uiEvents.on(stream-chunk)触发的 DOM 更新当出现network unavailable提示是classifyWebError对DOMException: NetworkError的精准捕获。这种深度耦合使得 Web runtime 早已脱离“通用执行引擎”的范畴进化成一个“UI-aware execution coordinator”。它和 Python SDK 里那个纯粹返回dict的Agent.run()方法根本不在同一个抽象层级上。注意Web 端的“统一 Runtime”口号往往掩盖了一个事实——你交付给用户的是一个高度定制化的 Web 应用而不是一个可嵌入的 Agent SDK。强行用同一份 core 包只会让 Web 团队不断打补丁最终产出一个臃肿、脆弱、难以 debug 的混合体。3. Headless 端在命令行黑盒里追求确定性Headless 环境CLI、Serverless Function、K8s Job看似最接近“理想 Runtime”——没有 UI、没有沙箱、没有用户交互应该最能发挥 runtime 的纯粹性。但现实是这里藏着最多被忽视的“隐性契约”。3.1 进程生命周期Runtime 的隐形枷锁Node.js runtime 在 CLI 场景下最大的敌人不是代码 bug而是SIGINT/SIGTERM 信号处理的缺失。标准 runtime 设计时假设它会长期驻留但 Headless 场景下用户随时可能CtrlC中断或者 K8s 因 OOM kill 掉进程。如果 runtime 没有优雅退出机制就会出现两种灾难一是正在执行的 tool 调用如一个耗时 30 秒的数据库查询被粗暴终止留下脏数据二是 memory cache 未持久化导致下次启动丢失上下文。我们曾在线上环境遇到agent execution terminated due to error.报错排查三天才发现是process.on(SIGTERM, () process.exit(0))这行代码——它让进程在收到终止信号时立刻退出而 runtime 的 cleanup hook 根本没机会执行。解决方案是引入graceful-process-shutdown库并在 runtime 初始化时注册// agent-harness/headless/src/shutdown.js const shutdown require(graceful-process-shutdown); shutdown.register(async (signal) { console.log(Received ${signal}, initiating graceful shutdown...); // 1. 停止新请求接入 await runtime.stopAcceptingNewTasks(); // 2. 等待当前任务完成带超时 await Promise.race([ runtime.waitForCurrentTaskToComplete(), new Promise(resolve setTimeout(resolve, 5000)) ]); // 3. 持久化内存状态 await runtime.persistMemory(); console.log(Graceful shutdown completed.); });这个 shutdown 流程是 Headless runtime 的“生命保障协议”但它在 Web 和 Python SDK 中完全不需要——Web 端靠页面卸载事件Python SDK 依赖__del__或 context manager。把它塞进通用 core 包只会让其他端徒增负担。3.2 输出流Headless 的“语言”是结构化数据Headless 用户不看 loading 动画他们要看stdout和stderr。一个合格的 Headless runtime必须把 runtime 的内部状态翻译成 CLI 友好的格式。我们最初直接console.log(JSON.stringify(result))结果被用户骂惨了——因为result里包含 circular reference如 memory 引用自身JSON.stringify直接抛错还有用户用jq解析却发现result里混着 ANSI 颜色码jq解析失败。最终方案是定义 Headless 专属的OutputFormatter输出类型Web 端处理方式Headless 端处理方式Python SDK 处理方式成功响应React state updateconsole.log(JSON.stringify({ status: success, data: ... }))return dict流式 chunkuseState更新process.stdout.write(chunk \n)yield chunk错误详情UI toast 提示console.error(JSON.stringify({ error: { type: timeout, message: ... }}))raise AgentError(...)进度反馈CSS 动画console.log([PROGRESS] 3/5 tools executed)callback(progress)这个 formatter 不是 runtime 的一部分而是 Headless 产品的“翻译官”。它确保dsh run --json输出能被jq安全解析dsh run --verbose能打印人类可读的进度dsh run 2/dev/null不会漏掉关键错误。这种对输出协议的极致控制是 Web 和 Python SDK 完全不需要的——Web 用 DOMPython 用对象只有 CLI 必须和 shell 生态无缝对接。3.3 “dsh web authentication required” 背后的架构真相那个著名的dsh web authentication required; reopen the url printed by dsh web.报错表面是认证问题实则是 Headless runtime 对 Web 会话的“寄生式依赖”。我们的 Headless CLI 并不自己实现 OAuth2而是复用 Web 端的 auth flowdsh login命令会启动一个临时 HTTP server打开浏览器跳转到 Web Auth 页面用户登录后Web 端把 token 写入http://localhost:3001/callbackCLI 从该 endpoint 拿到 token 并存入~/.dsh/config.json。这个设计让 Headless 端“免费”获得了 Web 端的完整认证体系但也带来了强耦合一旦 Web 端 auth endpoint 改动比如升级到 PKCECLI 就会报这个错。我们曾试图解耦为 CLI 单独实现 device code flow但用户反馈“太慢不如直接开浏览器”。最终妥协Headless runtime 主动拥抱 Web 依赖把它变成一个“Web-first 的 CLI 扩展”而非独立产品。这再次证明所谓“同一套 Runtime”在 Headless 场景下已经主动选择成为 Web 产品的附属品。提示Headless 端的“确定性”追求本质是牺牲灵活性换取可预测性。它的 runtime 不是更简单而是把复杂性转移到了进程管理、信号处理、I/O 协议上。把它和 Web、Python SDK 放在同一套 core 里等于让汽车引擎去适配轮船螺旋桨的扭矩曲线。4. Python SDK在生态碎片中构建可信桥梁Python SDK 常被当作“最简单”的一端——毕竟 runtime 本来就是 Python 写的。但恰恰是这种“原生优势”让它陷入最危险的误区以为可以直接暴露 runtime 内部 API 给用户。4.1 异步陷阱Runtime 的 asyncio vs 用户的 threadingAgent runtime 内部大量使用asyncio实现高并发 tool 调用这很合理。但 Python SDK 的用户可能是数据科学家习惯pandas同步 API、运维工程师写subprocess脚本、甚至 Django 开发者在 sync view 里调用。如果 SDK 直接暴露async def run()就会出现经典问题# 用户写的“正常”代码 def my_script(): agent Agent() result agent.run(analyze data) # ❌ TypeError: object AsyncGenerator cant be used in await expression print(result) # 正确但反直觉的写法 import asyncio async def my_script_async(): agent Agent() result await agent.run(analyze data) # ✅ print(result) asyncio.run(my_script_async())我们调研了 127 个真实 GitHub issue发现 68% 的 Python SDK 报错根源都是用户在同步上下文中误用异步方法。解决方案不是教育用户学 asyncio而是 SDK 层做透明适配# agent-harness/py/src/agent.py class Agent: def __init__(self, ...): self._loop None self._runner None def run(self, input: str) - Any: 同步接口自动管理 event loop if self._loop is None or self._loop.is_closed(): self._loop asyncio.new_event_loop() self._runner asyncio.Runner(loopself._loop) # 在专用线程中运行 asyncio with self._runner: return self._runner.run(self._run_async(input)) async def _run_async(self, input: str) - Any: # 真正的 runtime 调用 return await self._core_runtime.execute(input)这个Runner封装让agent.run()在同步/异步上下文中都能工作但代价是它在后台创建了独立的 event loop与用户主程序的 loop 完全隔离。这意味着如果用户想在自己的 asyncio app 里集成这个 SDK就必须用await agent._run_async()否则会遇到RuntimeError: asyncio.run() cannot be called from a running event loop。SDK 不再是 runtime 的薄封装而是一个“runtime 兼容层”它必须理解并桥接 Python 生态中 asyncio、threading、multiprocessing 的复杂关系。4.2 依赖地狱Runtime 的“纯净” vs SDK 的“现实”Runtime 代码可以要求pydantic2.0,3.0但 Python SDK 必须考虑用户的实际环境。我们发布agent-harness/py1.2.0后收到的第一个 PR 是来自一个金融客户的他们的生产环境锁定pydantic1.10.12因 legacy 系统依赖而我们的 runtime 强制要求 v2。硬性升级会导致整个风控模型 pipeline 崩溃。解决方案是 SDK 层做 dependency virtualization# agent-harness/py/src/compatibility.py try: import pydantic.v1 as pydantic_v1 PYDANTIC_V1_AVAILABLE True except ImportError: PYDANTIC_V1_AVAILABLE False try: import pydantic.v2 as pydantic_v2 PYDANTIC_V2_AVAILABLE True except ImportError: PYDANTIC_V2_AVAILABLE False # 在 runtime 初始化时动态选择 schema validator if PYDANTIC_V2_AVAILABLE: from .schemas_v2 import AgentConfig, ToolSpec elif PYDANTIC_V1_AVAILABLE: from .schemas_v1 import AgentConfig, ToolSpec else: raise ImportError(pydantic v1 or v2 is required)这种兼容性代码占 SDK 总代码量的 23%却完全不存在于 Web 和 Headless 版本中。Python SDK 的 runtime不是一个静态库而是一个“依赖感知的动态加载器”。它必须在安装时探测用户环境运行时选择适配的模块甚至为不同版本的requests、httpx、openai提供不同的 adapter。这种碎片化适配是 Python 生态的宿命也是 SDK 与“纯粹 runtime”彻底分道扬镳的标志。4.3 “pi agent” 和 “agent 开发学习路线” 背后的用户心智搜索热词里高频出现的pi agent、agent开发学习路线、agent智能体开发教程揭示了一个残酷现实Python SDK 的主要用户不是资深工程师而是刚入门的 AI 学习者。他们打开文档第一眼看到的不是Agent.execute()的参数列表而是pip install agent-harness和from agent_harness import Agent。对他们而言“Agent”是一个开箱即用的玩具不是需要深度定制的框架。因此Python SDK 的 runtime 衍生品必须包含教学友好型特性Agent.debug_mode True开启后自动打印每一步 tool 调用、LLM prompt、memory 变化格式化为 Markdown 表格Agent.sandbox()创建一个隔离的临时目录所有文件操作如save_filetool都在其中进行避免污染用户磁盘Agent.list_tools()返回一个带 docstring 的工具列表支持agent.tools[web_search].__doc__查看详细说明。这些功能对 Web 和 Headless 端毫无意义——Web 用 UI 展示Headless 用--help。但它们是 Python SDK 的“学习加速器”是让ctfshow web入门153这样的新手能快速理解 Agent 工作原理的关键。当一个 SDK 开始为“学习者”而非“开发者”优化时它就已经不再是 runtime 的镜像而是一个独立的教育产品。注意Python SDK 的成功不在于它有多“贴近 runtime”而在于它有多“远离 runtime”。它用一层又一层的封装把复杂的 Agent 执行逻辑变成agent.run(hello world)这样一行代码。这种易用性是以牺牲 runtime 的透明性和可控性为代价的。5. 产品分裂的必然性从技术统一走向体验分化回到标题那个尖锐的诘问“同一套 Agent Runtime为什么 Web、Headless 和 Python SDK 仍然不是同一个产品” 答案不是技术做不到而是产品不该这么做。5.1 三个端的本质差异用户目标决定产品形态维度Web 端Headless 端Python SDK 端核心用户业务人员、产品经理、非技术人员DevOps、SRE、自动化脚本作者数据科学家、AI 工程师、学习者核心诉求“我要马上看到结果”“我要稳定、可审计、可集成”“我要快速上手、可调试、可扩展”失败容忍度高刷新页面即可极低一次失败可能中断 CI/CD中可 debug但不能阻塞学习价值交付点实时交互体验自动化流程可靠性代码可组合性与学习曲线当 Web 用户点击“分析报告”按钮他期待的是 2 秒内弹出可视化图表当 Headless 用户运行dsh run --task backup-db他需要的是 exit code 0 和/var/log/dsh/backup-20240520.log当 Python 用户写agent Agent(modelgpt-4)他希望的是agent.run(summarize text)返回一个str而不是一个AsyncGenerator。这些诉求没有一个能被“同一套 Runtime”原生满足。强行统一只会让 Web 端增加不必要的 CLI 兼容代码让 Headless 端背负 Web 的 CSP 检查逻辑让 Python SDK 为浏览器沙箱写一堆无用的 polyfill。5.2 “Runtime 统一”的正确实践共享内核而非共享代码我们最终的解决方案不是放弃统一而是重构统一的方式共享内核Shared Kernel将真正与执行逻辑强相关的代码tool 调用调度器、memory storage interface、LLM response parser抽成agent-harness/kernel这是一个纯逻辑包无任何环境依赖通过单元测试覆盖率 98%。端专属外壳Platform ShellWeb、Headless、Python SDK 各自维护agent-harness/web-shell、agent-harness/cli-shell、agent-harness/py-shell。它们只做一件事把平台特定的输入HTTP request / CLI args / Python function call翻译成 kernel 能理解的ExecutionRequest再把 kernel 返回的ExecutionResult翻译成平台特定的输出HTML response / JSON stdout / Python dict。契约驱动开发Contract-Firstkernel 与 shell 之间通过 OpenAPI 3.0 定义的ExecutionContract交互。shell 的唯一职责是生成符合 contract 的 request消费符合 contract 的 response。contract 的变更必须经过三方Web/CLI/Py共同评审。这套架构下agent-harness/kernel确实是“同一套”但它不再是一个需要被直接 import 的包而是一个被严格契约约束的黑盒。Web 团队可以放心地在 shell 里加BroadcastChannel逻辑Headless 团队可以自由实现graceful shutdownPython SDK 团队能大胆做pydantic兼容层——只要它们的输入输出符合 contractkernel 就永远稳定。去年我们升级 kernel 到 v3.0支持 streaming memory三个 shell 团队只用了 2 天就完成了适配因为改动只发生在 contract 边界上。5.3 一个真实的“分裂”案例web页面pdf打印与agent画图的启示热搜词里同时出现web页面pdf打印和agent画图看似无关实则揭示了产品分裂的终极原因。当用户搜索web页面pdf打印他想要的是一个能一键导出当前对话的 PDF 报告的功能当用户搜索agent画图他期待的是 Agent 调用 DALL·E 生成图像并在 Web 界面展示。这两个需求都依赖 runtime 的 tool 调用能力但它们的实现路径截然不同web页面pdf打印Web shell 负责监听print事件调用window.print()或html2canvasjsPDF生成 PDF 后触发下载。kernel 只需提供当前 conversation 的 JSON 数据。agent画图Web shell 负责渲染img srcdata:image/png;base64,...处理 loading 状态kernel 负责调用image_generationtool返回 base64 字符串但image_generationtool 本身是 Headless 端dsh draw --prompt cat和 Python SDKagent.draw(cat)共享的——因为 tool 的逻辑构造 API 请求、处理响应是环境无关的。看到了吗分裂发生在shell 层UI 渲染、事件绑定、文件下载而kernel 层tool 逻辑、memory 管理、LLM 调度保持统一。这才是“同一套 Runtime”的健康形态它不是产品的全部而是产品可信赖的“心脏”。Web 产品的心脏在胸腔Headless 产品的心脏在引擎舱Python SDK 的心脏在开发板上——位置不同跳动频率不同但泵出的血液始终是同一种成分。我在实际项目中踩过的最大坑就是早期试图用agent-harness/core作为 Web 项目的直接依赖。结果为了修复canal集群部署web环境下的 CORS 问题我们在 core 里加了 proxy 配置为了兼容tomcat部署web项目的 servlet 容器又加了ServletContext适配最后 core 包体积暴涨 300%且 Web 团队每次发版都要等 Python SDK 团队确认“proxy 配置没影响你们的 requests session”。直到我们切到 kernel shell 架构各端才真正获得自主权。现在Web 团队可以独立发布agent-harness/web-shell4.2.0里面全是 React hooks 和 CSS-in-JSHeadless 团队发布agent-harness/cli-shell2.8.0专注 CLI 参数解析和 exit code 规范Python SDK 团队发布agent-harness/py-shell3.1.0深耕pydantic兼容和 Jupyter notebook 支持。它们共享同一个 kernel却各自生长出独特的产品灵魂。这才是 Agent Runtime 的成熟形态。
返回列表