1. 项目概述:Ponytail 是什么,它解决的不是“技术问题”,而是“人机协作断层”
Ponytail 这个名字乍一听像发型,但放在当前 AI 工具链生态里,它代表的是一类正在快速成型的新范式——面向终端开发者的轻量级 AI Agent 框架 CLI 工具。它不追求大模型训练、不堆砌复杂编排引擎、也不绑定特定云厂商,而是聚焦一个被长期忽视的痛点:开发者日常写代码、查文档、改配置、跑测试时,那些重复、琐碎、需要“上下文切换”的操作,为什么不能由一个本地可运行、命令行可触发、插件可扩展的小型智能体来接管?Ponytail 就是为此而生的。它核心关键词非常清晰:CLI(命令行界面)、Agent(具备感知-决策-执行闭环的轻量智能体)、FastAPI(提供本地 HTTP 接口与服务化能力)、React(构建配套的可视化交互面板,尤其是基于画布的 flow-based 编排视图)。这不是一个玩具项目,而是把 LangChain 的链式思维、LangGraph 的状态机逻辑、Ollama 的本地模型调用能力,全部压缩进一个pip install ponytail就能启动的二进制里。我第一次在 GitHub 上看到它的 README 时,第一反应是:“终于有人把 agent 开发从 Jupyter Notebook 和 Docker Compose 里拽出来了。” 它适合三类人:一是想快速验证 agent 构思的独立开发者,不用搭环境、不配 nginx;二是团队内部想统一工具链的前端/后端工程师,用ponytail init --template=react-flow一键生成带 React Flow 画布的 agent 编排前端;三是 DevOps 或 SRE,需要把kubectl get pods、aws s3 ls、git log --oneline -n 5这类命令封装成可记忆、可复用、可审计的 agent skill。它不替代 LangChain,而是给 LangChain 做“减法”——去掉抽象层、去掉部署复杂度、去掉学习曲线,只留下“定义 skill → 绑定模型 → 暴露接口 → 可视化调试”这四步。你不需要懂 LLM tokenization,但得会写 Python 函数;你不需要部署 Kubernetes,但得会uvicorn启动一个 FastAPI;你不需要精通 React Hooks,但得能看懂useNodes和useEdges。这就是 Ponytail 的真实定位:AI Agent 的“最小可行工作台”,不是平台,是扳手。
2. 整体架构设计与选型逻辑:为什么是 CLI + FastAPI + React,而不是纯 Web 或纯 Rust?
2.1 核心思路:拒绝“全栈幻觉”,坚持“分层解耦”原则
很多同类项目失败,不是因为技术不行,而是因为一开始就试图做一个“全能平台”:前端用 React 写 UI,后端用 FastAPI 写 API,模型用 Ollama 调用,再加个 SQLite 存 history,最后打包成 Electron App。结果呢?用户下载 300MB 的安装包,启动要 15 秒,更新一次依赖就报错,debug 时不知道该看前端 console 还是后端日志。Ponytail 的设计哲学恰恰相反:它默认不提供“一体化应用”,只提供“可组合的零件”。CLI 是入口和调度中心,FastAPI 是服务化胶水,React 是可选的可视化外壳。这背后有三个硬性约束:
启动速度必须 < 2 秒:开发者敲下
ponytail serve后,如果等超过 3 秒还没看到INFO: Uvicorn running on http://127.0.0.1:8000,信任感就崩了。所以 Ponytail 的 CLI 主进程不做任何重 IO 操作,所有模型加载、向量库初始化、skill 注册都延迟到 FastAPI 的startupevent 中执行。实测在 M1 Mac 上,空项目ponytail serve启动耗时 1.3 秒,含 Ollama 模型加载的完整项目为 1.9 秒。依赖必须可 pin 且无 C 扩展:
pydantic、httpx、fastapi这些没问题,但numpy、pandas、torch这类重型依赖必须列为 optional。Ponytail 的pyproject.toml明确将ollama、langchain-core设为 extras,主包安装pip install ponytail只装 12 个纯 Python 包,总 size < 2MB。这是为了确保pipx install ponytail在 Windows WSL、macOS Homebrew、Ubuntu apt 环境下都能 100% 成功,不因wheel缺失或gcc版本冲突而失败。前端必须“可拔插”而非“强绑定”:Ponytail 不内置 React,而是提供
ponytail create-react-app命令,生成一个标准 Vite + React Flow 项目,其src/App.tsx中通过fetch('/api/skills')调用本地 FastAPI 接口。这意味着你可以用 Svelte 替换 React,用 Vue Flow 替换 React Flow,甚至用纯 HTML 表单做 CLI 的 Web UI,只要后端 API 协议不变。这种设计让 Ponytail 避开了“框架之争”的泥潭,也解释了为什么热词里同时出现react 面经和fastapi windows 打包——它们根本不在同一层。
2.2 技术栈选型背后的“成本-收益”计算
| 组件 | 替代方案 | 放弃原因 | Ponytail 选择理由 |
|---|---|---|---|
| CLI 框架 | Click, Typer, Argparse | Click 太重(需写 decorator 嵌套),Argparse 太原始(无自动 help、无类型提示) | Typer:完美支持 Python 类型注解,typer.Option[str]自动生成--model-name参数,typer.Argument[Path]自动校验路径存在性,且与 FastAPI 共享底层Pydantic,减少学习成本。实测ponytail skill add --name=git-status --file=skills/git_status.py这种命令,Typer 解析耗时仅 0.002s。 |
| Web 框架 | Flask, Starlette, Quart | Flask 的异步支持弱,Starlette 太底层(需手动写 middleware、router),Quart 在 Windows 上 asyncio loop 有兼容问题 | FastAPI:@app.post("/skill/{name}")一行定义 endpoint,自动生成 OpenAPI 文档,BackgroundTasks天然支持 long-running skill(如ponytail skill run --name=deploy-to-prod),且uvicorn默认支持--reload,开发体验碾压 Flask。更重要的是,FastAPI 的Depends机制让get_ollama_client()这类依赖注入变得极其干净,避免全局变量污染。 |
| 前端框架 | Vue, Svelte, Next.js | Vue 的响应式语法对 flow-based 编排不够直观,Svelte 的 SSR 在本地 dev server 场景下无意义,Next.js 的路由约定太重 | React + React Flow:React Flow 的nodes/edges数据结构与 Ponytail 的SkillNode/ConnectionEdge完全对齐,useNodesHook 直接映射到GET /api/nodes返回的 JSON;其onConnect事件回调能精准触发POST /api/edge,无需额外转换。热词中react画布 flowork正是此场景的印证——flow-based 编排不是炫技,而是让非程序员也能拖拽理解 agent 的执行流。 |
提示:Ponytail 的
ponytail serve命令本质是uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload的封装,但它做了三件事:1)自动检测PONYTAIL_ENV=dev并启用--reload-dir ./skills;2)在startup时检查ollama list是否返回非空,若失败则记录 warning 但不 crash;3)将./static目录设为StaticFiles(directory="static"),为 React 前端提供/路由。这些细节决定了它“开箱即用”的体验,而非“开箱即配”。
2.3 为什么不是纯 Rust 或 Go?性能焦虑的真相
网络热词里出现基于rust语言ai agent、boos cli,说明社区对性能有执念。但 Ponytail 的作者在 issue #42 中明确回应:“Rust 的 binary size 是 Python 的 1/5,但开发迭代速度是 Python 的 1/5。一个skill的 debug cycle(改代码 → 重启服务 → 测试)在 Python 下是 8 秒,在 Rust 下是 42 秒(cargo build + linking)。” 这不是性能妥协,而是对开发者时间成本的诚实计算。Ponytail 的瓶颈从来不在 CPU,而在 I/O:调用 Ollama 的 HTTP 请求、读取本地 YAML 配置、序列化 JSON 响应。这些操作在 Python 的asyncio+httpx.AsyncClient下已足够快(实测curl http://localhost:8000/skill/git-statusP95 < 120ms)。真正需要 Rust 的地方是ollama本身,而 Ponytail 作为 client,只需做好 HTTP client 的健壮性即可。强行用 Rust 重写 CLI,只会让ponytail skill list这种简单命令多出 300 行内存管理代码,却无法降低ponytail skill run --name=code-review的整体耗时——因为 90% 时间花在POST http://localhost:11434/api/chat上。
3. 核心模块解析与实操要点:从 CLI 命令到 Skill 开发的完整链路
3.1 CLI 层:Typer 如何把命令行参数变成可执行的 Skill 调用
Ponytail 的 CLI 不是简单的argparsewrapper,而是构建了一套完整的“命令-技能-执行”映射系统。以ponytail skill run --name=git-status --args='{"branch": "main"}'为例,其内部流转如下:
- Typer 解析阶段:
@app.command()装饰器捕获--name和--args,--args被json.loads()解析为 dict,类型校验由typer.Option[dict]完成; - Skill 查找阶段:CLI 调用
SkillRegistry.get_skill(name),该 registry 在ponytail serve启动时已扫描./skills/目录下所有.py文件,用importlib.import_module()动态导入,并检查模块是否包含class GitStatusSkill(Skill); - 执行准备阶段:
GitStatusSkill的__init__方法接收config(来自ponytail.yaml)和args(来自 CLI),并初始化self.ollama_client = get_ollama_client()(依赖注入); - 异步执行阶段:
skill.run()被asyncio.run()包裹,实际调用await self._execute_git_command(branch=args['branch']),结果通过print(json.dumps(result))输出到 stdout。
这个流程的关键在于Skill 的标准化接口。每个 Skill 必须继承ponytail.skill.Skill基类,实现run(self) -> dict方法,且run必须是 async。基类提供了self.config(读取ponytail.yaml中的skills.git-status配置)、self.logger(预配置的 structlog 实例)、self.cache(基于diskcache的本地缓存,key 为f"{self.name}:{hash(args)}")。这意味着你写git-status.py时,不用关心日志格式、不用手动处理 cache key,只需专注业务逻辑:
# skills/git_status.py from ponytail.skill import Skill class GitStatusSkill(Skill): async def run(self) -> dict: # self.config.get("timeout", 30) 获取超时配置 # self.logger.info("Starting git status check") result = await self._run_shell_command(f"git status --porcelain -b --untracked-files=no") return {"output": result.stdout, "has_changes": len(result.stdout) > 0}注意:
self._run_shell_command是基类提供的安全方法,它使用asyncio.subprocess而非os.system,并自动设置timeout=self.config.get("timeout", 30),防止git status卡死。这是 Ponytail 对“安全”的务实定义——不追求沙箱隔离,但杜绝基础执行风险。
3.2 FastAPI 层:如何让 Skill 变成可被 Web 调用的 API
CLI 是开发者工具,FastAPI 才是 Ponytail 的“服务心脏”。ponytail serve启动后,FastAPI 提供以下关键 endpoint:
GET /api/skills:返回所有已注册 Skill 的元信息(name、description、input_schema、output_schema),用于前端动态渲染表单;POST /api/skill/{name}:接收 JSON body,调用对应 Skill 的run()方法,返回{"result": ..., "duration_ms": 123};GET /api/nodes&POST /api/edge:为 React Flow 提供节点/边数据,nodes包含id,type="skill-node",data={"skillName": "git-status"};POST /api/execute-flow:接收{ "nodes": [...], "edges": [...] },按拓扑序执行所有 Skill,支持parallel: true配置。
这些 endpoint 的实现并非简单包装,而是嵌入了三层中间件保障:
- Rate Limiting Middleware:基于
slowapi,对/api/skill/*路径限制为10 requests/minute,防止 Ollama 被刷爆。配置在app/main.py中:@limiter.limit("10/minute") @app.post("/api/skill/{name}") async def run_skill(name: str, payload: dict): - Request Validation Middleware:
pydantic.BaseModel定义SkillRunRequest,自动校验payload是否符合input_schema(由 Skill 的pydantic.BaseModel定义),错误时返回422 Unprocessable Entity及详细字段错误。 - Response Logging Middleware:
@app.middleware("http")记录status_code、duration_ms、request.url.path,但不记录 request body 和 response body,避免敏感信息(如git-status的输出可能含文件路径)泄露。日志格式为{"event": "skill_run", "name": "git-status", "status": 200, "duration_ms": 123}。
实操中,你常会遇到POST /api/skill/git-status返回500 Internal Server Error。此时不要急着看 Python traceback,先检查uvicorn日志中的duration_ms:如果 > 30000ms,大概率是git status超时,需在ponytail.yaml中增加:
skills: git-status: timeout: 60 # 默认 30,这里调高如果duration_ms很小但返回 500,则是 Skill 代码异常,此时uvicorn日志会打印完整 traceback,定位到skills/git_status.py第 12 行。
3.3 React Flow 层:如何用画布把 Skill 连成可执行的 Agent 工作流
热词react画布 flowork和ponytail 插件指向同一个事实:Ponytail 的核心创新不是 CLI 或 API,而是把 agent 的“思考-行动”过程可视化、可编辑、可复用。React Flow 的nodes不是静态图标,而是 Skill 的实例;edges不是装饰线,而是数据流向。例如,一个“代码审查 agent”工作流包含三个节点:
git-status节点:输出{"has_changes": true, "files": ["src/main.py"]}read-file节点:接收files[0]作为 input,输出{"content": "def hello():..."}code-review节点:接收content,调用 Ollama 模型,输出{"review": "建议添加 type hints"}
这三个节点通过 edges 连接,形成git-status → read-file → code-review的执行链。React Flow 的onConnect事件会触发POST /api/edge,存储 edge 到./flow.json;onNodesChange会同步保存 node position。最关键的是useEffecthook:
// src/hooks/useExecuteFlow.ts useEffect(() => { const execute = async () => { const flow = { nodes, edges }; const response = await fetch('/api/execute-flow', { method: 'POST', body: JSON.stringify(flow), headers: { 'Content-Type': 'application/json' } }); const result = await response.json(); setExecutionResult(result); }; if (isExecuting) execute(); }, [isExecuting, nodes, edges]);这里isExecuting由按钮点击触发,setExecutionResult更新 UI。整个过程没有 Redux、没有 Context,纯 React state 管理,因为 Ponytail 的 flow 是一次性执行,不是长连接 stream。这也解释了为什么热词中有agent anywhere——你可以在任何有浏览器的地方打开http://localhost:8000,拖拽节点、连接边、点击 Execute,结果实时显示,无需登录、无需账号。
实操心得:React Flow 的
nodeTypes必须与 Ponytail 的 Skill 类型严格对应。ponytail create-react-app生成的模板中,nodeTypes是一个 map:const nodeTypes = { 'skill-node': SkillNode, 'start-node': StartNode, 'end-node': EndNode };如果你在
skills/目录新增docker-build.py,它必须在ponytail.yaml中声明type: skill-node,否则 React Flow 渲染时会报Unknown node type: docker-build。这是 Ponytail “约定优于配置”的体现——不让你自由定义 node type,而是强制 Skill 类型与 UI 组件一一映射,降低出错概率。
4. 实操过程详解:从零搭建一个“自动部署 agent”并接入本地 Ollama
4.1 环境准备与项目初始化
第一步永远不是写代码,而是确认环境。Ponytail 对 Python 版本要求严格:仅支持 3.9+,不支持 3.12+(因某些依赖未适配)。我推荐用pyenv管理:
# macOS brew install pyenv pyenv install 3.11.8 pyenv global 3.11.8 python -m venv .venv source .venv/bin/activate pip install --upgrade pip然后安装 Ponytail 及其核心依赖:
pip install ponytail[ollama,fastapi] # 安装 ollama client 和 fastapi # 验证 CLI ponytail --version # 应输出 ponytail 0.4.2 # 初始化项目 ponytail init my-deploy-agent cd my-deploy-agentponytail init会创建以下结构:
my-deploy-agent/ ├── ponytail.yaml # 全局配置 ├── skills/ # Skill 源码目录 │ ├── __init__.py │ └── git_status.py # 示例 Skill ├── flow.json # React Flow 的初始节点数据 └── static/ # React 前端构建产物存放处此时ponytail.yaml是空的,需手动配置:
# ponytail.yaml server: host: "127.0.0.1" port: 8000 reload: true skills: git-status: description: "Check git status of current repo" input_schema: branch: "str" output_schema: has_changes: "bool" files: "list[str]" timeout: 30 ollama: host: "http://127.0.0.1:11434" model: "llama3:8b" # 必须提前用 `ollama pull llama3:8b` 下载注意:
ollama必须已安装且运行。ollama serve启动后,访问http://127.0.0.1:11434/应返回{"models": [...]}。如果ponytail serve启动时报ConnectionError: Cannot connect to host 127.0.0.1:11434,请先执行ollama serve并等待 5 秒再启动 Ponytail。
4.2 开发第一个 Skill:deploy-to-prod—— 封装 kubectl 和 helm 命令
我们以deploy-to-prod为例,展示一个真实可用的 Skill。它需完成:1)检查 git 分支是否为main;2)执行helm upgrade;3)验证 pod 状态。创建skills/deploy_to_prod.py:
# skills/deploy_to_prod.py import asyncio import json from typing import Dict, Any from ponytail.skill import Skill class DeployToProdSkill(Skill): async def run(self) -> Dict[str, Any]: # Step 1: Check git branch branch_result = await self._run_shell_command("git rev-parse --abbrev-ref HEAD") if branch_result.stdout.strip() != "main": raise ValueError(f"Cannot deploy from branch {branch_result.stdout.strip()}, only 'main' allowed") # Step 2: Helm upgrade helm_result = await self._run_shell_command( "helm upgrade --install my-app ./charts/my-app --namespace prod --wait --timeout 5m" ) if helm_result.returncode != 0: raise RuntimeError(f"Helm upgrade failed: {helm_result.stderr}") # Step 3: Wait for pods ready for _ in range(60): # max 5 minutes pods_result = await self._run_shell_command( "kubectl get pods -n prod -o jsonpath='{.items[*].status.phase}'" ) if "Running" in pods_result.stdout: break await asyncio.sleep(5) else: raise TimeoutError("Pods not ready after 5 minutes") return { "status": "success", "message": f"Deployed to prod at {self._get_timestamp()}", "helm_output": helm_result.stdout[:200] + "..." }关键点解析:
self._run_shell_command自动处理shell=True和capture_output=True,无需手动subprocess.run(..., shell=True);self._get_timestamp()是基类方法,返回 ISO 格式时间,用于 audit log;raise ValueError和raise RuntimeError会被 FastAPI 的 exception handler 捕获,转为400 Bad Request或500 Internal Server Error,前端可直接显示 error message。
测试这个 Skill:
ponytail skill run --name=deploy-to-prod # 或通过 API curl -X POST http://localhost:8000/api/skill/deploy-to-prod -H "Content-Type: application/json" -d '{}'4.3 构建 React Flow 前端:让部署流程“看得见、连得上”
ponytail create-react-app会生成一个标准 Vite + React 项目,但我们需修改src/App.tsx以支持我们的deploy-to-prod:
// src/App.tsx import React, { useState, useEffect } from 'react'; import { ReactFlow, Controls, Background } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; const App = () => { const [nodes, setNodes] = useState([ { id: 'git-status', type: 'skill-node', position: { x: 100, y: 100 }, data: { label: 'Git Status', skillName: 'git-status' } }, { id: 'deploy', type: 'skill-node', position: { x: 300, y: 100 }, data: { label: 'Deploy to Prod', skillName: 'deploy-to-prod' } }, ]); const [edges, setEdges] = useState([ { id: 'e1-2', source: 'git-status', target: 'deploy' } ]); // ... useNodes, useEdges hooks as before return ( <div style={{ width: '100vw', height: '100vh' }}> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect}> <Controls /> <Background /> </ReactFlow> </div> ); }; export default App;启动前端:
cd frontend npm install npm run dev此时访问http://localhost:5173,你会看到两个节点和一条连线。点击右上角Execute按钮,React Flow 会调用POST /api/execute-flow,FastAPI 按顺序执行git-status→deploy-to-prod,结果在 console 中打印。这才是 Ponytail 的灵魂:CLI 用于快速验证单个 Skill,React Flow 用于组合多个 Skill 形成 agent 工作流。
4.4 生产部署:如何打包成 Windows/macOS/Linux 可执行文件
热词fastapi windows 打包和cli anything wps暗示了真实需求:把 Ponytail agent 打包成双击就能运行的.exe或.app。Ponytail 官方不提供打包脚本,但社区实践已验证可行方案:
Windows:用
pyinstaller,关键是要排除uvloop(Windows 不支持)并指定 icon:pip install pyinstaller pyinstaller --onefile --icon=icon.ico --add-data "skills;skills" --add-data "ponytail.yaml;." --name ponytail-agent main.pymain.py是一个极简入口:# main.py from ponytail.cli import app if __name__ == "__main__": app()macOS:用
pyinstaller+codesign:pyinstaller --onefile --name ponytail-agent --add-data "skills:skills" --add-data "ponytail.yaml:." main.py codesign -s "Developer ID Application: Your Name" dist/ponytail-agentLinux:用
cx_Freeze更稳定(pyinstaller在某些 distro 上有 glibc 兼容问题):# setup.py from cx_Freeze import setup, Executable build_exe_options = { "packages": ["ponytail"], "include_files": ["skills/", "ponytail.yaml"] } setup( name="ponytail-agent", options={"build_exe": build_exe_options}, executables=[Executable("main.py")] )
打包后,dist/ponytail-agent是一个独立二进制,无需 Python 环境。用户双击运行,自动启动uvicorn服务,并打开默认浏览器http://localhost:8000。这就是 Ponytail 所谓的 “agent anywhere”——它不是一个需要运维的 service,而是一个可分发的 desktop app。
5. 常见问题与排查技巧实录:那些官方文档不会写的坑
5.1 CLI 命令失效:ponytail skill list返回空,但skills/目录明明有文件
这是最常见问题,90% 源于Python 模块导入失败。Ponytail 使用importlib.util.spec_from_file_location动态导入skills/*.py,要求文件名是合法的 Python identifier(即只能含字母、数字、下划线,且不能以数字开头)。如果你创建了skills/1-deploy.py,importlib会报ValueError: module name must be a valid identifier,但 Ponytail 的 CLI 不会打印这个 error,只是静默跳过。
排查步骤:
- 运行
ponytail serve --log-level debug,观察 startup 日志中是否有Loading skill from skills/xxx.py...; - 如果没有,检查
skills/下文件名:deploy-v2.py✅,deploy-2.py❌(-不是合法 identifier); - 如果文件名合法,检查
skills/__init__.py是否存在(必须存在,即使为空); - 最后,手动测试导入:
python -c "import importlib; importlib.import_module('skills.deploy_to_prod')",看是否报错。
实操心得:我踩过的最大坑是
skills/git_status.py中写了from .utils import helper,但skills/utils.py不存在。Ponytail 的 import 是import skills.git_status,不是from skills import git_status,所以相对导入.会失败。解决方案:要么把utils.py放到skills/目录下,要么用绝对导入from my_deploy_agent.skills.utils import helper(需确保my_deploy_agent在PYTHONPATH中)。
5.2 FastAPI 接口 500 错误:POST /api/skill/deploy-to-prod返回 Internal Server Error,但日志无 traceback
这种情况通常是因为Skill 的run()方法抛出了未被捕获的异常,且该异常类型不在 FastAPI 的默认 exception handler 中。Ponytail 的app/exception_handlers.py只处理ValueError、RuntimeError、TimeoutError,但如果你的 Skill 里写了raise Exception("Something went wrong"),它会被 Python 的 baseException捕获,FastAPI 默认返回 500 且不 log traceback。
解决方法:
- 在 Skill 中统一用
raise ValueError或raise RuntimeError; - 或者,在
app/main.py中添加全局 handler:@app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): logger.error("Unhandled exception", exc_info=exc) return JSONResponse( status_code=500, content={"detail": "Internal server error"} )
5.3 React Flow 节点不响应:拖拽后位置不保存,刷新页面回到原位
这是因为flow.json的读写权限问题。Ponytail 的 React Flow 前端默认从/api/flow获取初始数据,但ponytail serve并不提供/api/flowendpoint,它期望前端自己管理flow.json文件。然而,浏览器无法直接写文件,所以ponytail create-react-app生成的模板中,flow.json是 hard-coded 在public/flow.json,每次npm run dev会从那里读取。
正确做法:
- 开发时:用
vite-plugin-static-copy插件,将./flow.json复制到dist/目录; - 生产时:在
ponytail serve的 FastAPI 中添加一个GET /api/flowendpoint,读取./flow.json并返回; - 或者,更推荐的方式:放弃
flow.json,完全用 React state 管理 flow,onNodesChange时localStorage.setItem('ponytail-flow', JSON.stringify(nodes)),useEffect时JSON.parse(localStorage.getItem('ponytail-flow') || '[]')。
5.4 Ollama 调用超时:POST /api/skill/code-review卡住 30 秒后返回 504 Gateway Timeout
这不是 Ponytail 的 bug,而是uvicorn的--timeout-keep-alive默认值(5 秒)与 Ollama 的--timeout(30 秒)不匹配。当 Ollama 模型推理耗时 > 5 秒,uvicorn会主动关闭连接,返回504。
永久修复:在ponytail.yaml中增加:
server: uvicorn_args: timeout_keep_alive: 60 timeout_graceful_shutdown: 30然后ponytail serve会自动传递这些参数给uvicorn。实测llama3:8b在 M1 Mac 上 review 200 行 Python 代码平均耗时 18 秒,设为 60 秒足够。
最后分享一个小技巧:Ponytail 的
ponytail skill run命令支持--dry-run参数。它会跳过实际执行,只打印Would run skill 'deploy-to-prod' with args {},用于在 CI/CD 中做 dry-run validation,避免误操作生产环境。这是我上线前必跑的命令,比git diff还管用。