Savor:开源LLM代理网关,实现API成本控制与安全管控
1. 项目概述一个为LLM应用量身定制的智能代理网关如果你正在使用像OpenClaw、Claude Code这类客户端来调用大模型API或者你的应用基于LangChain、LlamaIndex等框架集成了OpenAI或Anthropic的服务那么你很可能已经遇到了几个绕不开的痛点API调用成本像流水一样难以控制偶尔出现的工具调用死循环会瞬间烧掉大量Token不同团队成员的滥用导致账单激增还有那些不经意间在对话中泄露的敏感信息。Savor这个项目就是为解决这些问题而生的。简单来说Savor是一个部署在你本地或服务器上的轻量级代理网关。它扮演着一个“智能中间人”的角色所有从客户端发往OpenAI或Anthropic官方API的请求都会先经过它。在这个过程中Savor会不动声色地完成一系列关键工作实时监控每一次调用的Token消耗和成本自动拦截并打断可能导致无限循环的重复工具调用基于客户端IP实施精细化的请求限流甚至能帮你智能地截断过长的对话历史以节省费用。最棒的是它对客户端完全透明支持OpenAI Chat Completions和Anthropic Messages两种主流协议这意味着你几乎不需要修改现有客户端的任何配置就能无缝享受到这些管理功能。我自己在深度使用各类大模型API开发应用时深感缺乏一个统一的管控层。要么是成本失控要么是稳定性受第三方服务影响。Savor的出现让我能把控制权牢牢抓在自己手里。它不仅仅是一个简单的转发代理更是一个配备了仪表盘、拥有策略引擎的API网关特别适合中小型团队或个人开发者在享受大模型能力的同时实现成本、安全和稳定性的自主管理。2. 核心功能与设计思路拆解Savor的设计哲学非常明确非侵入、高透明、强管控。它不希望成为你技术栈中的又一个负担而是力求成为一个“无感”的基础设施。下面我们来拆解它的几个核心设计思路。2.1 双协议并行的透明代理架构这是Savor的基石。市面上很多代理工具只支持单一协议通常是OpenAI但实际开发中我们可能同时使用基于GPT的OpenClaw和基于Claude的Claude Code。Savor在架构层面就将两种协议处理逻辑解耦。实现原理Savor内部维护着两套独立的路由和配置处理管道。当请求到达时它会根据请求路径或Header特征例如Anthropic协议有特定的anthropic-version头自动进行协议识别。识别后请求会被路由到对应的处理管道。OpenAI管道的请求会被转发到config.js中配置的upstream地址通常是https://api.openai.com/v1或第三方兼容服务而Anthropic管道的请求则被转发到anthropicUpstream。两套管道共享底层的限流、循环保护、监控等中间件但上游地址、API Key替换、模型重写等配置完全独立互不干扰。这样设计的好处是显而易见的。你可以在一个网关后面同时管理对接不同供应商的多个API终端统一监控和管控策略而无需为每个客户端单独部署代理。对于我这样经常在GPT-4和Claude-3之间切换使用的开发者来说管理效率提升了一大截。2.2 以“省钱”和“防滥用”为核心的控制策略Savor的绝大多数功能都围绕着这两个核心目标展开。我们来看看它是如何实现的1. 循环保护 (Loop Guard)这是防止“Token泄漏”的杀手锏。大模型的工具调用Function Calling功能非常强大但设计不当的提示词或工具逻辑可能导致AI反复调用同一个工具陷入死循环。Savor的机制很简单但有效它在内存中维护一个基于“会话ID 工具调用模式”的计数器。如果在短时间内例如10秒窗口检测到完全相同的工具调用模式发生了多次默认3次就会立即熔断向客户端返回一个结构化的错误而不是将请求继续转发给上游API。这直接避免了无意义的API调用和Token消耗。我在实际测试中故意构造了一个会循环调用“获取时间”工具的提示词Savor在第三次调用时果断拦截为我省下了一次可能长达几十轮循环的API费用。2. 上下文截断 (Context Truncation)大模型按Token收费而对话历史Context是消耗Token的大户。对于很多任务我们并不需要完整的上下文。Savor允许你配置只保留最近N轮对话。它的实现并非简单粗暴地丢弃最老的消息而是以“用户发言”为轮次边界进行智能截断确保保留下来的每一轮对话都是完整的包含用户消息和AI的回复。这意味着当你设置contextTruncation: { keepRounds: 2 }时模型看到的永远是最近两轮完整的问答对这对于维持对话连贯性同时控制成本非常有效。3. 命令系统 (Command System)这是将控制权交还给用户的创新设计。除了后台配置你还可以在对话中直接输入特殊命令来实时控制上下文。例如输入\翻译后跟文本Savor会截断所有历史上下文只将当前这条“翻译”指令和文本发送给模型。这特别适合需要模型“忘记”之前对话、专注于当前独立任务的场景。而\1、\2这样的数字命令则可以动态指定保留最近1轮或2轮历史。这个功能让我在编写代码时可以随时让模型“忘记”之前复杂的讨论只关注最新的代码片段既灵活又节省了大量Token。2.3 面向生产环境的运维考量Savor并非一个玩具项目它在设计之初就考虑了生产部署的需求。配置热更新这是我最欣赏的特性之一。修改config.js中的绝大多数配置如上游地址、限流规则、API Key后服务无需重启1秒内自动生效。这得益于一个文件监听器fs.watch和一套精心设计的热加载逻辑。对于线上服务这意味着你可以动态调整限流阈值、切换备用API节点而不会中断任何正在进行的用户会话。只有修改端口、HTTPS证书等需要重新绑定系统资源的配置时才需要重启服务。全面的可观测性内置的Web监控看板提供了近乎实时的数据可视化。你可以看到总请求数、成功/失败率、Token消耗趋势、预估成本图表以及详细的请求日志。所有数据都持久化在SQLite数据库中并会自动清理8天前的旧数据防止磁盘爆满。这对于分析API使用模式、定位异常请求例如某个IP突然暴增的调用量至关重要。健壮的安全与隐私特性除了基础的CORS白名单和Helmet安全头Savor还内置了隐私信息过滤功能。它会自动检测并过滤请求和响应中的手机号、身份证号、邮箱等敏感信息在日志和监控看板中用[FILTERED]替代。这避免了调试日志意外泄露用户隐私的风险。同时API Key替换功能确保了你配置在Savor中的真实API Key永远不会通过网络传输到客户端客户端只需要使用一个占位Key即可。3. 从零开始的部署与配置实战理论说得再多不如动手部署一遍。下面我将以最推荐的Docker方式带你完成一次完整的Savor部署和客户端对接。3.1 环境准备与Docker部署首先确保你的服务器或本地开发机已经安装了Docker和Docker Compose。这是目前最简洁、依赖最少的部署方式。# 1. 创建一个专属目录并进入 mkdir -p ~/savor-deploy cd ~/savor-deploy # 2. 拉取必要的配置文件 # 使用curl从GitHub仓库直接下载docker-compose.yml和默认的config.js curl -sSL -o docker-compose.yml https://raw.githubusercontent.com/chziyue/savor/main/docker-compose.yml curl -sSL -o config.js https://raw.githubusercontent.com/chziyue/savor/main/config.js # 3. 关键一步编辑配置文件填入你的上游API信息 # 使用vim或你喜欢的编辑器打开config.js vim config.js现在我们来仔细看一下config.js里需要修改的核心部分。假设你使用的是OpenAI官方API和一个第三方的Claude兼容服务// config.js 关键配置节选 module.exports { // OpenAI协议上游地址 - 指向你的OpenAI兼容API upstream: https://api.openai.com/v1, // 例如官方API // 如果你的服务商不需要/v1后缀或使用其他路径可以通过upstreamSuffix覆盖 // upstreamSuffix: /v1/chat/completions, // Anthropic协议上游地址 - 指向你的Claude兼容API anthropicUpstream: https://api.anthropic.com, // 例如官方Anthropic API // 同样可通过anthropicUpstreamSuffix调整路径 // anthropicUpstreamSuffix: /v1/messages, // 服务监听配置 port: 3456, // HTTP服务端口 host: 0.0.0.0, // 监听所有网络接口 // API Key保护功能强烈建议开启 // 这里配置的是真实的API Key它们只会被发送到上游不会暴露给客户端 apiKeyOverride: { enabled: true, apiKey: sk-your-real-openai-api-key-here // 替换为你的真实OpenAI Key }, anthropicApiKeyOverride: { enabled: true, apiKey: sk-ant-your-real-anthropic-api-key-here // 替换为你的真实Anthropic Key }, // 模型重写可选强制将所有请求的模型指向一个特定模型 // modelOverride: { enabled: true, model: gpt-3.5-turbo }, // 循环保护配置 loopGuard: { enabled: true, threshold: 3, // 10秒内相同模式出现3次则熔断 windowMs: 10000 // 时间窗口10秒 }, // 限流配置基于客户端IP rateLimit: { enabled: true, windowMs: 60000, // 统计窗口时长1分钟 maxRequests: 100, // 每个IP每分钟最多100请求 lockedDuration: 300000 // 触发限流后锁定5分钟300000毫秒 } };编辑保存后一键启动服务# 4. 使用docker-compose在后台启动服务 docker compose up -d使用docker compose logs -f可以实时查看启动日志确认服务是否正常运行。看到类似[Savor] Server is running on http://0.0.0.0:3456的输出就表示成功了。注意默认的Docker镜像chziyue/savor:latest会从Docker Hub拉取。如果你身处网络环境特殊的地区可能会拉取缓慢或失败。这时你可以考虑使用国内镜像源或者按照项目README中的“自行构建镜像”部分从GitHub源码本地构建。3.2 客户端配置详解网关跑起来了接下来就是让客户端连接它。这里的核心在于理解Savor的透明代理特性它完全模拟了官方API的接口所以客户端的配置几乎就是把原来的官方API地址换成Savor的地址。对于OpenAI协议客户端如OpenClaw找到OpenClaw的配置文件。通常位于~/.openclaw/openclaw.jsonLinux/macOS或%APPDATA%\.openclaw\openclaw.jsonWindows。修改其中的baseUrl指向你部署的Savor服务并确保以/v1结尾。API Key可以填写任意值如sk-savor因为真实的Key已经在Savor的apiKeyOverride中配置了。// ~/.openclaw/openclaw.json 配置示例 { models: { providers: { my-savor-gateway: { // 给你的网关起个名字 baseUrl: http://你的服务器IP:3456/v1, // 关键地址/v1 apiKey: sk-any-string-here // 这里填什么都行Savor会替换它 } } } }对于Anthropic协议客户端如Claude Code找到Claude Code的设置文件通常在~/.claude/settings.json。修改baseURL指向Savor服务地址注意这里结尾没有/v1。// ~/.claude/settings.json 配置示例 { apiConfiguration: { baseURL: http://你的服务器IP:3456, // 关键地址没有/v1 apiKey: sk-ant-any-string // 同样任意值即可 } }配置完成后重启你的客户端。此时所有通过客户端发起的请求都会先到达你的Savor网关再被转发到真正的API服务商。你可以在Savor的监控看板http://你的服务器IP:3456上实时看到请求记录和统计图表。3.3 监控看板与日常运维访问Savor的监控看板你会看到一个清晰的数据面板。主页展示了今日/近7天的核心指标请求总量、成功/失败分布、Token消耗区分输入/输出以及根据你配置的单价估算出的成本。日志页面是排查问题的利器。这里记录了每一笔请求的详细信息时间戳、客户端IP、请求模型、消耗的Token数、响应状态码和耗时。你可以通过搜索框快速过滤特定IP或模型的请求。如果某次请求触发了循环保护或限流这里也会有明确的标记。运维常用命令# 查看服务运行状态和实时日志 docker compose logs -f savor # 停止服务 docker compose down # 重启服务例如修改了需要重启的配置后 docker compose restart savor # 进入容器内部用于高级调试 docker compose exec savor sh对于追求更高可用性的生产环境你可以考虑使用pm2在宿主机直接管理Node.js进程或者结合Kubernetes进行容器编排。但就我个人经验而言对于中小规模的使用Docker Compose方案已经足够稳定和便捷。4. 高级功能与深度调优指南当基础服务稳定运行后我们可以根据实际需求对Savor进行更精细化的调优和高级功能配置。4.1 精细化限流与客户端管理默认的限流策略每分钟100次可能不适合所有场景。比如你可能希望对某些受信任的IP放宽限制或者对疑似爬虫的IP进行更严格的封锁。配置详解 在config.js的rateLimit部分你可以调整以下参数windowMs: 限流统计的时间窗口毫秒。设为3600000就是一小时。maxRequests: 在时间窗口内允许的最大请求数。lockedDuration: 触发限流后客户端被锁定的时间毫秒。设为0则表示永久锁定需要手动解锁。通过API管理客户端 Savor提供了管理接口方便你查看和操作限流状态。# 查看所有被限流或监控中的客户端状态 curl http://localhost:3456/rate-limit/status | jq . # 假设发现IP为192.168.1.100的客户端被误锁可以手动解锁它 curl -X POST http://localhost:3456/rate-limit/reset?clientId192.168.1.100 # 在紧急情况下例如误配置导致所有客户端被锁解锁所有客户端 curl -X POST http://localhost:3456/rate-limit/reset?alltrue这个API在集成到自动化运维脚本中时非常有用。例如你可以设置一个监控当某个重要服务的IP被意外锁定时自动调用解锁API。4.2 启用HTTPS与安全加固如果Savor部署在公网或者你需要通过互联网访问监控看板启用HTTPS是必须的。方案一使用自签名证书适合内网或测试在Savor项目目录下执行mkdir -p certs cd certs openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout key.pem -out cert.pem \ -subj /CCN/STBeijing/LBeijing/OMyOrg/CNsavor.local然后在config.js中启用HTTPShttps: { enabled: true, port: 3457, // HTTPS监听端口与HTTP(3456)不同 keyPath: ./certs/key.pem, certPath: ./certs/cert.pem }客户端需要将baseUrl中的http改为https端口改为3457。由于是自签名证书客户端可能需要额外配置以跳过证书验证这取决于具体客户端。方案二使用Let‘s Encrypt等权威证书适合公网如果你有域名可以使用Certbot等工具获取免费证书。将得到的fullchain.pem和privkey.pem文件放入certs目录并在配置中指定路径即可。这是生产环境的推荐做法。安全配置建议CORS白名单在config.js中配置cors.origin只允许你前端应用所在的域名访问防止跨站攻击。监控看板访问控制目前的看板没有密码验证。如果部署在公网建议通过Nginx反向代理为其添加HTTP Basic认证或者限制访问IP。4.3 成本控制与Token估算优化Savor面板上的成本估算依赖于一个可配置的Token单价系数。默认的估算模型可能和你的实际API供应商计价方式有出入。调整Token估算系数 在config.js中找到tokenEstimation配置项。它定义了不同类型字符的Token折算系数。tokenEstimation: { // 默认系数基于常见LLM的Tokenizer如cl100k_base近似估算 defaultChar: 0.25, // 普通字符如英文、中文 digitChar: 0.6, // 数字字符 jsonStructChar: 0.7, // JSON结构字符如 { } [ ] : , whitespaceChar: 0.5 // 空白字符 }例如如果你使用的API对中文Token的消耗特别高某些按字计费的国产模型你可以适当调高defaultChar的值让成本估算更接近真实账单。你可以通过对比一段时间内Savor的估算值和API后台的实际消耗来校准这些系数。利用命令系统主动节流 养成在对话中使用命令的习惯是更直接的省钱方式。场景A翻译/总结独立内容直接使用\翻译 待翻译文本。这会抛弃所有历史上下文让模型只处理当前指令通常能节省70%以上的Token。场景B多轮调试后聚焦最新问题在进行了多轮代码调试对话后输入\2然后提出你的新问题。这样模型只会看到最近两轮对话既能保持一定的上下文关联又避免了为遥远的、已解决的问题历史付费。5. 常见问题排查与实战经验在实际部署和使用Savor的过程中你可能会遇到一些典型问题。下面是我总结的排查清单和实战心得。5.1 客户端连接失败问题排查表问题现象可能原因排查步骤与解决方案客户端报错Connection refused或Cannot connect to proxy1. Savor服务未启动。2. 防火墙/安全组阻止了端口。3. 客户端配置的地址或端口错误。1. 运行docker compose ps或pm2 list确认Savor进程状态。2. 在服务器上运行curl http://localhost:3456测试本地访问。3. 检查服务器防火墙如ufw和云服务商安全组规则确保3456端口开放。4. 核对客户端配置的baseUrl的IP和端口是否与Savor服务监听的一致。客户端报错404 Not Found或Invalid URL1. OpenAI协议客户端未在地址后加/v1。2. Anthropic协议客户端错误地加了/v1。1.OpenAI客户端确保baseUrl格式为http(s)://host:port/v1。2.Anthropic客户端确保baseURL格式为http(s)://host:port无/v1。3. 查看Savor日志确认请求是否被正确路由。请求成功但模型返回权限错误或无效模型1. Savor中配置的上游API地址或API Key错误。2. 模型名称不匹配。1. 检查config.js中的upstream和apiKeyOverride配置确保其指向有效且有权访问的API服务。2. 尝试在Savor中开启modelOverride强制将所有请求转发到你有权限的模型如gpt-3.5-turbo测试基础连通性。3. 查看Savor日志中转发给上游的完整请求体确认模型名和Key是否正确。监控看板无法访问1. 浏览器访问地址错误。2. Savor的dashboard功能被禁用。1. 默认地址是http://服务器IP:3456。确保端口正确。2. 检查config.js中features.webDashboard是否为true。5.2 性能与稳定性相关经验关于数据库锁与性能Savor使用SQLite存储日志和统计信息。在高并发写入场景下虽然对于LLM API网关QPS通常不会极高可能会遇到数据库锁问题。Savor从v0.5.4版本引入了异步写入队列WriteQueue将日志批量写入显著降低了锁冲突概率。如果你发现日志记录延迟或错误可以查看日志中是否有SQLITE_BUSY相关错误。必要时可以考虑将数据库文件放在更快的磁盘如SSD上。内存使用监控Savor的循环保护和限流功能需要在内存中维护一些状态如计数器、用户数据。虽然项目内置了闲置数据清理机制TTLMap但在长期运行且客户端IP非常多的情况下内存会缓慢增长。建议在生产环境中使用pm2或容器监控工具为Savor进程设置内存上限如512MB并配置在内存超限时自动重启。日志文件管理除了数据库Savor还会输出文本日志文件默认在logs/目录。建议配置日志轮转Log Rotation防止单个日志文件过大。如果你使用Docker可以通过docker-compose.yml中的日志驱动配置来实现如果使用pm2可以使用pm2-logrotate模块。5.3 我踩过的“坑”与最佳实践“透明”代理的副作用Savor是透明代理意味着它会把客户端的请求头包括可能存在的认证头原样转发。如果你的上游API对请求头有特殊校验例如某些第三方网关会检查特定的Header这可能会导致问题。解决方案是在Savor的转发逻辑中添加请求头改写中间件但这需要修改源码。一个更简单的办法是确保你的客户端只发送必要的、标准的头。流式响应Streaming的监控对于流式响应Savor的Token统计是在流式传输完成后通过估算响应文本来完成的。这意味着在流式传输过程中监控看板上的Token数不会实时增长。这是设计使然并非Bug。对于需要实时监控流式调用成本的场景目前只能等调用结束后查看统计。Docker容器时区问题早期版本中Docker容器内默认是UTC时区导致监控看板中“今日统计”的时间范围不对。v0.5.0之后版本已经通过在Dockerfile中设置TZAsia/Shanghai环境变量修复了这个问题。如果你自行构建镜像或发现时间不对检查时区设置即可。配置热更新的“陷阱”虽然大部分配置支持热更新但修改port,host,https等涉及网络绑定的配置后必须重启服务。一个常见的错误是在config.js中修改了端口然后一直等待却不生效。记住这个口诀“网络证书需重启其他配置热更新”。这个项目在我管理多个AI应用和团队协作的过程中已经成为了不可或缺的基础设施。它带来的不仅仅是经济上的节省更重要的是那种对资源使用情况一目了然的掌控感。从最初的简单代理到如今集成了监控、管控、安全于一身的网关Savor的迭代路线非常清晰地反映了一个开发者在真实使用场景中遇到的痛点。如果你也在为管理大模型API而烦恼不妨花点时间部署一下它相信你很快就能体会到这种“把钱花在刀刃上”的踏实感。