
用户问:“我的 ORDER-A 到哪里了?”模型能读懂问题,却不知道此刻的物流状态。它需要先提出查订单请求,让业务程序验证权限、查询订单,再把结果交回模型组织回答。
接入 SGLang 后,这个来回仍然存在。模型生成工具调用,不会使数据库自动接上;返回了工具结果,也不代表这份结果仍属于用户当前要问的订单。下面沿同一条 call_order_a 把请求、执行、回填和用户改口接起来。
本文使用 SGLang 的 OpenAI 兼容 Chat Completions 工具消息形状,依据 2026-09-12 核验的官方 Tool Parser 文档编写。订单、状态、标识和消息均为教学示例,未启动模型或访问真实订单服务。模型、模板、解析器与实际部署版本仍须一起验证;示例不承诺任意模型配置直接可用,也不使用 Responses API 的另一套字段。
第一次调用:让模型提出一个可以检查的请求

配图中的 name / arguments 是应用侧待执行对象的抽象示意,ORDER-EXAMPLE 也是示意订单名;下面改用 ORDER-A 展开实际协议消息形状,两者不应混作同一个原始响应。
应用发送用户问题及允许的工具定义。本例只开放一个只读工具 get_order_status,参数只包含订单号,登录身份由服务器持有。以下 Python 展示第一次非流式请求的完整关键参数;client 是指向实际 SGLang /v1 服务的客户端,model_name 是该服务的实际模型名称,需由接入者配置。
messages = [
{"role": "system", "content": "依据订单工具结果回答;查询失败时说明未查到,不编造物流。"},
{"role": "user", "content": "我的 ORDER-A 到哪里了?"},
]
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "查询当前登录用户有权访问的订单状态",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
}]
first = client.chat.completions.create(
model=model_name, messages=messages, tools=tools, stream=False
)
SGLang 官方工具解析示例使用这个调用入口,并从返回的 message.tool_calls 中取得工具名与参数。文档中的解析器选项对应模型输出格式;外层接口兼容,不等于内部模板或模型生成形式都一样。
假设第一次响应的 choices[0].message 如下。这里展示的是 assistant 消息,不是整个 HTTP 响应:
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_order_a",
"type": "function",
"function": {
"name": "get_order_status",
"arguments": "{\"order_id\":\"ORDER-A\"}"
}
}]
}
arguments 在这份消息里是 JSON 字符串,需要解析后再校验。call_order_a 是这条模型工具调用的关联 ID,不是订单号,也不是当前登录用户的身份证明。应用应保存原始 assistant 消息及其所属任务,不能只留下函数名和参数后丢掉 ID。
先检查返回里是否真的存在工具调用,再逐个检查名称、参数和标识。本例只接受一条只读查单调用;如果模型返回多条或提出修改操作,接入程序按本例的范围拒绝执行并记录原因,不能默默只执行第一条后把整轮当作完成。非流式示例也不意味着可以把流式分片直接当作完整参数;流式接入需按调用位置收齐相关片段后再校验,本篇不展开流式拼装器。
工具执行期间,模型并没有替你等待数据库

得到工具调用后,先执行服务器侧检查,再访问业务数据。模型的工具说明写着“当前用户”,只是在描述用途,真正的授权不能靠这句话完成。
假设服务器已经验证了登录会话,得到 principal。本例的业务流程是:解析 JSON 字符串,要求只有合法的 order_id;确认工具名在只读允许列表中;用可信的登录主体及订单号查询其可访问范围。若系统采用租户隔离,还需带上服务器确认的租户范围。不能从模型参数中取一个 user_id 来代替 principal。
一种简单的订单归属模型,可以用下面的参数化查询说明授权位置。它是作者设计示例,不是 SGLang 内置订单 API;实际系统存在代理人、组织角色等规则时,应替换为真实授权策略。
SELECT order_id, status
FROM orders
WHERE order_id = :order_id
AND owner_user_id = :authenticated_user_id;
两个参数分别来自校验后的工具参数与服务器登录身份,不能把模型文本拼接成 SQL。这样,查询对象和访问范围在同一次受约束读取中确定。没有匹配行时,本例向模型返回统一的“未找到可访问订单”,不额外泄露该订单是否属于别人。未经授权时也不应先取出整份敏感订单,再指望模型自行忽略。
假设业务服务在本例返回“运输中”。交给模型的结果只保留本次回答必要的字段,例如 ok、订单号、物流状态;内部用户标识、访问令牌与数据库诊断不需要进入工具正文。业务查询耗时可能来自数据库、第三方物流接口或网络,两次模型生成都很快也不能抵消它。
至此,应用才拿到了业务结果。下一步还要把它配回提出这次调用的 assistant 消息,而不是当作一条脱离上下文的新用户消息。
第二次调用,要带上这次任务需要的上下文

