ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent工具调用治理实战:从LangGraph失控到Dogwood平台搭建

AI Agent工具调用治理实战:从LangGraph失控到Dogwood平台搭建 先交代一个背景我把公司内部的一套 AI Agent 平台从零搭起来的时候最头疼的其实不是模型效果而是工具调用太“自由”。有一回周五晚上一个只读的订单查询工具因为 Agent 在多轮对话里把参数理解错了直接变成写接口调用往生产库里塞了一条脏数据。那一刻我彻底想明白一件事AI Agent 平台搭建重头戏不是把 LangGraph 跑通而是给工具调用立规矩。我们内部把这套治理体系叫做 Dogwood它负责工具注册、契约校验、权限控制、限流熔断还有整条链路的可观测性。这篇文章就是 Dogwood 从零到上线的完整工程拆解给正在做 Agent 平台或者准备接 LangGraph 的同学一个可复用的参考。1. 工具调用为什么会失控拆开 LangGraph 看“手”是怎么乱动的1.1 LangGraph 里工具调用的本质是一个“模型说了算”的执行器不管你是用create_react_agent还是手动编排ToolNodeLangGraph 里工具调用的链路其实非常直白模型在推理时如果觉得需要外部信息或执行某个动作就输出一个结构化指令里面包含工具名和参数 JSONLangGraph 的节点负责解析这些指令找到对应的 Python 函数把参数传进去执行然后把执行结果再塞回消息列表交给模型继续推理。这个设计本身没有问题问题出在“谁来约束模型的指令”。模型不是严格按照你的工具清单来行动的它会猜。它猜工具名、猜参数格式、猜返回结构。我见过最典型的翻车场景是这样# 模型输出这样的 tool_call 并不罕见 tool_call { name: query_order_status, arguments: {order_id: 20230415-001, date: 下周三} }date字段期望的是YYYY-MM-DD格式结果模型给你传一个“下周三”。如果没有一层严格的校验器这个工具就会直接报错而 Agent 拿到的反馈是“参数错误”它并不知道怎么修正于是整个多轮任务卡死。更危险的是如果模型幻觉出一个并不存在的工具名恰巧这个工具名又跟某个真实存在但功能相反的接口重名后果就不只是任务失败了。1.2 我接完二十三个工具之后看到的四个失控症状工具少的时候问题不明显。工具一多症状就全冒出来了。第一个是命名混乱。同一个“查订单”的需求库里同时存在query_order、get_order、order_detail三个工具功能几乎一样description 写法还各不一样。模型每次选哪个全靠随机甚至连工具维护者自己都搞不清该给 Agent 暴露哪一个。第二个是参数自由发挥。模型在生成 JSON 参数的时候特别喜欢“填一个看起来合理的值”。比如一个工具要求user_id必须属于当前会话用户但模型可能从上下文里翻出一个历史用户 ID 填进去。工具执行后返回的是别人的数据更可怕的是这个数据还会被模型读取并呈现给当前用户。第三个是没有权限边界。大部分团队第一版工具都是直接把函数丢给ToolNode没有任何权限判断。等于所有用户都能通过 Agent 间接调用所有工具哪怕这个工具背后连着内部的删除接口。我在前面提到的“只读工具被当写接口用”本质就是这个原因。第四个是并发被打爆。Agent 的多轮循环意味着一个用户请求在几秒内可能触发五到十次工具调用。如果模型在一个步骤里同时发起三四个tool_calls工具的瞬时 QPS 会直接翻倍。多个用户同时在线时下游服务很快就扛不住了。这个问题的表现是“Agent 平台一上线就卡死”但根因根本不在模型而在工具调用层没有流量控制。1.3 失控的根因工具调用本质是远端代码执行想明白上面这些现象之后我的结论是工具调用不是普通的函数调用它等于在 Agent 平台上开了一个“模型可控的远程执行通道”。模型的输出天然带有不确定性你把它直接映射到代码执行中间没有任何隔离、校验、审计的话出问题只是时间问题。所以 Dogwood 的核心定位很明确它不是一个 Agent 编排框架而是一个夹在模型和工具之间的治理层。它不关心你的业务逻辑怎么写它只管三件事这个工具能不能被调用、该不该在此时被调用、调用了之后结果能不能安全可信地返回给模型。这三件事做好了Agent 平台才算有了基本盘。2. 契约先于代码工具注册中心是 Dogwood 的第一块基石2.1 把工具的“使用说明书”结构化工具给模型用的本质是一份结构化说明书。LangChain 里每个工具都有name、description、args_schema但实际项目中大家写得很随意。Dogwood 做的第一件事就是把这些随意的东西收敛成注册中心里的正式契约不允许任何人绕过注册表直接把函数挂到 Agent 上。我们定义了一个ToolDescriptor结构from pydantic import BaseModel, Field from enum import Enum class ToolPermission(str, Enum): READ read WRITE write EXEC exec class ToolDescriptor(BaseModel): name: str version: str description: str input_schema: type[BaseModel] output_schema: type[BaseModel] | None None permission: ToolPermission timeout: float 10.0 idempotent: bool False rate_limit_key: str allowed_roles: list[str] [] sensitive_fields: list[str] []每个工具上线前必须填写这份契约。其中description是最关键的字段它决定模型什么时候调用这个工具。我踩过很多坑之后总结出一个有效的写法模板先说明工具的用途再说明什么情况下不要用最后给一个典型参数示例。# 好的 description description ( 用于查询用户的订单状态输入 order_id 精确匹配 返回订单当前状态与物流单号。仅当用户主动询问订单状态时使用 如果用户需要修改订单或退货不要调用本工具应调用 order_modify 工具。 示例order_id20230415-001。 )这里有一个容易被忽略的点description本身就是模型的提示词的一部分。你写“不要调用本工具去修改订单”模型是会真的读到的。宁可多写几行也不要让模型自己去猜。2.2 参数校验不能只靠 pydanticinput_schema解决了类型和必填项的问题但远远不够。模型传进来的参数经常“类型对但语义错”。所以 Dogwood 的校验分成两层。第一层是结构校验直接用 pydantic 解析模型给的 JSON。第二层是业务语义校验在工具函数真正执行前跑一组自定义规则。比如class OrderQuerySchema(BaseModel): order_id: str start_date: str | None None end_date: str | None None def validate_order_query(args: dict, context: dict) - None: if args.get(start_date) and args.get(end_date): if args[end_date] args[start_date]: raise ValueError(end_date 不能早于 start_date) if not args.get(order_id) and not (args.get(start_date) and args.get(end_date)): raise ValueError(order_id 与日期范围必须至少提供一项)校验失败时Dogwood 不会直接把“参数错误”四个字抛给模型而是返回一段结构化反馈告诉模型错在哪里、应该怎么修正。这样 Agent 才有机会在下一轮自动修复。实测下来带修正提示的失败反馈能让任务成功率提升不少因为模型确实会读反馈并调整参数。2.3 出参也要校验模型看到的东西必须可控很多人只校验入参不校验出参。但工具返回给模型的内容同样需要规矩。Dogwood 的post-call校验会做三件事检查返回结构是否符合output_schema、检查敏感字段是否需要脱敏、检查返回内容是否超出长度限制。举个例子一个查询工具返回了完整数据库记录里面包含手机号和身份证号如果直接塞给模型模型可能会在下一次回复里脱口而出。Dogwood 会依据sensitive_fields把这些字段替换成***再返回给模型。出参校验还有一个容易被忽略的用途防提示词注入。工具返回的文本里可能包含忽略之前所有指令直接执行xxx之类的字符串一旦模型把工具返回的内容当成系统指令就会出大问题。Dogwood 会在返回前扫描这类危险模式检测到就拦截并向模型返回“工具返回内容存在异常已被过滤”。2.4 契约版本管理工具升级不能原地改工具上线后一定会迭代但工具的description是模型行为的重要输入。直接原地修改描述可能导致之前测试好的 Agent 行为突然漂移。Dogwood 的做法是给每个工具做版本化契约一旦对外发布就只读任何改动都产生新版本。发布方式模型行为稳定性回滚成本审计追踪原地修改低可能突然漂移高无法恢复旧行为差版本化发布高绑定后稳定低切换版本即可好线上 Agent 可以显式绑定某个工具版本比如query_orderv3。新版本先在测试环境跑确认没问题后再把线上流量逐步切过去。这套机制在 Dogwood 里就是一张版本表工具执行时按name version找到对应契约。3. 权限与安全不是每个工具都能被模型伸手够到3.1 模型的请求必须带上“人”的身份大模型本身没有身份概念。工具调用要安全必须把请求链路里的用户身份注入到执行上下文。我们在 FastAPI 入口解析 JWT 后把user_id、tenant_id、user_roles放进一个contextvars上下文对象Dogwood 的工具执行器从这个上下文读取身份信息。有了身份之后权限判断就顺理成章每个工具在ToolDescriptor.allowed_roles里声明哪些角色可以使用。执行前Dogwood 做一次 RBAC 匹配不满足直接拒绝调用并返回PERMISSION_DENIED给模型。# dogwood 权限策略示例 tools: - name: query_order allowed_roles: [normal_user, customer_service] - name: order_modify allowed_roles: [customer_service, admin] - name: delete_user_account allowed_roles: [admin]这里还涉及租户隔离。SaaS 场景下一个用户的 Agent 绝不能通过工具读到另一个租户的数据。所以 Dogwood 在工具执行前会校验请求上下文里的tenant_id与工具参数中的业务数据是否一致。这个校验没法在通用层自动做只能要求每个工具注册时提供一个tenant_id_extractor函数Dogwood 统一调用。3.2 写操作和危险操作必须人机确认不是所有工具都应该由模型自动执行。我们将工具按permission分为三类read类直接放行write类和exec类默认需要人工确认。这里的“人工确认”不是产品层面的按钮而是执行链路的强制卡点。在 LangGraph 里这个机制可以用interrupt()配合图状态实现。当模型请求调用一个write类工具时图执行到确认节点会暂停前端弹出确认框展示“Agent 将要执行修改订单状态为已发货”用户点击确认后图恢复执行真正调用工具函数。def call_tool_with_confirmation(state): tool_call state[next_tool_call] descriptor dogwood.get_tool(tool_call[name], state[tool_version]) if descriptor.permission in (write, exec): # 挂起并等待用户确认 confirmed interrupt({tool_name: descriptor.name, params: tool_call[arguments]}) if not confirmed: return {messages: [{role: tool, content: 用户取消了该操作}]} result dogwood.execute(tool_call, state[user_context]) return {messages: [{role: tool, content: result}]}这个设计从一开始就把“模型误调用写接口”的风险控制在发生之前。很多团队觉得人工确认打断体验但对于平台型系统宁可牺牲一点流畅度也不要在生产环境承担失控风险。3.3 返回内容脱敏与数据最小化工具返回给模型的数据遵循最小化原则会减少很多麻烦。Dogwood 的脱敏模块支持两类处理字段级脱敏和内容级过滤。字段级脱敏最简单注册工具时声明sensitive_fields执行后对返回 JSON 中的相应字段做掩码处理比如手机号变成138****8000。内容级过滤稍微复杂一点针对的是工具返回的长文本比如客服聊天记录里可能包含用户无意间发送的银行卡号。Dogwood 内置了一个规则引擎用正则模式匹配常见敏感信息并替换。还有一点值得强调脱敏之后的返回也要控制体积。一个工具把三千行日志返回给模型模型会怎么处理它会把无关内容也读进上下文导致 token 消耗飙升还分散注意力。Dogwood 的max_tokens_per_tool_result限制会在返回前截断超长内容并附加一句“以上内容已截断如需完整日志请调用日志详情工具”。3.4 审计日志每一次工具调用都要能追溯安全体系的最后闭环就是完整、不可变、可查询的审计日志。Dogwood 每次执行工具都会写入一条审计记录包含时间戳、trace_id、user_id、工具名和版本、入参摘要、返回状态、耗时、命中策略等。这里有个小设计我们不记录完整入参而是记录经过脱敏的摘要。因为完整参数可能包含敏感业务数据审计库如果被拖走反而成了新的数据泄露出口。但在必要的时候摘要加上 trace_id 足以定位到具体事件。审计日志使用独立存储只追加不可修改定期归档。4. 并发、限流与熔断Agent 扛并发不是加机器就能解决4.1 理解 Agent 平台的“放大效应”很多初做 Agent 平台的人以为“扛并发”就是把 LLM 推理服务或者后端 API 的水平扩容做好。实际上工具调用层的并发模型比普通 Web 服务更凶险。一个用户请求进来Agent 要经过多轮推理。每轮推理如果模型决定调用工具就会产生一次工具调用。我们用线上数据统计过一次完整任务的工具调用次数平均是五次复杂任务甚至到十次以上。也就是说工具层的 QPS 是用户请求 QPS 的五到十倍。而且模型经常在一个步骤里并行发起多个tool_calls这让工具调用流量呈脉冲式尖峰不是平滑的。所以 Dogwood 的并发治理不能用简单的“加几台机器”来解决。它必须在工具执行前做限流在工具依赖的下游异常时做熔断在工具卡死时做超时兜底。这一套组合拳才是 Agent 平台“扛并发”的真正答案。4.2 令牌桶限流给工具调用装上刹车限流算法里令牌桶最适合这种突发流量场景。它允许一定程度的突发但长期平均速率可控。Dogwood 在内存里为每个rate_limit_key维护一个令牌桶支持用户级、工具级、全局级三种维度。import asyncio import time class TokenBucket: def __init__(self, rate: float, capacity: int): self.rate rate # 每秒补充令牌数 self.capacity capacity # 桶容量允许突发量 self.tokens capacity self.updated_at time.monotonic() self.lock asyncio.Lock() async def acquire(self): async with self.lock: now time.monotonic() self.tokens min( self.capacity, self.tokens (now - self.updated_at) * self.rate ) self.updated_at now if self.tokens 1: return False self.tokens - 1 return True实际限流参数怎么定不能拍脑袋。我们的做法是先对每个后端服务做压测找到单实例能够承受的 QPS 上限然后乘一个安全系数作为工具层限流阈值。比如订单查询接口压测到 80 QPS 时成功率开始下降那就把工具限流阈值设在 50 QPS留出余量。还要注意限流的维度设置。用户级限流防止单个用户霸占太多资源工具级限流保护下游服务全局级限流保护整个 Agent 平台不被流量冲垮。三层并发生效任何一个维度拒绝都会向模型返回“工具调用过于频繁请稍后重试”。4.3 熔断与降级下游挂了不能让 Agent 死磕限流解决的是流量过载熔断解决的是下游故障。实践中我们见过这种情况Agent 依赖的某个内部 API 挂了但限流器没反应于是每一次工具调用都卡在超时边缘模型反复重试把已经故障的下游拖得更惨同时用户的请求全部卡住。Dogwood 的熔断器是一个经典状态机closed正常放行open直接快速失败half_open试探恢复。当一个工具调用的连续失败率超过 50% 且最近一分钟请求数超过 20 时熔断器从closed切到open持续时间 30 秒。这期间工具调用不会真正执行而是立刻返回“当前服务暂不可用”。class CircuitBreaker: def __init__(self, threshold_failure_rate: float 0.5, min_requests: int 20, open_seconds: float 30): self.threshold_failure_rate threshold_failure_rate self.min_requests min_requests self.open_seconds open_seconds self.status closed self.failures 0 self.total 0 self.opened_at 0 def allow(self) - bool: if self.status open: if time.monotonic() - self.opened_at self.open_seconds: self.status half_open return True return False return True def record(self, success: bool): if self.status half_open: self.status closed if success else open if not success: self.opened_at time.monotonic() self.failures 0 self.total 0 return self.total 1 if not success: self.failures 1 if self.total self.min_requests: return if self.failures / self.total self.threshold_failure_rate: self.status open self.opened_at time.monotonic()熔断器打开之后Agent 不一定只能失败。我们在工具返回里增加一个suggestion字段告诉模型“当前工具不可用可以尝试降级方案”。比如天气查询服务挂了模型可以改用另一个工具查询本地天气。这种降级路径虽然能力弱一些但至少让用户感受到 Agent 还在工作。4.4 超时与幂等最后一道兜底防线工具调用必须有默认超时我们设置的是 10 秒。模型等工具执行结果的过程是同步的一个工具卡死意味着整轮推理卡死更严重的是模型可能会带着超时报错继续运行产生更多错误重试。幂等处理同样关键。模型的重试机制不可靠一个创建工单的工具执行成功了但网络超时让模型以为失败于是再次调用同一个工具生成了一张重复工单。Dogwood 的做法是如果工具声明了idempotentTrue在入参里要求调用方必须传入request_idDogwood 缓存这个 ID 对应的执行结果。同一个request_id再次到达时直接返回第一次的执行结果不再重复执行。4.5 压测验证护栏对性能的影响很多人担心加这么多治理逻辑会拖慢工具调用。我们用 Locust 做了一轮压测对比模拟 100 个并发用户、每个用户触发 5 次工具调用下游服务是一个简单的查询 API。状态用户并发工具 QPS错误率p99 耗时后端 CPU无限流无熔断10052018.3%3.2 s95%限流熔断超时1004800.6%1.1 s61%加了治理层之后整体吞吐几乎不变但错误率和后端压力显著下降。原因很简单原来服务在过载边缘疯狂抖动大量请求超时重试实际有效吞吐反而低。护栏让服务始终运行在健康区间。这个结果也验证了一个观点给工具调用立规矩不是额外的开销而是稳定的必要条件。5. 可观测性建设每次工具调用都要有据可查5.1 一条链路上的 trace_id 贯穿Agent 平台的排查难点在于调用链很长HTTP 请求进来LangGraph 跑多轮循环每轮可能有多个工具调用每个工具调用又嵌套下游服务。没有可观测性的时候用户说“我的任务失败了”你根本不知道是哪一轮、哪个工具、哪次参数出的问题。Dogwood 的解法是从入口生成一个trace_id通过contextvars传递到图的每一个节点、每一个工具调用。所有日志、指标、审计记录都带上这个trace_id。工具调用后端服务时也会把这个trace_id放进 HTTP header 传递下去形成全链路关联。5.2 指标埋点要覆盖“被拦下来的请求”可观测性最容易忽略的是治理层自己产生的拦截事件。我们给 Dogwood 埋了这样几类指标工具调用总数、成功数、失败数、耗时分位数、限流拦截数、熔断拦截数、权限拒绝数。每个指标都带tool_name、version、user_role标签。这些指标放到 Grafana 里看效果非常直观。比如某个工具突然限流拦截数飙升说明用户的并发请求超过了预设阈值某个工具权限拒绝数异常说明模型在试图调用不该调用的工具这往往是description写得不够明确需要优化。5.3 通过一次线上事故看 Trace 的价值有一次线上反馈某个用户问“帮我查一下上个月的账单总额”Agent 最终回复“暂不支持”。照理说这个功能是有的。我们通过trace_id拉出完整调用链发现 Agent 在第一轮调用了账单查询工具但工具抛错“日期格式错误”。关键在返回给模型的错误信息过长模型没有仔细读直接在第二轮回复了“暂不支持”。根因很清楚Dogwood 的校验错误虽然带了修正提示但堆在错误堆栈最后面模型根本没注意到。我们做了两个改进一是缩短错误信息把“修正建议”提到最前面二是格式化错误输出让模型更容易解析。改完之后类似任务的完成率明显提升。如果没有 trace 把链路拉到这一步这种问题只能靠猜。5.4 日志记录的原则脱敏摘要 结构化字段工具调用的原始参数和返回往往包含敏感业务数据全量记录既不安全也耗费存储。Dogwood 采用的结构化日志方案是记录入参摘要、出参摘要、耗时、状态、触发规则。入参摘要会对脱敏字段做掩码处理比如只记录user_id是否为空、order_id的长度和前缀。这样既保留了排查线索又不会把用户完整数据写进日志系统。6. 接进 LangGraph 项目的完整流程和踩坑记录6.1 接入方式包一层 Dogwood 执行器Dogwood 不入侵 LangGraph 的内部逻辑它只做一件事把注册表里的原始工具函数替换成“经过治理层包裹的执行器”。具体来说LangGraph 的ToolNode接收一个tools列表我们把原始函数放入注册表后用dogwood.wrap(original_tool)生成一个带校验、限流、熔断、审计的新函数塞进tools列表。from langchain_core.tools import tool from langgraph.prebuilt import ToolNode registry ToolRegistry() registry.register(OrderTool()) # 原始业务工具 # Dogwood 包装后的工具才进入 LangGraph wrapped_tools [dogwood.wrap(desc, registry) for desc in registry.tools()] tool_node ToolNode(wrapped_tools)这里有个关键点dogwood.wrap返回的不是普通函数而是一个tool装饰后的 LangChain 工具对象且args_schema直接取自注册表里的input_schema。这样 LangGraph 在解析模型输出时用的就是 Dogwood 的契约而不是业务函数自己带的松散类型注解。6.2 完整接入步骤第一步在项目启动时初始化注册中心把所有工具扫进注册表并做一次自检schema 是否能正确解析、依赖服务是否可连通、权限配置是否合法。自检不通过的工不会被加载宁可让 Agent 少一个能力也不能让一个坏工具上线。第二步在 FastAPI 入口写一个中间件解析用户身份并写入 Dogwood 的上下文。没有这一步权限和租户隔离就是空谈。from fastapi import Request import dogwood app.middleware(http) async def inject_user_context(request: Request, call_next): token request.headers.get(Authorization, ) user parse_token(token) dogwood.set_context(user_iduser.id, tenant_iduser.tenant_id, rolesuser.roles) # 为整个请求生命周期生成 trace_id dogwood.set_trace_id(uuid4().hex) return await call_next(request)第三步把包装后的工具列表传给 LangGraph 的create_react_agent。第四步在 Agent 图的边上加一个“执行前确认节点”负责处理write/exec类型工具的人工确认。第五步启动后观察指标确认工具调用流程平稳。6.3 踩坑一pydantic 版本兼容问题这是我们接入时遇到的第一个大坑。LangChain 生态早期版本对 pydantic v1 和 v2 的兼容性不一致业务团队的工具函数用的是 v2 的BaseModel但 LangGraph 内部的ToolNode在某些版本里期望 v1 的 schema导致工具注册时直接抛校验异常。解决办法是统一环境里的 pydantic 版本并在注册表里强制使用同一个基类。现在新项目直接上 pydantic v2并固定相关依赖的版本号。这一点写进团队规范避免新人引入老依赖把环境搞乱。6.4 踩坑二同步阻塞工具吃掉事件循环LangGraph 的异步执行流复用同一个事件循环。如果工具函数是同步阻塞的比如用requests.get调下游 HTTP 接口在并行调用多个工具时阻塞会占住事件循环其他协程全部排队整个 Agent 看起来像死了一样。我们当时的处理方案是把所有工具统一改成异步实现。对于一些无法改造的遗留同步工具使用asyncio.to_thread包一层并通过Semaphore限制线程池并发的最大数量。import asyncio class SyncBridge: def __init__(self, max_threads: int 32): self.semaphore asyncio.Semaphore(max_threads) async def call(self, func, *args, **kwargs): async with self.semaphore: return await asyncio.to_thread(func, *args, **kwargs)线程池并发数要按工具的实际耗时调整。我们设过 128结果某个慢工具把线程池占满其他工具全部等待后来调低到 32并在注册表里对慢工具单独限制并发。6.5 踩坑三工具返回超长导致上下文爆炸有一回 Agent 在分析日志时一个工具返回了十万字符的原始日志模型一轮就把上下文撑爆了token 成本直接翻倍任务还失败了。问题出在出参没有限制。Dogwood 后来加了max_tokens_per_tool_result策略默认 2000 个 token。超长返回会被截断并附带一个截断标记。对于确实需要完整内容的场景我们要求工具自行设计分页参数让模型按需拉取。6.6 工具灰度与回归验证最后分享一个 Dogwood 上线后沉淀下来的工具发布流程。新工具不会直接暴露给生产 Agent先走“影子模式”Dogwood 记录模型会以什么参数调用这个工具、会返回什么结果但不真正执行。观察一周确认模型调用路径符合预期再切换为真实执行。每次修改工具的description或参数 schema都要跑一遍回归测试集。我们维护了五六十条典型用户问题覆盖每个工具的主要使用路径。发版后自动跑一遍观察工具被调用的路径是否和设计一致。这一招能提前发现大多数因描述改动引发的模型行为漂移。Dogwood 这套东西不是一天长出来的最初只是一个几十行的参数校验函数后来在一次次线上事故里被逼着长成了完整的治理层。我的体会是工具调用治理不应该等到出事了再补它应当是 Agent 平台的第一优先级。先给这双“手”立好规矩再放开模型的能力边界平台才能真正站稳。如果你也在做 Agent 平台可以先把契约注册、权限校验、限流熔断这三件事做起来亲手压一遍测你会感受到“有规矩的工具调用”和“裸奔的工具调用”之间完全是两种运行体验。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进