恢复被中断的会话#
请求可能在没有完成时就结束了:连接断了而重试已经用完,用户按了停止,进程被杀掉。对话保存到了那个位置,你想从那里接着做,而不是输入“继续”再指望模型自己看出缺了什么。
resume() 就是没有新用户消息的 run()。会话被恢复,它的最后一条消息说明了它是怎么被中断的,请求从那里接着往下走。
try:
reply = await agent.run("Refactor the parser")
except ProviderTimeoutError:
reply = await agent.resume()
先问再恢复#
没有任何东西会记录“请求没有完成”:被杀掉的进程什么也写不了。留下来的只有历史,而它的最后一条消息就是记录。完成了的请求以一条没有请求任何东西、正常停止的助手回答结尾;其他任何结尾都是中断。
resume_point() 会读取会话并告诉你是哪一种,而不改动它。它不写入任何东西,也不调用任何模型。
point = await agent.resume_point()
if point is not None:
show_button("Resume", tooltip=point.description)
没有可恢复的内容时,它返回 None。对这些情况,resume() 会抛出 AgentResumeError,并带有 reason,所以你也可以直接试一下。
恢复会做什么#
由最后一条消息决定。ResumePoint 上的 kind 说明适用哪一行。
历史结尾是 |
是怎么走到这一步的 |
恢复会做什么 |
|---|---|---|
一条用户消息或 internal message |
模型从未回答:连接失败,或请求被取消了。 |
|
工具结果,且都在 |
工具已经完成,而下一次模型调用失败了。也包括在工具执行中途取消的情况,那时这次调用已经被作为“已取消”回答过了。 |
|
提供方暂停的一步 |
提供方把控制权交了回来,并且已经达到暂停次数的上限。 |
|
没有结果的工具调用 |
工具运行期间进程停止了。 |
|
停止原因为 |
流被故障切断了。 |
|
停止原因为 |
它达到了输出上限。 |
|
一个正常的回答 |
请求已经完成。 |
没有可恢复的内容。抛出 |
一次拒绝 |
模型拒绝了。 |
抛出 |
停止原因为 |
上下文窗口满了。 |
抛出 |
其他任何停止原因 |
本库不认识的取值。 |
抛出 |
空会话会抛出 "empty";历史以一条不对应任何调用的工具结果结尾,则抛出 "malformed"。
工具绝不会自己再次运行#
崩溃可能留下这样的状态:助手消息请求了两个工具调用,却只有一个结果。另一个可能已经运行,可能运行了一半,也可能根本没开始,而没有任何东西能说明是哪种。写文件或发消息的工具可能已经完成了它的动作,再运行一次就可能做两遍。
所以缺少结果的调用是被“回答”,而不是被重复执行。模型会得到一条失败的结果:
The process stopped while this tool was running, so it may or may not have completed, and whatever it changes may or may not have happened. Check the current state before running it again.(以上是发给模型的英文原文,意思是:进程在此工具运行期间停止了,它可能完成了也可能没有,它所做的改动可能发生了也可能没有;再次运行之前请先检查当前状态。)
然后由模型决定怎么做,通常是先查看当前状态,需要的话再重复这次调用。已经到达的那个结果会被保留,所以模型知道第一个工具已经运行过了。
当到来的是新消息而不是 resume 时,适用同样的规则。在追加新输入之前,任何没有结果的工具调用都会用这种方式关闭,因为提供方会拒绝带有这种调用的历史。以前,恢复会话会把被中断的整批调用都丢掉,包括已经到达的结果,于是模型会忘记那些工具已经运行过;现在这些结果会被保留。
完整的离线程序请 下载 resume.py:它在一次失败和一次崩溃之后恢复会话,并检查没有任何工具运行了两次。
async def main() -> None:
storage = SQLiteSessionExtension("resume.sqlite")
# A failure: the tool ran, then the connection dropped before the model saw the result.
agent = await Agent.create(FlakyModel(), config=AgentRunConfig("failure"), tools=[write_file], extensions=[storage])
try:
await agent.run("Write notes.txt")
except httpx.ConnectError:
pass
point = await agent.resume_point()
assert point is not None and point.kind.value == "call_model"
answer = await agent.resume()
assert answer.content == "Done: wrote notes.txt"
assert WRITTEN == ["notes.txt"] # resuming did not run the tool a second time
assert await agent.resume_point() is None # and there is nothing left to resume
# A crash: the process died after the model asked for two files and one was written.
# Nothing could be saved for the other, so its result is missing from the history.
calls = (ToolCall("a", "write_file", {"name": "a.txt"}), ToolCall("b", "write_file", {"name": "b.txt"}))
await storage.storage.create_session(session_id="crash")
for message in (
UserMessage(content="Write a.txt and b.txt"),
AssistantMessage(tool_calls=calls),
ToolMessage(tool_call_id="a", name="write_file", content="wrote a.txt", success=True),
):
await storage.storage.append("crash", "killed-request", message)
agent = await Agent.create(AnswerModel(), config=AgentRunConfig("crash"), tools=[write_file], extensions=[storage])
point = await agent.resume_point()
assert point is not None and point.kind.value == "recover_tools"
assert [call.id for call in point.unanswered_tool_calls] == ["b"]
answer = await agent.resume()
assert answer.content == "1 tool was interrupted" # the model was told, and decides what to do
assert WRITTEN == ["notes.txt"] # still nothing was run on its own
await storage.storage.close()
print("Resumed after a failure and a crash; no tool ran twice")
需要再次传入什么#
被中断的请求,除了它的消息之外,没有任何东西被保存。请像调用 run 那样,传入被中断请求所用的模型、工具、扩展和设置。你也可以传入另一个模型,例如某个提供方一直失败时;恢复并不会改变那个模型是否接受另一个提供方写下的历史。
流式、回调与同步智能体#
stream_resume() 产生的事件和 stream() 相同,AgentClient 和 SyncAgent 也有 resume、stream_resume 和 resume_point。随后的事件就是普通请求的事件:恢复后的请求以 RUN_COMPLETED 结束,被恢复的那一步如果出现非正常停止,会像其他情况一样通过 MODEL_STOPPED 报告,所以再次被截断的响应可以再次恢复。
不同之处:
没有新的用户消息,所以不会调用
on_message钩子,context.input_message为None。before_run和after_run钩子与普通请求一样运行。JSONL 扩展会把被恢复的这一轮从最后一条用户消息开始记录。为被中断请求启用的目标模式,不会被重新启用。
没有可恢复内容的会话,会作为请求失败而抛出,错误钩子会像其他失败的请求一样运行。想避免这一点,请先调用
resume_point()。
它不做什么#
它不会找回请求停止时正在流出的文本。 不完整的输出只有在模型完成这一步之后才会保存,所以用户在屏幕上看到而模型没有完成的内容会丢失。响应被截断之后,模型会被告知把它从头再做一遍。
它不会替你重试。 运行时会在没有任何输出之前重试失败的请求;
resume()用于重试结束之后,并且由宿主来调用它。它不会阻止两个进程同时恢复同一个会话。 拒绝同一会话第二个请求的检查只在单个进程内有效。
它不知道请求为什么停止。 历史只说明了它当时在做什么,不说明原因是超时、停止按钮还是崩溃,所以想展示原因的宿主需要自己保存。
下一步:参阅 了解模型为什么停下 了解响应的结束方式如何被报告,参阅 保存并恢复对话 了解历史如何保存和恢复。
相关 API:ResumePoint、ResumeKind、AgentResumeError。