官方文档的“Execute the Tool”与“Send Results Back to Model”先追加模型的工具调用消息,再追加带 tool_call_id 的 tool 消息,最后再次调用模型。按这份形状,本例成功路径的第二次请求是:
second_messages = [
{"role": "system", "content": "依据订单工具结果回答;查询失败时说明未查到,不编造物流。"},
{"role": "user", "content": "我的 ORDER-A 到哪里了?"},
{
"role": "assistant",
"content": None,
"tool_calls": [{
"id": "call_order_a",
"type": "function",
"function": {
"name": "get_order_status",
"arguments": '{"order_id":"ORDER-A"}',
},
}],
},
{
"role": "tool",
"tool_call_id": "call_order_a",
"name": "get_order_status",
"content": '{"ok":true,"order_id":"ORDER-A","status":"运输中"}',
},
]
second = client.chat.completions.create(
model=model_name, messages=second_messages, tools=tools, stream=False
)
这段代码把上一轮消息展开写出来,是为了让配对关系可见。实际程序应保留经过校验的原始 assistant 消息,并把业务执行的真实结果序列化为 tool 内容,不能把示例中的“运输中”固定写进服务。工具结果的 ok、status 是本例业务字段,协议关联靠 tool_call_id;二者的职责不同。
可以沿 ID 验收这一轮:assistant 提出 call_order_a,业务执行记录绑定它,回填 tool 的 tool_call_id 仍是 call_order_a,最终回答再绑定发起第二次模型调用的任务。顺序错了、ID 写成另一条调用,或者结果内容属于 ORDER-B,均不能靠“模型大概看得懂”放行。
假设第二次模型返回“ORDER-A 正在运输中”,这才形成了本例的一次完整来回:模型调用 1→授权与查询→模型调用 2→当前任务输出。模型也可能继续提出工具调用或返回不符合要求的内容;本例的应用预算到第二次为止,遇到进一步调用就结束为未完成并记录原因,不无限执行,不将“恰好两次”宣传成所有 Agent 的固定机制。
如果查不到可访问订单,仍可在同一个配对位置放入安全的失败结果,例如 {"ok":false,"error":{"code":"not_found_or_not_accessible"}},让当前任务说明查询未成功。工具返回失败不等于可以省略关联消息或使用上一条成功结果补空。
Session-Aware Radix Cache 文档说明,其中的 session_id 标记缓存引用,不会自动追加或重建对话。即使复用了缓存,应用仍需传入这轮预期的上下文。这里选择的是上面明确的消息列表;若切换到其他有状态接口,应重新核对那个接口的恢复契约,不能把所有叫 session 的字段视作同一件事。
用户改口后,旧结果还算数吗

接下来给同一条调用加一个竞态。假设应用将当前用户任务标记为 generation 41,正在等 call_order_a 查询 ORDER-A;用户随后说“改查 ORDER-B”,应用创建 generation 42。generation 是本文的应用侧任务版本号,不是 SGLang 工具协议字段。
| 应用事件顺序(教学推演,非日志时间线) | 当前 generation | 处理 |
|---|---|---|
| 为 ORDER-A 注册调用 | 41 | 保存 call_order_a→generation 41、ORDER-A、pending |
| 用户改问 ORDER-B | 42 | 创建新任务上下文;旧任务标为不再对外输出 |
| ORDER-A 的查询返回 | 42 | 在受保护的状态检查中发现所属 generation 为 41,记为旧任务结果,不追加到 B 的消息列表 |
| 为 ORDER-B 注册并完成 call_order_b | 42 | 通过身份、ID、订单与任务配对后,才进入 B 的第二次模型调用 |
这比给结果附带一个 ID 多了一步:应用要在决定使用结果的那一刻,核实该任务仍然有效。本例至少保存连接或会话范围、generation、call ID、工具名、参数摘要、执行尝试和终态。相同字符串 ID 在不同任务里出现,不应自动覆盖同一条记录;索引键必须包含实际任务范围。
同一工具调用还可能经历超时与重试,因此业务回调最好通过应用持有的待执行记录或 attempt 标识关联,不接受外部返回任意一个 call ID 就自行归入当前任务。若订单服务不回显订单号,仍能依据创建该执行时保存的参数与尝试记录配对;若回显了不同订单号,应视为异常而非替换原参数。
下面是结果接受规则的伪代码。state_lock 代表任务切换与结果接受共用的串行化机制;单进程可用同一事件循环或锁,多实例则需要等价的条件更新,不能把它当作已实现的分布式事务。
在 state_lock 内:
按应用保存的 task/generation/call_id/attempt 取待执行记录
不存在 → 记录 unmatched,不回填,不猜测归属
已有终态 → 记录 duplicate,不追加第二条 tool 结果
不属于当前 generation → 记录 stale,不进入当前任务消息
超过本次接受期限 → 记录 late,不让成功覆盖超时终态
工具名、参数归属或可信执行来源不匹配 → 记录 mismatch,拒绝回填
否则标记终态并构造该任务的 tool 消息,排入该任务的后续生成
检查与状态转换需要作为一个整体完成,否则“检查时还是 41、追加时已经是 42”的空隙仍会串单。消息列表也应属于具体任务,不能让 A 的后台回调拿到一个已经改装成 B 的共享列表。
还存在第二个更晚的窗口:A 的工具结果可能在切换前已经被接受,A 的第二次模型调用也已发出,但回答到达时用户才刚改问 B。因此,最终文本的输出端仍要按 generation 再检查一次。切换后可尝试取消旧模型请求以减少工作量;即便取消未及时生效,旧回答也不能进入新任务的输出。已经展示给用户的旧内容无法通过事后检查收回,这个规则约束的是尚未交付的内容。
超时、重复与错配,不能靠重新生成参数解决

