本地脚本里跑得挺顺的智能体,一旦要给别人调用就露馅:多个请求串了会话、跑一半超时、上游一挂整条链路卡死、出问题还查不到是哪一步。这篇不讲协议怎么写、客户端怎么配,只讲把它变成服务的那些工程细节:接口边界怎么划、状态放哪、超时并发怎么设、降级和日志怎么留、上线前拿什么验。
本地脚本里跑得挺顺的智能体,一旦要给别人调用就露馅:多个请求串了会话、跑一半超时、上游一挂整条链路卡死、出问题还查不到是哪一步。这篇不讲协议怎么写、客户端怎么配,只讲把它变成服务的那些工程细节:接口边界怎么划、状态放哪、超时并发怎么设、降级和日志怎么留、上线前拿什么验。
先把预期摆正:封装成 API 不是加个 web 框架壳子就完事,而是要把「一次调用」重新定义成一个可被外部依赖的契约。契约包括输入范围、输出结构、可接受耗时、失败时怎么告诉对方。契约没定清楚,后面加超时、加降级、加日志全是补丁摞补丁。下面按这个顺序拆。
先划边界:哪些能力值得暴露成接口
一次调用先定义成「输入 + 输出 + 可接受耗时」,而不是把内部工具链全暴露出去。调用方只该看到目标结果和必要的中间状态,比如「正在调用搜索」这种进度,而不是内部每一步的函数名和参数。边界画不清,后面所有超时和降级都没法设,因为你不知道哪一步算「必须完成」。
- 长任务别硬扛同步请求:超过调用方愿意等的时间,就改成提交任务拿 ID、轮询或回调取结果的两段式。
- 区分「对话型」和「执行型」两类接口,前者要带会话标识,后者应当无状态、可重放;混在一个端点里后面很难改。
- 不适合暴露的情况:依赖本地文件路径、需要人工点确认、或者一次调用会改外部不可回滚的数据,这类先留在内部。
- 输入做白名单校验:限定字段、长度和类型,别让调用方直接塞一段能改写提示词的文本进来。
判断标准很简单:如果一个能力失败后你没法用一句话告诉调用方「重试有没有用」,它就还不适合对外。什么时候不适用这条:内部服务之间调用,重试语义可以靠约定解决,那可以放宽。
状态放哪:会话、上下文与幂等键
会话记忆不要塞进进程内存,多实例一扩就丢。放 Redis 或数据库,键用会话 ID,并设过期时间。过期时间要按业务定,不是越长越好,长会话既占空间又会让上下文裁剪越来越难做。上下文的裁剪规则放在服务侧,不要指望调用方每次都传全量历史。
- 会话 ID
- 调用方传入或服务端生成,用来定位这段记忆,必须能从头贯穿到日志
- 幂等键
- 同一次业务动作重复提交时用它去重,重复请求返回同一结果
- 上下文窗口
- 服务端自己裁,优先级是最近对话、关键指令、工具结果,其余压缩或丢弃
- 过期策略
- 按业务活跃度设,过期后新会话重新开始,别让旧记忆污染新任务
| 场景 | 状态放哪 | 要注意什么 |
|---|---|---|
| 单轮执行型任务 | 不存 | 接口无状态,重放安全,幂等键必带 |
| 多轮对话 | Redis 或数据库 | 会话 ID 贯穿,并发写同一会话要排队或拒绝 |
| 带工具写操作 | 数据库 + 幂等键 | 重试前先确认没写进去,避免重复执行 |
| 长任务两段式 | 任务表 | 提交返回任务 ID,轮询或回调取结果,任务状态要可查 |
超时、并发与降级:别让一个慢请求拖垮整条链
把总耗时拆成「排队 + 模型调用 + 工具调用」三段分别设上限,任一段超时就走既定分支,而不是整体一刀切。并发上限按上游额度定,宁可排队也别打满;打满后先返回排队中或稍后重试,比直接 429 体验好。具体的上游额度到 /price-watch 看当时的数字,别照搬别人的配置。
- 拆耗时预算给排队、模型、工具各设一个上限,总和要小于调用方愿意等的时间,留出一点余量
- 定并发闸门按上游额度设令牌或信号量,打满时返回排队状态而不是直接失败
- 写降级顺序换更快的模型、砍掉非必要工具、退回只给部分结果,每一步都在响应里标明是降级结果
- 限定重试范围只对幂等且可判定的失败重试,工具写操作失败先确认没写进去再决定
降级不是万能药:如果降级后的结果已经不能满足调用方的业务需求,那不如直接返回明确的失败,让对方自己决定要不要重试。什么时候不适用「宁可降级也别失败」:结果会被写进数据库或对外展示的场景,错的结果比没有结果更麻烦。
日志与上线前的那一版评测
每次调用至少记:请求 ID、会话 ID、用了哪个模型、每段耗时、工具调用次数与结果、是否降级、最终状态。缺哪项,后面出问题就查不动。日志里别落原始敏感内容,长文本截断存;要看完整链路就靠请求 ID 串起来。上线前拿一组固定用例跑一遍,用例改动要留版本,否则你没法判断这次是变好了还是碰巧。
| 用例类型 | 要验证什么 | 不通过怎么办 |
|---|---|---|
| 正常输入 | 主链路能不能走通、输出结构是否稳定 | 先查接口契约有没有变 |
| 超时 | 哪一段先超、降级分支有没有触发 | 回头调那一段的耗时上限 |
| 工具报错 | 重试是否生效、有没有重复写 | 检查幂等键和写操作顺序 |
| 上游限流 | 排队或稍后重试是不是按预期返回 | 核对并发闸门设置 |
| 空输入 / 超长输入 | 校验和截断逻辑是否兜住 | 补输入白名单和长度限制 |
- 成功率:掉下来先看是不是某类用例集中失败
- 降级率:持续升高说明上游或工具在变慢
- 单次平均耗时:拆到三段看是哪段涨了
- 工具调用次数:异常增多往往是死循环或重试放大
评测用例不用多,覆盖正常、超时、工具报错、上游限流、空输入、超长输入这几类就够起步。每次改完提示词或换模型,把这组用例重跑一遍,结果留档。至于具体用哪家模型、当时的额度多少,到 /sites 核对当时的数字,别把配置写死在代码里。
下一步把你现在的智能体按上面的边界、状态、超时降级、日志评测四项过一遍,缺哪项补哪项;动手前可以先到评测页面看看同类服务在响应时间和稳定性上的常见表现,心里有个底。→常见问题
本地脚本跑得好好的,为什么一做成服务就各种问题?
本地是单请求、可人工盯着,做成服务后是并发、无人值守。会话状态、超时、重试、并发上限这些在脚本里根本不存在,一上多实例就暴露。先按上面四块补齐,再谈优化。
智能体要跑好几分钟,接口怎么设计才合理?
别做成长连接死等。改成提交任务返回任务 ID,调用方轮询或走回调;同时把进度写进状态存储,这样断了也能续查,不用重跑整条链。
服务上线后该看哪几个数?
先看成功率、降级率和每段耗时分布,这三项能覆盖大部分异常。具体阈值别照抄别人,拿你自己的历史数据定基线;模型单价和额度口径去 /official-api 和 /usage 对照当期数字。