注册并分享邀请链接,可获得视频播放与邀请奖励。

搜索结果 def_quality
def_quality 贴吧
一个关键词就是一个贴吧,路径全站唯一。
创建贴吧
用户
未找到
包含 def_quality 的推特
据 The Defiant,a16z 联合 DeFi 教育基金(DEF)向美 SEC 委员 Hester Peirce 牵头的加密任务组提交联合提案,敦促 SEC 设立安全港规则,建立去中心化交易协议(DEX)及其前端应用不属于《证券交易法》下“交易所”的可抗辩推定(Rebuttable presumption)。该提案提出了免于注册的“四大核心标准”:DEX 协议本身必须具备非托管、自主自动化执行、无许可访问以及可信中立性。两家机构援引 Peirce 委员关于“真正由自动化软件驱动的去中心化系统无需豁免”的表态,强调纯自主协议不涉及中介违约或欺诈风险,应直接给予明确的合规豁免而非繁琐审批。同日 a16z 还建议为有中介性质的中心化加密平台建立专门注册通道,允许其合规交易代币化证券及证券/非证券混合交易对。
显示更多
AWS 资深专家解决方案架构师 Daniel Abib 在官方博客里算了一笔账:同步调用时,Lambda 的计费时长几乎等于 agent 思考的时长,等待本身在按全量计算计费。 《在无服务器流水线中异步调用 Amazon Bedrock AgentCore agent 的模式》 在无服务器流水线里异步调用 Amazon Bedrock AgentCore agent,可以消除你的 AI agent 处理请求期间的空转计算成本。一个常见的例子是文档校验:在房地产融资的后台办公室里,agent 可以读取房产记录或贷款合同,推理信息是否完整、一致,然后返回一个下游步骤据此行动的结论。Amazon Bedrock AgentCore 提供了一个平台,让你能用任意框架或模型大规模地构建、连接和优化 agent。 这类 agent 带来一个传统流水线步骤没有的特性:它们在回答之前要思考一会儿。思考多久取决于提示词、模型和文档,但很少是即时的,而这个延迟改变了你调用它的方式。最常见的首版实现是用一个计算服务(比如 AWS Lambda 函数)调用 agent 并等待响应。函数等待期间什么都不做,但它仍在运行,每一秒都在计费。 值得看清成本到底落在哪里,因为调用的两侧计费方式不同。Amazon Bedrock AgentCore runtime(Amazon Bedrock AgentCore 的一项能力)采用基于消耗的计费模型,agent 空闲时不收 CPU 费用。例如,在等待大语言模型生成响应、或等待工具或 Model Context Protocol(MCP)调用返回的这段时间里,你只按内存计费,不按 CPU 计费。调用 agent 的那个计算服务没有这种特性。发出同步调用的 Lambda 函数、容器或 Amazon EC2 实例会一直阻塞,在 agent 响应之前始终持有(并支付)完整的计算配额。所以浪费不在 agent 一侧,而在调用方——它挂在一个打开的连接上空转。 这就让调用方的成本跟着 agent 的运行时间走。阻塞在 agent 上的函数,计费时长几乎等于整个处理时间;而启动 agent 后立即返回的函数,只按短暂的派发计费。解法是在等待期间释放调用方的计算资源,等 agent 出了结果再恢复流水线。本文展示三种做到这一点的模式(task-token 回调、直接服务集成、durable function),并与阻塞式反模式对比。 示例流水线 为了让各模式在同等条件下比较,我们把每个模式都跑在同一条流水线上,只更换调用 agent 的那一步。这条流水线是一个刻意简单、虚构的场景(房地产融资文档校验),选它只是为了把编排讲清楚。它不是本文的重点,它代表任何先调用 agent(或其他慢服务)、再对结果采取行动的流程,你可以把自家用例代入。 流水线有五个阶段: 1. Extract:一个 Lambda 函数对文档做 OCR 和文本提取。(提取是模拟的,所以场景不需要真实文档。) 2. Identify:一个 Lambda 函数对文档分类并设置路由标志(shouldOrganize、shouldValidate)。 3. Route:一个 Choice 状态根据这些标志决定流程走向。 4. Organize and Validate:一个 Parallel 状态在组织文档的同时,让 Amazon Bedrock AgentCore agent 在另一个分支里做校验。这个 Validate 分支是各模式之间唯一变化的部分。 5. Result:一个 Lambda 函数处理 agent 的结论,决定下一步动作(批准,或退回修改)。 图示如下:水平流程依次是 Start、Extract、Identify、Route(Choice)、并行的 Organize-and-Validate 阶段、Result、End,Validate 分支被标为可更换,图例列出了它的四种实现方式:阻塞反模式、模式 1(task token)、模式 2(直接集成)、模式 3(durable function)。流水线本身在每种情况下保持不变,只有 Validate 分支被换掉来演示各自的调用模式。 同一个 Amazon Bedrock AgentCore agent 服务全部四种情况。agent 检查每次调用并选择如何响应:如果收到 AWS Step Functions 的 task token,就在完成时唤醒那个执行;如果收到 durable function 的 callback ID,就唤醒那个 durable function;两者都没收到,就直接在响应里返回结论。这意味着你可以更换编排模式,而不必修改或重新部署 agent。 agent 如何不阻塞调用方就把控制权交回来 机制是 agent 的 action group 里的一个"归还控制权"动作。agent 推理结束后,调用一个 Lambda,把结果和 task token 发回 Step Functions。(后面的模式 2 会让 Step Functions 直接与 AgentCore 集成,完全省掉这个 Lambda。) 下面这段代码是这个 Lambda 的核心: The tool the agent calls once it reaches a verdict @tool def conclude_validation(approved: bool, issues: list, summary: str) -> str: verdict = {"approved": approved, "issues": issues, "summary": summary, "source": "agentcore"} # A Step Functions task token was passed: resume that execution if task_token: sfn.send_task_success(taskToken=task_token, output=json.dumps(verdict)) return "Step Functions resumed." # A durable-function callback ID was passed: resume the durable function if callback_id: lambda_client.send_durable_execution_callback_success( CallbackId=callback_id, Result=json.dumps(verdict).encode("utf-8")) return "Durable function resumed." # Neither was passed: this is a synchronous call, return the verdict inline return "Verdict recorded." 入口函数根据同样的信号决定在后台跑还是同步跑: @app.async_task async def validate_document_async(prompt, document, extracted_text): # Background work; conclude_validation fires the right callback when done agent = build_agent() await agent.invoke_async(message(prompt, document, extracted_text)) @app.entrypoint async def handler(event): task_token = event.get("taskToken") # passed by the task-token pattern callback_id = event.get("callbackId") # passed by the durable-function pattern # Asynchronous: start the work and return "accepted" right away if task_token or callback_id: asyncio.create_task(validate_document_async(...)) return {"status": "accepted"} # Synchronous: run now and return the verdict in the response agent = build_agent() await agent.invoke_async(message(...)) return verdict agent 就位之后,本文剩下的部分聚焦于调用它的四种方式。代码和基础设施定义都是示例中的摘录,用来演示每种模式。 调用 agent:四种方式 先讲阻塞式反模式以确立基准成本,再展示避开它的三个模式。 阻塞式反模式 最直接的实现:在同一个 Lambda 函数里调用 agent 并等待答案。它能用,而且实现简单——这正是它如此常见的原因——但函数在 agent 思考的整个期间一直活着。 // The Lambda function blocks here until the agent responds const response = await agentcore.send( new InvokeAgentRuntimeCommand({ agentRuntimeArn: AGENT_RUNTIME_ARN, payload: new TextEncoder().encode(JSON.stringify(payload)), runtimeSessionId: sessionId, }) ); // The function stays alive and billed for the entire time the agent is thinking. 函数的计费时长最终大约等于 agent 的处理时间。后面三个模式消除这段空闲成本,各自做出不同的取舍。特别是模式 2 使用 Step Functions 对 AgentCore Harness (InvokeHarness) 的优化集成,完全去掉 Lambda。 模式 1:task-token 回调 + 派发函数 这个模式在路径里保留一个 Lambda 做自定义逻辑,但去掉空闲成本。Step Functions 用 waitForTaskToken 集成调用这个函数——传入一个 task token 后暂停执行。函数用这个 token 启动 agent,然后在几秒内返回。执行保持暂停,不产生任何计算计费,直到 agent 用这个 token 调用 SendTaskSuccess 恢复它。 // Start the agent, pass the task token, and return without waiting const response = await agentcore.send( new InvokeAgentRuntimeCommand({ agentRuntimeArn: AGENT_RUNTIME_ARN, payload: new TextEncoder().encode(JSON.stringify({ ...payload, taskToken })), runtimeSessionId: sessionId, }) ); // Returning here does not complete the step. Step Functions stays paused until // the agent calls SendTaskSuccess with this task token. return { dispatched: true }; 对应的状态从上下文取出 token,并设置超时和心跳作为安全网——这样沉默的 agent 会让执行干净地失败,而不是无限期停在那里: "ValidateDispatch": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken", "Parameters": { "FunctionName": "${ValidateDispatcherFunctionArn}", "Payload": { "taskToken.$": "$$.Task.Token", "document.$": "$.document", "extractedText.$": "$.extract.extractedText", "executionId.$": "$$.Execution.Id" } }, "TimeoutSeconds": 120, "HeartbeatSeconds": 60, "Next": "AgentCoreValidation" } 成本。 Lambda 函数仍然会跑,但只跑到启动 agent 并返回为止:几秒钟,无论 agent 之后要跑多久。你只为这次短暂派发付费,不为等待付费——因为函数在 agent 干活时已经关掉了。等待由暂停的 Step Functions 执行承担,它不为空闲计算计费。这是与阻塞版本的关键区别:阻塞版本里函数的计费时间跟着 agent 的处理时间走。 模式 2:直接服务集成 当你不需要围绕 agent 调用写自定义代码时,可以去掉派发函数,让 Lambda 完全离开路径。Step Functions 可以通过 AWS SDK 服务集成直接调用 Amazon Bedrock AgentCore,所以 agent 的响应直接流入下一个状态。Validate 分支于是变成单个 Task 状态: "ValidateDirect": { "Type": "Task", "Resource": "arn:aws:states:::aws-sdk:bedrockagentcore:invokeAgentRuntime", "Parameters": { "AgentRuntimeArn": "${AgentRuntimeArn}", "RuntimeSessionId.$": "States.Hash($$.Execution.Id, 'SHA-256')", "Payload.$": "States.JsonToString($.prep.agentInput)" }, "ResultSelector": { "raw.$": "$.Response" }, "TimeoutSeconds": 120, "Next": "ParseVerdict" } 成本。 路径里没有 Lambda,所以没有空闲的 Lambda 计算可付。Step Functions 承担等待,Standard 工作流按状态转换计费而不是按等待时长计费——处理期间的实质成本只有 agent 本身。 模式 3:Lambda durable function 如果你更愿意用代码而不是状态机来表达编排,Lambda durable function 给你同样的成本行为。用 @aws/durable-execution-sdk-js SDK,流水线阶段变成 context.step 调用,并行工作变成 context.parallel,等待 agent 变成 context.waitForCallback。等待期间函数挂起,不计计算费。agent 用 SendDurableExecutionCallbackSuccess 恢复它。 // Suspend the function until the agent calls back const result = await ctx.waitForCallback( "validate-agentcore", async (callbackId) => dispatchAgentCore(callbackId, document, extractedText, executionId), { timeout: { seconds: 120 } } ); 成本。 一个函数承载整条流水线,但挂起等待 agent 期间不按计算计费。你只为挂起之间短暂爆发的那几次执行付费——与 task-token 模式相同的经济性——而不是为等待付费。 测量差异 重点不是某个具体数字。agent 的运行时间随提示词、模型和文档变化。重要的是 task-token 模式里两个值之间的关系:Validate 状态活跃了多久,对比派发函数实际被计费了多久。我们测试中的单次运行让这个关系可见: Step Functions, ValidateDispatch state Returned (TaskSubmitted): 14:08:19 <- the function returned and shut down Resumed (TaskSucceeded): 14:08:34 <- the agent woke the execution State active ........ 19.6s Lambda, dispatcher function (CloudWatch REPORT) Billed duration ..... 4.8s Result: the state was active for 19.6s, but the function was billed for 4.8s. The ~14.8s in between is wait time with no Lambda function running. 在 Step Functions 的事件历史里也能看到同样的事:task-token 模式下,TaskSubmitted 事件(函数返回)与 TaskSucceeded(agent 恢复流程)之间隔着 agent 的处理时间。同步调用没有这样的间隔。 下表总结了每种模式下 Validate 分支(前面图示中高亮的部分)的实现方式: • 编排器:阻塞=Step Functions;模式 1=Step Functions;模式 2=Step Functions;模式 3=Lambda(代码) • 路径中的 Lambda:阻塞=有,存活且计费;模式 1=有,但提前返回;模式 2=无;模式 3=durable function 本身(挂起) • 等待期间的空闲 Lambda 计算:阻塞=为整个等待付费;模式 1=无;模式 2=无;模式 3=无 • agent 调用周围的自定义代码:阻塞=有;模式 1=有;模式 2=有限(状态转换);模式 3=有 • 调用方与 agent 解耦:阻塞=否;模式 1=是;模式 2=否;模式 3=是 • 相对复杂度:阻塞=最低;模式 1=较高(token 和回调的 IAM);模式 2=最低;模式 3=中等(检查点/重放) 读这张表时有一个提醒:总流水线时长不是有用的比较指标,因为它被 agent 自己的推理时间主导——每次运行都不同,而且在所有场景里基本相同。有意义的差异是等待期间你为多少计算付了钱,这由上表和计费时长读数体现。agent 运行时间增长时,派发函数的计费时间保持平稳。上面的数字来自单次运行,把它们当作这个关系的示意而非基准,请测量你自己的负载。 如何选择模式 下表总结了取舍,帮你选择模式: • 调用方成本:阻塞=整个 agent 处理时间;模式 1=几秒(仅派发);模式 2=零(无 Lambda);模式 3=几秒(仅派发) • 集成工作量:阻塞=低;模式 1=中高(IAM、心跳、超时);模式 2=低(单个 Task 状态);模式 3=中等(检查点-重放模型) • 业务逻辑位置:阻塞=在 Lambda(前 + 后);模式 1=在 Lambda(前 + 后);模式 2=只在 Amazon States Language(ASL)(内置函数);模式 3=在 Lambda(顺序代码) • 最适合:阻塞=原型、短 agent;模式 1=自定义前后处理逻辑;模式 2=纯编排、无自定义代码;模式 3=单个函数里的复杂异步工作流 最佳实践 防 agent 永不回答。 给每个 waitForTaskToken 状态设置 TimeoutSeconds,让执行以 States.Timeout 失败,而不是无限期挂起。如果你的 agent 发送心跳,也设置 HeartbeatSeconds 以更快发现死掉的 agent。捕获错误并路由到失败或人工审核路径。 重试时使用稳定的会话 ID。 把 sessionId 设为从执行上下文派生的值(比如 Step Functions 执行名),这样重试会恢复同一个 agent 会话,而不是重新开始。task-token 模式里在派发函数中设置;直接集成模式里在 Task 状态参数中设置。 打开 AWS X-Ray。 在 Step Functions 和 Lambda 配置中启用 Tracing: Active。X-Ray 会精确显示 agent 思考了多久、调用方等待了多久,确认你的模式确实在间隔期间释放了计算。 按速度而不是按 agent 的负载来配派发函数。 派发函数只序列化请求并调用端点。256 MB 内存和 30 秒超时通常就够。重活都在 agent 那边。 成本 这些模式会产生 Lambda 计算、Step Functions 状态转换、durable function 执行存储的费用,以及所有方案共有的 Amazon Bedrock AgentCore runtime 和 Amazon Bedrock 模型推理费用。agent 和模型成本在四种情况下相同。架构只改变编排开销,以及阻塞反模式中浪费的空闲 Lambda 计算。当前价格请查看各服务的定价页。 结论 把 AI agent 放进流水线是简单的部分。让它被经济地调用,才是区分原型与生产设计的地方。阻塞在 agent 上的 Lambda 函数实现简单,却在悄悄烧钱——计费时间里的大部分都在等待。本文展示了三种规避方式:需要在路径里保留 Lambda 时用 task-token 回调;最简单的情况用直接服务集成;偏好用代码表达编排时用 durable function。三者由同一个 Amazon Bedrock AgentCore agent 驱动,它会根据被调用方式调整自己的响应。 完整示例(完整的 agent、状态机定义和 durable function)在 GitHub 的 sample-bedrock-agentcore-async-stepfunctions 仓库。想深入的话,可以看 Amazon Bedrock AgentCore 关于异步任务处理的文档。生产部署请使用 Amazon Bedrock Guardrails,对 agent 的输入输出实施负责任 AI 控制。 原文: #Bedrock# #Serverless# #StepFunctions#
显示更多
决定代码复杂度的东西不在代码里,在写代码的人的脑子里。LLM 碰不到它。 这是 软件工程师 Pol Alvarez Vecino 读完 Peter Naur 1985 年的论文后得出的结论。 为什么 LLM 没法让你的代码更简单 本文最初发表于 Medium( tl;dr:Peter Naur 的《Programming as Theory building》指出,真正的程序——他称之为 Theory,大写的 T——存在于工程师的脑子里。代码和文档只是下游的(因而不完整的)产物。我对 LLM 最大的抱怨之一,就是它写出来的代码有多啰嗦、复杂度是怎样到处蔓延的。我一直抱着一丝信念:也许我们可以用 LoC 或者独立代码路径数量之类的指标来约束它们。然而,读完 Naur 之后我意识到,我们想降低的那个复杂度是 Theory 的复杂度,不是代码的复杂度,而这方面没有任何可用的度量,因为它非常主观。 我最近读了 Peter Naur 那篇精彩的论文《Programming as Theory building》( LLM 能做什么、不能做什么的看法,也改变了我对如何给它们写提示词、当前 agent 系统的主要局限,以及结对编程为何如此有效的看法。 今天我只聚焦它和代码复杂度的关系。 如果你还没读过这篇论文,我真的建议你读一读。说实话,我写这篇文章的主要目的,就是让一些人去读原论文。它值得花这个功夫。这也是练习(或学习!)在 Solveit 里做精读的绝佳机会,因为在 Solveit 里这要容易得多:你可以在阅读过程中随时提问,钻进任何你感兴趣的兔子洞,或者直接让 Solveit 帮你把语言讲清楚。关于精读的更多信息见这篇博文( fork 我的对话记录快速上手( 话说回来,如果你还是决定不读,这里是论文的 tl;dr: 程序是构建和维护它的人所持有的 Theory:一种理解——程序如何与现实世界的问题相关联,哪些约束和权衡塑造了它,它为什么能工作,以及哪些改动符合它的设计。代码和文档是这套 Theory 的下游产物,永远无法完整地承载它。 工程师通过经验发展出这种理解:与用户交谈、观察故障、学习领域知识、观察系统在真实世界中的表现。这种理解指导着对相关性、相似性、简洁性和良好设计的判断。 LLM 不太擅长持续学习,不擅长和用户交谈,也不擅长在真实世界里体验事物。但这和复杂度有什么关系呢? 我想我们都同意:LLM 总体上倾向于让代码库的复杂度上升,如果没人管的话。原因有很多:它们没意识到某个方法已经存在,于是又写了一遍;它们写过度防御的代码,比如为不可能发生的边界情况做防护;或者过早地过度优化。顺便说一句,大多数前沿实验室从你消耗的 token 里赚大钱,所以它们多少有动机去推广 token 最大化。总而言之,LLM 很少遵循 KISS 原则。这个问题在你不看输出、纯 vibe-coding 的时候最严重。但即使你会审查代码,要想让程序保持简洁,也需要主动付出努力去尽量削减复杂度。 在我天真的日子里(大约两周前),我曾以为我们早晚能爬出这个复杂度的大坑。前沿实验室只需要在 RL 训练里加一些复杂度惩罚就行。他们可以用总 LoC 作为最小化的指标,但我们都同意,有时候一行代码比两三行更复杂。另一个选项是圈复杂度(cyclomatic complexity),它衡量独立代码路径的总数。读完 Peter Naur 之后我意识到,这些东西无法真正解决问题(也许能稍微缓解一点)。我们来看看为什么。 为什么代码复杂度是错的指标 在下面这个(我编的)例子里,我们想支持调用 OpenAI 和 Anthropic。假设所有的消息准备和重试逻辑完全一样,只有请求体参数略有不同,于是我们有两个不同的方法 call_openai 和 call_anthropic。 在第一个朴素版本里,我们有两个不同的类,带重复的样板代码(即 prepare 和 with_retries)。 分开 —— 指标会发现重复 class OpenAIClient: def complete(self, prompt): msgs = prepare(prompt) # 公共样板代码 y = call_openai(msgs) # 唯一不同的一行 return with_retries(y) # 公共样板代码 class AnthropicClient: def complete(self, prompt): msgs = prepare(prompt) y = call_anthropic(msgs) return with_retries(y) 一个直接的 refactor 是创建单个类,做到 DRY。按很多指标看这都是更好的实现:行数更少、Halstead 容量更好(V=N×log2(n),N 为程序长度,n 为词汇量)、可维护性指数(Maintainability Index)也更好。 合并版 —— 按指标看确实更好:DRY,行数更少 class LLMClient: def __init__(self, provider): self.provider = provider def complete(self, prompt): msgs = prepare(prompt) y = call_openai(msgs) if self.provider == "openai" else call_anthropic(msgs) return with_retries(y) 一般来说,第二个版本(或者类似减少 LoC 和重复的版本)复杂度更低。但是,如果我告诉你,下个月我们很可能就停止支持 Anthropic 了呢?在那种情况下,我更倾向于让它们保持分开,这样到时候我只要删掉包含 AnthropicClient 的那个文件就行。 当然,这种简单的例子很容易解决,尤其是现在 LLM 可以帮你写代码。但如果你的目标不是 2 个供应商,而是像 LiteLLM 那样支持 165+ 个供应商呢?那种情况下,直接用 LiteLLM 就行。但那样你就把 120 多万行 Python 代码放到了你和最终供应商之间。值得吗? 设计良好的 API 是抽象复杂度的绝佳方式。你有清晰的契约,不需要理解背后发生了什么。即使 LiteLLM 是一个庞大的包,它也可能不计入你的 Theory 总复杂度。LLM 推理端点早期的日子就是这样:「文本进,文本出」。然而,当契约不再可靠时,这一切就会崩塌。原因可能是它有 bug,可能是 API 背后藏着大量你拿不到的状态,也可能仅仅是你不确定某个新供应商特性是否被支持。 每当你被迫窥视 API 抽象层背后的深渊时,那份复杂度就成了你的问题。这个问题正变得越来越普遍。像 OpenAI 和 Anthropic 这样的供应商,越来越多地把数据藏在服务端,比如加密的压缩数据或推理 token(更多讨论见 如果你在快速推进、只用基础功能、想尝试很多供应商,那么 LiteLLM 或类似的库可能值得用。反过来,如果你看重控制力、调试和对技术栈的理解,那可能就不值得。不存在「正确的」复杂度(虽然我非常偏好第二种选择)。 进入 Theory 决定走哪条路的信息不在代码里。这些信息属于 Naur 所说的程序的 Theory 的一部分。迄今为止,LLM 几乎接触不到这些信息,因为它们活在人的脑子里。它们可以从 IM、邮件或其他书面文档里得到一些线索,但那些永远只是局部的(最好的情况下)。 其中一些信息可以作为上下文提供给 LLM,比如业务优先级、预期的产品变化、运维约束,以及早期决策背后的原因。这样做也许能改善它的选择,但这些仍然只是 Theory 的产物。它们无法完整地传递团队发展出这套 Theory 所依赖的经验和判断。 这些都很好,但如果我全身心投入 vibe-coding 和 token 最大化、完全不在乎代码呢?那样的话,这篇博客后面的内容说服不了你。如果你处于两者之间,我来描述一个我们在 亲身经历的真实情况,关于 Solveit 的计费系统。 剧透警告:在 最初的计费系统 Solveit 是一个平台,你可以在里面用 AI 在一个类 notebook 的环境里工作。环境是持久化的,所以我们对 LLM 用量、CPU、磁盘、内存和带宽收费。 最初的计划是向用户收月度订阅费(比如 5 美元)。这笔月度订阅费变成当月可以消耗的积分(credits),如果全部用完,下个月之前就得充值。我们先在一个更小的项目里测试了这套方法来验证它。 这套方法后来证明比我们想要的更复杂。第一,积分 + 订阅的机制会把人(比如我自己)搞晕。第二,它让代码在多个层面上更复杂。你得处理「先消耗月度订阅积分、再消耗普通积分」的所有逻辑,以及剩下的积分怎么办。在 Stripe 这一侧,它有两个不同的代码路径:手动充值和订阅服务。 对不熟悉的人来说,Stripe 订阅是一个全托管服务。Stripe 管理整个生命周期(扣款周期、发票、重试全在他们那边)。 你大致只需要这样创建订阅: stripe.Subscription.create(customer=cust_id, items=[{"price": "price_5usd_monthly"}]) 然后监听他们的 webhook,在订阅状态变化或付款到达时更新你的数据库(还有很多其他事件可以选择)。 直接收款则需要你启动一个 checkout session,让用户跳转到 Stripe 的域名填卡。这需要你提供一个 customer ID。那应该在什么时候创建 Stripe customer?用户注册时?他们尝试付款时?还是别的时机?全都是合理选项。 stripe.checkout.Session.create(mode="payment", customer=cust_id, line_items=[{"price": "price_5usd", "quantity": 1}], success_url="") 到目前为止还好吧?如果你对 Stripe 或支付没有太多经验,很可能你已经感到吃力,没法把这一切全装进脑子里。也许你设法把它简化成了: • 订阅 → 交给 Stripe 订阅服务管理 • 充值 → Stripe checkout 一个不明显的问题是:使用 Stripe 托管服务意味着你有重复的数据。一半数据在 Stripe 的后端,而你必须保证本地数据库和它同步。另一个问题是,调试的时候,你既要查 Stripe 的服务,又要查自己的数据库。比如,一笔付款没到账,是 Stripe 没发 webhook(「他们的错」),还是我们没把它存进数据库(「我们的错」)? Stripe 订阅服务很棒、很容易上手,但它是为支持海量用例而设计的。这意味着,即使设计得很好(它确实很好),这个 API 抽象最终也相当复杂。在这种情况下,你在用「卷起袖子自己写代码的复杂度」交换「学习 Stripe API 的复杂度」。 LiteLLM 和 Stripe 在不同规模上展示了同一个权衡:只要契约成立,外部抽象能极大地简化你的 Theory;但每当你需要调试、修改或超出契约去推理时,它隐藏的复杂度就变成你的了。 AAI 的做法 在 经过很多天的探索和讨论,我们最终定下了一个简单得多的系统。 首先,我们只做积分(credits),按用量收费。这是一个超级简单的模型(和 Theory!):充值积分,用多少付多少。 订阅一去掉,我们就可以删掉一大块用来保持同步的代码。剩下的付款路径只有两条:手动充值和自动充值。要做自动充值,你需要能保存客户的信用卡,以便随时扣款。而 Stripe checkout 不会保存信用卡,你只是让 Stripe 在他们的 UI 里处理这次付款。 长话短说,我们最终发现最简单的办法是:用户一注册就保存他们的信用卡。卡一旦在档,用户可以用它手动充值,也可以设置成自动充值。因为全部是我们自己处理的,同步问题几乎为零。我们只监听支付成功事件(没有订阅了!)。 结果就是,我们支付系统的 Theory 可以用一句话概括: 客户注册时添加信用卡,之后我们对该卡扣款——要么手动(充值),要么在余额不足时自动扣。 手动充值和自动充值现在走同一条支付路径。我们整个支付技术栈——拆在 Solveit 和 faststripe 之间——大约 300 行代码。 结果是非常低的复杂度,但这是数小时的探索、尝试和讨论换来的。我这里的解释充其量只是触及皮毛。 印度登场 那么上线那天发生了什么?一切顺利吗?没有。上线后我们发现,印度信用卡不支持你想什么时候扣就什么时候扣的 off-session 扣款。手动充值属于 on-session,仍然可以工作,因为用户会在我们的 UI 里看到一个类似 3DS 的验证流程;但自动充值不行。 在寻找解决方案时,我们发现 Stripe 托管订阅在印度确实能工作。为什么?因为 Stripe 替你绕开了所有这些复杂度。他们提前一天创建并持有 off-session 的支付意图(payment intent),这样银行就能在实际扣款前向用户发送预扣款通知或认证请求。 我们迁移出托管订阅时,就失去了这个特性。我问了一个前沿 LLM——我记得是 GPT-5.5——我们该怎么处理这个问题。你猜它给出的方案是什么? 用回 Stripe 订阅来处理 这个 LLM 提议的正是我们刚刚迁移出来的方案。读到这里,你会怎么说?你的意见是什么?我们应该迁回去吗? LLM 列出了两个选项:要么同时支持两套系统(随时可以开工,你一句话就行!),要么完全迁回旧的系统。我们做了什么?什么都没做。我们非常看重 Theory 复杂度,于是我们决定:让印度用户手动充值就好了(抱歉了各位!),换来一个更简单、更健壮的平台。 如果你不同意我们的选择,反思一下为什么不同意。真的,现在就停下来想一想。 这个问题没有正确答案,而这篇博客的目的之一,就是帮你看清你的答案来自哪里。你的论据是什么?更具体地说,它们是从哪里来的? 有很多场景下 Stripe 托管服务是更优的选择。举几个例子: • 公司收入最重要,手动充值的额外摩擦可能会让我们损失一些印度销售额,那我们应该迁回去(或者同时支持两套) • 销售团队用 Stripe Dashboard 的 UI,所有支付信息都放在我们自己的数据库里并不理想,因为他们没法在那里管理 我的观点是:你的代码的复杂度,真的取决于一大堆和代码无关的因素,而那些信息不在代码里。 隐藏的优势 那么为什么不干脆让 LLM 全权处理这些事呢?在我看来,最妙的答案是:我们的工作方式揭示了一个可能的商业机会。印度不支持 debit mandate(借记授权)的方式和世界其他地方不一样。Stripe 试图解决这个问题,但远远不是一个完整的解决方案。 在理解和简化流程与 Theory 的过程中,我们学到了产品之外有价值的东西。在我看来,这类洞察是可以转化为竞争优势的。 努力去理解,本质上就是一个简化 Theory 的过程。如果你放任 LLM 在复杂度上为所欲为,你可以很快产出大量代码。选择简化路线更长、更费劲,但长远来看我认为它是值得的,而且你可能会在路上发现隐藏的宝石。 原文: #LLM# #编程# #代码复杂度#
显示更多