超时需要一个明确的截止点。若当前任务在期限内没有结果,本例只给这条调用确定一次 timeout 终态,并在任务仍有效时回填匹配 call ID 的失败内容。之后同一执行的迟到成功保留在受控追踪中,不能再追加第二条互相矛盾的 tool 消息。generation 已失效时,则连这条失败消息也不应送入新任务。
| 观察 | 本例的应用决定 | 为什么 |
|---|---|---|
| 同一尝试回调两次,结果相同 | 只接受首次终态;后一次记 duplicate | 避免重复续写或向消息列表追加两份工具结果 |
| 同一 call ID 出现不同结果或不同参数归属 | 标记冲突,停止自动使用异常结果 | ID 不是结果真实性证明,不能靠“最后到的覆盖前面的”处理 |
| 返回了未登记的 ID 或属于其他会话的记录 | 拒绝回填并查关联路径 | 不能为凑齐一轮消息,把未知结果配给当前唯一工具 |
| 当前只读查询超时 | 按剩余期限决定是否进行有界的新尝试;记录 attempt,仍只产生一个被接受的逻辑结果 | 新尝试需要与原尝试区分,旧回调不能越过终态规则 |
| 当前任务换成另一单 | 旧任务结果留作追踪,不供新任务作答 | 工具是否真的执行与用户是否仍在等待它,是两件事 |
应用内部重试一次业务读取,不一定需要再让模型生成一份同样参数;但每次尝试仍要重新满足当前授权、任务有效性与请求预算。若旧尝试已经确定为本例的终态,就不能用新尝试悄悄改写已回填的失败,需作为新的受控动作处理。追踪信息也应按权限和保留策略保存,不等于无限期保存完整订单内容。
对于退款、修改地址等写操作,超时风险不同:操作可能已经执行,只是响应丢了。查询结果配对用的 tool_call_id 不是天然的业务幂等键;还需由业务服务定义稳定操作标识、状态查询与幂等策略,并执行所需授权。模型重新生成参数,不能证明退款尚未发生。本例只开放查询,不把这套只读重试方案直接当作退款实现。
把一次完整任务的时间留下来

调优时,分别记录第一次模型请求、授权与业务查询、第二次模型请求,以及最终输出是否被当前 generation 接受。SGLang 的服务指标用于观察模型服务,应用记录则补上业务等待与任务有效性。两边应使用可关联的请求标识和明确的时间口径;generation、工具 call ID 与底层服务请求 ID 不能互相冒充。
对刚才的 A→B 推演,记录不能只写“订单查询成功两次”。它应该保留:A 查询完成但结果 stale,未启动或未交付 A 的后续回答;B 的调用按 call_order_b 正确配对并完成当前任务。前者可计入后台工作量,却不能算用户当前问题的有效完成量。这样才能解释为什么模型服务看似都成功,用户仍可能收到不相关答案。
第一次接入可以只开放这个只读工具,按相同消息形状逐项验证成功、未找到可访问订单、非法参数、超时、重复回调、错配 ID、用户切单及迟到模型回答。通过条件不是日志里出现一个 tool_calls,而是每个被交付的回答都有当前任务、授权范围、对应工具结果和输出接受记录。这些场景在本文均未实跑,示例给出了实现与验收的具体位置。