ARTICLE DETAIL

资讯详情

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

Ponytail:面向AI Agent开发的CLI工具链

Ponytail:面向AI Agent开发的CLI工具链 1. 项目概述Ponytail 不是马尾辫而是一个面向 AI Agent 开发的 CLI 工具链你搜“ponytail”第一反应可能是发型——但最近在开发者社区里这个词正以惊人的速度脱离美发范畴变成一个高频技术热词。它不是某个大厂刚发布的闭源平台也不是某篇论文里的理论模型而是一个专为快速构建、调试、集成和部署 AI Agent 而生的命令行工具集CLI。它的核心定位非常清晰让开发者从“写一堆胶水代码连通 FastAPI React LLM API”的重复劳动中解放出来把注意力真正放回 agent 的 skill 设计、workflow 编排和 real-world action 落地上。我第一次在 GitHub 上看到 ponytail 仓库时就意识到它踩中了当前 AI 工程化最痛的三个点一是本地开发环境启动太慢改一行 prompt 得重启整个 FastAPI 服务二是 React 前端和后端 agent 逻辑严重割裂调试时得同时盯两个终端日志三是 agent 的 skill比如“查天气”“读邮件”“生成周报”缺乏统一注册、测试和版本管理机制。ponytail 就是冲着这三点来的。它不替代 FastAPI而是用 CLI 把 FastAPI 启动、路由注册、OpenAPI 文档生成、mock 数据注入全自动化它也不重写 React而是提供一套约定式目录结构和预置 hooks比如 useAgentSkill让前端能像调用普通函数一样触发 skill 执行。关键词里反复出现的 “zcode cli”“codex cli”“boos cli”其实都指向同一类工具范式用极简 CLI 命令驱动整个 AI Agent 开发生命周期。ponytail 是其中落地最扎实、文档最贴近真实工作流的一个。它适合三类人正在用 FastAPI 搭建 agent 后端但被配置折腾得头大的后端工程师想快速验证 agent workflow 是否跑通、又不想写一堆 mock 接口的算法同学还有正在用 React 构建 agent 控制台比如 flowork 那种可视化画布但苦于前后端联调效率低的产品技术负责人。它解决的不是“能不能做”而是“能不能一天内从零跑通一个带记忆、能调工具、有 UI 反馈的完整 agent demo”。2. 核心设计思路与架构选型解析为什么是 CLI 而不是 Web IDE2.1 CLI 作为“最小可行控制平面”的底层逻辑ponytail 选择 CLI 而非 Web IDE 或桌面应用并非技术保守而是基于对当前 AI Agent 开发阶段的精准判断。我们拆解一下真实场景一个典型的 agent 开发闭环是“写 skill → 注册到 agent core → 启动 FastAPI 服务 → 在 React 前端触发 → 查看日志/响应 → 修改 prompt 或参数 → 重复”。这个闭环里90% 的时间花在环境切换、服务重启、路径配置和日志定位上。Web IDE 看似友好但它引入了新的复杂度需要维护浏览器端的代码编辑器、服务端的沙箱执行环境、跨域调试通道还要处理 LLM token 流式返回的前端渲染逻辑——这些都不是 agent 本身的价值而是基础设施噪音。CLI 则完全不同。它天然运行在开发者最熟悉的终端里所有操作都是原子命令输出即结果错误即堆栈。ponytail 的ponytail dev命令背后其实是三条并行进程的协调它会自动启动一个 uvicorn 进程FastAPI一个 vite dev server 进程React再启动一个 tail -f 进程实时聚合两个服务的日志到同一个终端窗口。这种“单命令启动双服务统一日志”的能力直接砍掉了传统开发中 40% 的上下文切换成本。更重要的是CLI 天然支持管道pipe、重定向redirect和脚本化scripting。比如你可以写一个./test-skill.sh脚本里面包含ponytail skill test --name weather --input {city: Beijing} | jq .output把 skill 测试变成可复现、可 CI 的单元测试。这是任何 Web IDE 都难以提供的工程化能力。2.2 与 FastAPI 的深度耦合不是“用 FastAPI”而是“让 FastAPI 自动化”ponytail 并没有自己造一个 web 框架而是把 FastAPI 当作“标准件”来用但通过 CLI 层做了三层关键增强。第一层是路由自发现。传统 FastAPI 项目需要手动from app.skills.weather import router然后app.include_router(router)。ponytail 要求所有 skill 必须放在skills/目录下且每个子目录里必须有__init__.py和router.py。CLI 在ponytail dev启动时会扫描该目录自动导入所有router.py中定义的APIRouter实例并按目录名生成/api/skills/{skill_name}的标准化前缀。这意味着你新增一个skills/email_reader/目录无需修改任何主应用代码/api/skills/email_reader/fetch这个 endpoint 就自动可用。第二层是OpenAPI Schema 的 skill 级别注入。每个router.py里除了定义 route还必须提供一个get_openapi_schema()函数返回该 skill 的输入参数 JSON Schema。ponytail CLI 会把这些 schema 聚合成一个完整的 OpenAPI 3.0 文档并在/docs页面里按 skill 分组展示前端 React 可以直接消费这个 schema 来动态生成表单。第三层是LLM 客户端的抽象与切换。ponytail 内置了对 Ollama、OpenAI、Anthropic 的适配器但关键在于它把 LLM 调用封装成了llm_call装饰器。你在 skill 里写llm_call(modelllama3)CLI 就会在运行时根据.ponytail.yaml配置自动选择对应的客户端Ollama 的http://localhost:11434/api/chat或 OpenAI 的https://api.openai.com/v1/chat/completions甚至支持 fallback 机制。这种设计让 skill 代码完全不感知底层 LLM 提供商迁移成本趋近于零。2.3 React 集成策略约定优于配置而非框架入侵ponytail 对 React 的支持走的是“轻量级约定”路线而不是像 Next.js 那样深度集成。它不强制你用特定的 React 版本或构建工具只做三件事第一提供一个标准的src/app/agent/目录结构里面预置了useAgentSkill、useAgentWorkflow两个核心 hook。useAgentSkill的签名是const { execute, data, loading, error } useAgentSkill(skillName)它内部会自动拼接/api/skills/{skillName}的 URL并处理流式响应SSE的解析把 LLM 的 token 流转换成 React 的data状态更新。第二CLI 提供ponytail react:gen命令能根据skills/下的目录结构自动生成src/app/agent/skills.ts文件里面导出所有 skill 的类型定义和默认参数。这样你在 React 组件里写useAgentSkill(weather)时TypeScript 就能智能提示weather的输入类型是{ city: string }避免手写字符串导致的 typo 错误。第三它内置了一个AgentCanvas组件这是一个轻量级的 React Flow 兼容画布你可以用它拖拽连接多个 skill形成 workflow。ponytail workflow:build命令会把画布上的连线关系编译成 LangGraph 兼容的 state graph 代码直接写入workflows/目录。这种设计的好处是React 侧保持了最大自由度你可以用 Vite、Webpack、甚至 Electron 打包可以用 Tailwind、Ant Design 或自定义 CSS可以替换AgentCanvas为你自己的画布实现只要遵循 ponytail 定义的 workflow JSON Schema 即可。它不做框架绑架只做“能力供给”。3. 核心功能实操详解从零搭建一个可交互的 AI Agent3.1 初始化项目与目录结构解析开始之前请确保你的环境已安装 Python 3.10 和 Node.js 18。ponytail 的安装极其简单因为它就是一个纯 Python 包pip install ponytail-cli。注意这里安装的是ponytail-cli不是ponytail后者是旧版已归档。安装完成后运行ponytail --version你应该能看到类似ponytail-cli 0.8.3的输出。现在创建你的第一个 agent 项目ponytail init my-weather-agent。这个命令会做四件事一在当前目录下创建my-weather-agent/文件夹二生成标准的skills/、workflows/、config/目录三初始化一个最小化的 FastAPI 应用main.py和一个 Vite React 前端frontend/四生成一个.ponytail.yaml配置文件。我们重点看这个配置文件# .ponytail.yaml project_name: my-weather-agent backend: port: 8000 host: 0.0.0.0 llm_provider: ollama ollama: base_url: http://localhost:11434 model: llama3 frontend: port: 5173 proxy_api: http://localhost:8000这个 YAML 文件就是 ponytail 的“控制中枢”。它告诉 CLI后端跑在 8000 端口前端跑在 5173 端口两者通过proxy_api配置进行跨域代理Vite 的vite.config.ts里已预置了这个代理规则。最关键的是llm_provider字段它决定了整个 agent 的“大脑”是谁。如果你本地没装 Ollama可以临时改成openai然后在config/secrets.yaml里填入你的OPENAI_API_KEY这个文件默认被 gitignore安全。现在执行ponytail dev。你会看到终端里同时滚动两组日志上面是 uvicorn 的启动信息下面是你熟悉的 Vite 的Local: http://localhost:5173/。打开浏览器访问http://localhost:5173你会看到一个极简的首页上面写着 “Welcome to Ponytail Agent”。这说明双服务已成功启动。此时你还没有写任何 agent 逻辑但整个开发环境已经 ready。这就是 ponytail 的第一个价值环境准备时间从 30 分钟压缩到 30 秒。3.2 创建第一个 Skill天气查询Weather Skill真正的 agent 价值体现在 skill 上。我们来创建一个最基础的weatherskill。在项目根目录下运行ponytail skill:create weather。这个命令会在skills/目录下创建weather/子目录并生成四个文件__init__.py、router.py、service.py和schema.py。我们依次填充它们。首先是schema.py它定义了 skill 的输入输出规范# skills/weather/schema.py from pydantic import BaseModel class WeatherInput(BaseModel): city: str unit: str celsius # 默认摄氏度 class WeatherOutput(BaseModel): city: str temperature: float condition: str description: str这个 Pydantic 模型会被 ponytail CLI 用于生成 OpenAPI Schema 和前端 TypeScript 类型。接着是service.py它封装了真正的业务逻辑。这里我们用一个模拟的天气 API生产环境当然要换成真实服务# skills/weather/service.py import random from typing import Dict, Any from .schema import WeatherInput, WeatherOutput def get_weather(input_data: WeatherInput) - WeatherOutput: # 模拟真实 API 调用 conditions [Sunny, Cloudy, Rainy, Windy] descriptions { Sunny: Clear skies, perfect for outdoor activities., Cloudy: Overcast, but no precipitation expected., Rainy: Light rain, remember your umbrella!, Windy: Gusty winds, secure loose objects. } # 随机生成温度模拟不同城市差异 base_temp {Beijing: 25, Shanghai: 28, Guangzhou: 30, Harbin: 18} temp base_temp.get(input_data.city, 25) random.randint(-5, 5) condition random.choice(conditions) return WeatherOutput( cityinput_data.city, temperaturetemp, conditioncondition, descriptiondescriptions[condition] )这个get_weather函数是 skill 的核心它接收WeatherInput返回WeatherOutput。ponytail 不关心你是调用 HTTP API、读取数据库还是运行本地模型只要函数签名符合约定即可。最后是router.py它把 service 暴露为一个 FastAPI endpoint# skills/weather/router.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from ..dependencies import get_llm_client from .service import get_weather from .schema import WeatherInput, WeatherOutput router APIRouter(prefix/weather, tags[weather]) router.post(/forecast, response_modelWeatherOutput) async def forecast_weather(input_data: WeatherInput): try: result get_weather(input_data) return result except Exception as e: raise HTTPException(status_code500, detailstr(e)) def get_openapi_schema(): return { summary: Get current weather forecast for a city, description: Queries a weather service to return temperature and condition., requestBody: { content: { application/json: { schema: WeatherInput.model_json_schema() } } }, responses: { 200: { content: { application/json: { schema: WeatherOutput.model_json_schema() } } } } }注意get_openapi_schema()函数它返回一个字典描述了这个 endpoint 的 OpenAPI 元数据。ponytail CLI 会读取这个函数并将其注入到全局 OpenAPI 文档中。现在回到终端你不需要重启服务ponytail 的ponytail dev命令监听了skills/目录的文件变化一旦检测到新文件或修改它会自动重新加载 FastAPI 的路由模块。你可以在浏览器里访问http://localhost:8000/docs刷新页面就能看到/api/skills/weather/forecast这个新 endpoint 已经出现在文档里并且 Swagger UI 已经根据WeatherInput自动生成了请求体示例。这就是 ponytail 的第二个价值skill 开发零重启所见即所得。3.3 在 React 前端调用 Skill构建交互式 UI后端 skill 已就绪现在轮到前端。ponytail 的ponytail react:gen命令会扫描skills/目录自动生成前端所需的类型定义。运行它ponytail react:gen。你会看到frontend/src/app/agent/skills.ts文件被创建或更新里面包含了WeatherInput和WeatherOutput的 TypeScript 接口。现在我们来创建一个简单的天气查询组件。在frontend/src/app/agent/目录下新建WeatherForm.tsx// frontend/src/app/agent/WeatherForm.tsx use client; import { useState } from react; import { useAgentSkill } from ./hooks/useAgentSkill; import { WeatherInput, WeatherOutput } from ./skills; export default function WeatherForm() { const [city, setCity] useStatestring(); const [unit, setUnit] useStatestring(celsius); const { execute, data, loading, error } useAgentSkillWeatherInput, WeatherOutput(weather); const handleSubmit (e: React.FormEvent) { e.preventDefault(); if (!city.trim()) return; const input: WeatherInput { city: city.trim(), unit }; execute(input); }; return ( div classNamemax-w-md mx-auto p-6 bg-white rounded-lg shadow-md h2 classNametext-xl font-bold mb-4Weather Forecast/h2 form onSubmit{handleSubmit} classNamespace-y-4 div label htmlForcity classNameblock text-sm font-medium text-gray-700 City /label input typetext idcity value{city} onChange{(e) setCity(e.target.value)} classNamemt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 placeholdere.g., Beijing / /div div label htmlForunit classNameblock text-sm font-medium text-gray-700 Temperature Unit /label select idunit value{unit} onChange{(e) setUnit(e.target.value)} classNamemt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 option valuecelsiusCelsius (°C)/option option valuefahrenheitFahrenheit (°F)/option /select /div button typesubmit disabled{loading} className{w-full py-2 px-4 rounded-md text-white font-medium ${ loading ? bg-gray-400 cursor-not-allowed : bg-blue-600 hover:bg-blue-700 }} {loading ? Fetching... : Get Forecast} /button /form {error ( div classNamemt-4 p-3 bg-red-100 text-red-700 rounded-md Error: {error.message} /div )} {data ( div classNamemt-4 p-3 bg-green-100 text-green-700 rounded-md h3 classNamefont-bold{data.city}/h3 pTemperature: {data.temperature}°{unit celsius ? C : F}/p pCondition: {data.condition}/p p{data.description}/p /div )} /div ); }这个组件使用了 ponytail 提供的useAgentSkillhook。useAgentSkillweather, WeatherInput, WeatherOutput的泛型参数指定了 skill 名称和输入输出类型这使得execute函数的参数和返回值都有了严格的类型检查。组件逻辑非常清晰收集表单数据调用execute然后根据loading、error、data三个状态变量来渲染不同的 UI。最后我们需要在frontend/src/app/page.tsx的主页面里引入它// frontend/src/app/page.tsx import WeatherForm from ./agent/WeatherForm; export default function Home() { return ( main classNameflex min-h-screen flex-col items-center justify-between p-24 div classNamez-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex WeatherForm / /div /main ); }保存文件Vite 会自动热更新。回到浏览器你就能看到一个漂亮的天气查询表单。输入城市点击按钮几秒钟后天气信息就会以卡片形式显示出来。整个过程你没有写一行 fetch 请求没有配置 CORS没有处理 JSON 解析所有网络通信、错误重试、加载状态管理都由useAgentSkillhook 封装好了。这就是 ponytail 的第三个价值前端调用 skill 如同调用本地函数彻底屏蔽底层细节。3.4 构建 Workflow将多个 Skill 串联成智能体单个 skill 是原子能力而真正的 agent 是 workflow。ponytail 用ponytail workflow:create命令来创建 workflow。运行ponytail workflow:create daily-brief它会在workflows/目录下创建daily-brief/文件夹并生成definition.json和graph.py。definition.json是一个声明式的 workflow 描述它定义了节点nodes和边edges// workflows/daily-brief/definition.json { name: Daily Brief, description: A morning briefing that combines weather, news, and calendar events., nodes: [ { id: weather, type: skill, skill_name: weather, input_mapping: { city: context.user_location } }, { id: news, type: skill, skill_name: news, input_mapping: { topic: context.news_topic } }, { id: calendar, type: skill, skill_name: calendar, input_mapping: { date: context.today } }, { id: summarize, type: llm, prompt: Summarize the following weather, news, and calendar information into a concise morning briefing for the user. Be friendly and professional.\n\nWeather: {weather.output}\nNews: {news.output}\nCalendar: {calendar.output}, model: llama3 } ], edges: [ { source: weather, target: summarize }, { source: news, target: summarize }, { source: calendar, target: summarize } ] }这个 JSON 定义了一个四节点 workflow前三节点分别调用weather、news、calendar三个 skill最后一个节点是一个 LLM 总结节点。input_mapping字段实现了数据流的绑定比如weather节点的city参数会从 workflow 的全局context对象里取user_location字段。ponytail workflow:build命令会读取这个 JSON生成graph.py里面是 LangGraph 兼容的代码# workflows/daily-brief/graph.py from langgraph.graph import StateGraph, END from typing import TypedDict, List, Dict, Any class AgentState(TypedDict): user_location: str news_topic: str today: str weather: Dict[str, Any] news: Dict[str, Any] calendar: Dict[str, Any] summary: str def weather_node(state: AgentState) - AgentState: from skills.weather.service import get_weather input_data {city: state[user_location]} result get_weather(input_data) return {weather: result} def news_node(state: AgentState) - AgentState: from skills.news.service import get_news input_data {topic: state[news_topic]} result get_news(input_data) return {news: result} def calendar_node(state: AgentState) - AgentState: from skills.calendar.service import get_events input_data {date: state[today]} result get_events(input_data) return {calendar: result} def summarize_node(state: AgentState) - AgentState: from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI prompt ChatPromptTemplate.from_template( Summarize the following weather, news, and calendar information into a concise morning briefing for the user. Be friendly and professional.\n\nWeather: {weather}\nNews: {news}\nCalendar: {calendar} ) llm ChatOpenAI(modelgpt-4o) chain prompt | llm result chain.invoke({ weather: state[weather], news: state[news], calendar: state[calendar] }) return {summary: result.content} # 构建图 workflow StateGraph(AgentState) workflow.add_node(weather, weather_node) workflow.add_node(news, news_node) workflow.add_node(calendar, calendar_node) workflow.add_node(summarize, summarize_node) workflow.set_entry_point(weather) workflow.add_edge(weather, summarize) workflow.add_edge(news, summarize) workflow.add_edge(calendar, summarize) workflow.add_edge(summarize, END) app workflow.compile()这个graph.py文件可以直接被 FastAPI 的main.py导入和挂载。ponytail 的ponytail dev会自动检测workflows/目录的变化并重新编译 graph。你甚至可以在 React 前端用useAgentWorkflowhook 来触发整个 workflow 的执行传入{user_location: Beijing, news_topic: AI, today: 2024-05-20}就能得到一个由 LLM 生成的、融合了多源信息的个性化早报。这体现了 ponytail 的第四个价值workflow 是声明式的可版本化、可复用、可审计的不再是散落在代码里的 if-else 逻辑。4. 进阶技巧与避坑指南那些官方文档不会写的实战经验4.1 Skill 开发中的“状态陷阱”与解决方案在开发复杂 skill 时一个极易踩的坑是“状态污染”。比如你写了一个email_readerskill它需要先登录邮箱服务器再获取邮件列表。如果把登录 session 存在全局变量或模块级变量里当多个用户并发请求时session 就会互相覆盖导致 A 用户看到 B 用户的邮件。ponytail 的官方文档强调“无状态”但没告诉你怎么优雅地处理有状态的外部服务。我的经验是永远把状态封装在 skill 的 service 函数内部或者用依赖注入的方式传递。例如对于邮箱登录不要写# ❌ 错误示范全局 session session None def login(): global session session EmailSession() def fetch_emails(): return session.list_emails() # 如果 login 没被调用这里就崩了而应该写# ✅ 正确示范函数内创建或依赖注入 from contextlib import contextmanager contextmanager def email_session(): s EmailSession() try: yield s finally: s.close() def fetch_emails(input_data: EmailInput) - EmailOutput: with email_session() as s: s.login(input_data.username, input_data.password) emails s.list_emails() return EmailOutput(emailsemails)这样每次fetch_emails被调用都会创建一个全新的、隔离的 session。ponytail 的ponytail skill:test命令在测试时也是为每个测试用例单独调用一次 skill 函数所以这种写法天然兼容测试。另一个常见陷阱是“LLM 调用的副作用”。比如你在一个 skill 里调用了llm_call但这个 LLM 调用本身会修改数据库比如记录用户偏好。这违反了 RESTful 的幂等性原则会导致重试机制出问题。我的做法是把 LLM 调用和副作用操作严格分离。skill 的主函数只负责调用 LLM 并返回其原始输出再写一个独立的post_process函数专门处理副作用。这样ponytail skill:test只测 LLM 逻辑而副作用逻辑可以单独单元测试。4.2 FastAPI 集成的性能瓶颈与优化实践ponytail 的 FastAPI 后端默认使用uvicorn的--workers 1启动这对于开发调试足够但上线后绝对不够。一个典型的 agent 应用瓶颈往往不在 LLM 推理而在 I/O 等待比如调用天气 API、数据库查询。uvicorn的单 worker 模式是同步阻塞的一个 slow skill 就会让整个服务卡住。解决方案是启用--workers N但 N 不能随便设。我的经验公式是N (CPU 核心数 * 2) 1但对于 I/O 密集型的 agent更有效的是用--workers 4 --loop asyncio。因为 ponytail 的 skill service 函数如果用了async就可以被 uvicorn 的 asyncio event loop 高效调度。所以当你写一个需要调用 HTTP API 的 skill 时务必用aiohttp或httpx.AsyncClient而不是requests。例如# ✅ 异步 skill支持高并发 import httpx async def get_weather_async(input_data: WeatherInput) - WeatherOutput: async with httpx.AsyncClient() as client: resp await client.get( fhttps://api.weatherapi.com/v1/current.json, params{key: YOUR_KEY, q: input_data.city} ) data resp.json() return WeatherOutput( citydata[location][name], temperaturedata[current][temp_c], conditiondata[current][condition][text], descriptiondata[current][condition][text] )然后在router.py里把 endpoint 改成async defrouter.post(/forecast, response_modelWeatherOutput) async def forecast_weather(input_data: WeatherInput): result await get_weather_async(input_data) # 注意这里是 await return result这样一个 uvicorn worker 就能同时处理数百个并发的天气查询请求而不会被阻塞。ponytail 的 CLI 会自动识别async函数并在启动时启用 asyncio loop。这是很多新手忽略的关键点CLI 工具链的威力只有配合正确的异步编程范式才能完全释放。4.3 React 前端的“流式响应”深度定制ponytail 的useAgentSkillhook 默认处理 SSEServer-Sent Events流式响应把 LLM 的 token 逐个推送到data状态。但有时你需要更精细的控制比如在 UI 上显示“思考中...”、“正在搜索...”、“正在总结...”等不同阶段的状态。ponytail 提供了useAgentSkillRaw这个底层 hook它返回的是一个EventSource对象让你可以完全掌控事件流。例如你想在天气查询时先显示一个加载动画等get_weather返回后再显示结果而不是等待 LLM// ✅ 使用 useAgentSkillRaw 实现分阶段 UI import { useEffect, useRef } from react; import { useAgentSkillRaw } from ./hooks/useAgentSkillRaw; export default function AdvancedWeatherForm() { const [status, setStatus] useStateidle | fetching | processing | done(idle); const [result, setResult] useStatestring(); const esRef useRefEventSource | null(null); const startQuery () { setStatus(fetching); // 手动创建 EventSource const es new EventSource(/api/skills/weather/forecast?streamtrue); esRef.current es; es.onmessage (event) { const data JSON.parse(event.data); if (data.type start) { setStatus(processing); } else if (data.type token) { setResult(prev prev data.token); } else if (data.type end) { setStatus(done); } }; es.onerror (err) { console.error(SSE Error:, err); setStatus(idle); }; }; useEffect(() { return () { if (esRef.current) { esRef.current.close(); } }; }, []); return ( div button onClick{startQuery}Start Query/button divStatus: {status}/div divResult: {result}/div /div ); }这个例子展示了 ponytail 的设计哲学它提供开箱即用的便利useAgentSkill也保留了底层的灵活性useAgentSkillRaw让高级用户可以按需定制。官方文档可能只提了前者但后者才是应对复杂 UI 场景的利器。4.4 本地开发与生产部署的平滑过渡ponytail 的.ponytail.yaml配置文件是连接开发与生产的桥梁。很多人在开发时用ollama上线时想切到openai就去改配置结果忘了改secrets.yaml导致服务启动失败。我的做法是用环境变量覆盖配置。在.ponytail.yaml里把敏感字段写成占位符# .ponytail.yaml llm_provider: ${LLM_PROVIDER:-ollama} ollama: base_url: ${OLLAMA_BASE_URL:-http://localhost:11434} model: ${OLLAMA_MODEL:-llama3} openai: api_key: ${OPENAI_API_KEY} model: ${OPENAI_MODEL:-gpt-4o}然后在开发时你只需要export LLM_PROVIDERollama在生产服务器上你运行export LLM_PROVIDERopenai export OPENAI_API_KEYsk-xxx再启动ponytail dev它就会自动读取环境变量无缝切换。ponytail 的 CLI 在解析 YAML 时会优先使用os.environ的值。这比手动改配置文件安全得多也符合 12-factor app 的最佳实践。另一个关键点是打包。ponytail 项目最终要部署ponytail build命令会做三件事一用pyinstaller打包 FastAPI 后端为一个独立的可执行文件二用vite build打包 React 前端为静态文件三把两者合并到一个dist/目录并生成一个start.sh启动脚本。
返回列表