两种等待发生在不同位置

async with 管理资源的异步进入和退出;async for 消费一个异步可迭代对象,每次取下一项都可能需要等待。它们并不表示循环体内部自动并行。

实验沿用 boot_agent_sandbox 名称,但把真实沙箱替换成字符串;不连接外部服务。chunks 只模拟两段数据,不模拟网络协议。

import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def boot_agent_sandbox():
    print("enter")
    try:
        yield "sandbox"
    finally:
        print("exit")

async def chunks():
    for value in ("A", "B"):
        await asyncio.sleep(0)
        yield value

async def exercise():
    async with boot_agent_sandbox() as sandbox:
        print(sandbox)
        async for chunk in chunks():
            print(chunk)

if __name__ == "__main__":
    asyncio.run(exercise())

下载并运行本节 example.py。只使用 Python 标准库;推荐 Python 3.12,最低 Python 3.11。

运行结果:

enter
sandbox
A
B
exit

先看上下文管理器

带 @asynccontextmanager 的异步生成器,在 yield 之前准备资源,把 yield 的值交给 as 后的变量;退出作用域后继续执行后面的清理。finally 使异常退出也有机会清理。

Slime 的 boot_agent_sandbox() 在准备成功后 yield 沙箱,退出时调用 __aexit__()。不要把进入、使用和回收分散到互不关联的调用里理解。

再看异步迭代

async def 中出现 yield,这个函数就产生异步生成器。调用它得到的对象用于异步迭代,不能像普通协程那样直接 await chunks()。

Slime 的流式 rollout 在 async for raw_line in response.aiter_lines() 中逐段处理服务返回的数据;Python 语法本身不绑定 HTTPX 或某种模型服务。

展开 Slime 源码:boot_agent_sandbox

boot_agent_sandbox
examples/coding_agent_rl/generate.py · L123–L134
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

                CONFIG.boot_retries,
                type(e).__name__,
                str(e)[:200],
            )
            await asyncio.sleep(1 + attempt + random.random())
    if sb is None:
        assert last_err is not None
        raise last_err
    try:
        yield sb
    finally:
        await sb.__aexit__(None, None, None)
展开 Slime 源码:流式响应读取

流式响应读取
slime/rollout/sglang_streaming_rollout.py · L123–L138
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

            sample, "sglang_generate_stream", attrs={"max_new_tokens": sampling_params["max_new_tokens"]}
        ) as span:
            async with client.stream("POST", url, json=payload, headers=headers) as response:
                response.raise_for_status()
                async for raw_line in response.aiter_lines():
                    if not raw_line or not raw_line.startswith("data:"):
                        continue
                    data_str = raw_line[len("data:") :].strip()
                    if not data_str or data_str == "[DONE]":
                        continue
                    try:
                        chunk = json.loads(data_str)
                    except json.JSONDecodeError:
                        logger.warning("sglang_streaming: skipping non-JSON chunk: %r", data_str[:120])
                        continue
复述检查

await 等一个结果;async for 逐个等结果;async with 管理资源进入和退出时的等待。async for 的循环体仍然按顺序推进,除非里面另行安排并发任务。

语义核对:asynccontextmanager、异步迭代。

本节对应的全部源码位置

下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。

展开本节 63 条源码记录
记录 / 讲解源码位置原代码
occ-000003examples/coding_agent_rl/generate.py:28
<module>
from contextlib import asynccontextmanager
occ-000005examples/coding_agent_rl/generate.py:95
<module>
@asynccontextmanager
occ-000006examples/coding_agent_rl/generate.py:96
<module>
async def boot_agent_sandbox(image: str, instance_id: str) -> AsyncIterator[E2BSandbox]:
occ-000008examples/coding_agent_rl/generate.py:108
boot_agent_sandbox
async with _BOOT_SEM:
occ-000016examples/coding_agent_rl/generate.py:132
boot_agent_sandbox
yield sb
occ-000020examples/coding_agent_rl/generate.py:202
generate
async with asyncio.timeout(CONFIG.rollout_guard_sec):
occ-000022examples/coding_agent_rl/generate.py:203
generate
async with boot_agent_sandbox(md["image"], instance_id) as sb:
occ-000102examples/coding_agent_rl/swe.py:269
_grade_scaleswe
async with E2BSandbox(image) as ev:
occ-000148examples/coding_agent_rl/swe.py:465
_grade_swebench
async with E2BSandbox(image) as ev:
occ-000197examples/retool/generate_with_retool.py:192
execute_predictions
async with SEMAPHORE:
occ-000217examples/retool/tool_sandbox.py:342
ToolRegistry.execute_tool
async with SEMAPHORE:
occ-000229examples/search-r1/generate_with_search.py:129
execute_predictions
async with SEMAPHORE:
occ-000240examples/search-r1/google_search_server.py:52
fetch
async with semaphore:
occ-000241examples/search-r1/google_search_server.py:54
fetch
async with session.get(url, headers=headers) as response:
occ-000248examples/search-r1/google_search_server.py:68
fetch_all
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
occ-000253examples/search-r1/google_search_server.py:99
google_search
async with aiohttp.ClientSession(**session_kwargs) as session:
occ-000254examples/search-r1/google_search_server.py:100
google_search
async with session.post(
occ-000265examples/search-r1/local_search_server.py:66
local_search
async with aiohttp.ClientSession(**session_kwargs) as session:
occ-000266examples/search-r1/local_search_server.py:67
local_search
async with session.post(search_url, json=payload, timeout=timeout_obj) as resp:
occ-000402slime/agent/adapters/common.py:475
call_sglang_generate
async with aiohttp.ClientSession(timeout=timeout) as sess, sess.post(
occ-000413slime/agent/adapters/common.py:521
_abort_sglang_request
async with aiohttp.ClientSession(timeout=timeout) as session:
occ-000415slime/agent/adapters/common.py:525
_abort_sglang_request.abort_worker
async with session.post(f"{url}/abort_request", json={"rid": rid}):
occ-000416slime/agent/adapters/common.py:530
_abort_sglang_request
async with session.get(f"{sglang_url}/workers") as response:
occ-000612slime/backends/megatron_utils/server/megatron_server.py:452
_build_http_app.update_from_disk
async with update_lock:
occ-000624slime/backends/megatron_utils/server/megatron_server.py:504
_build_http_app.update_from_disk
async with update_lock:
occ-000671slime/backends/sglang_utils/server_control.py:38
get_live_router_workers
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
occ-000838slime/data/transport.py:376
run_rollout_io
async with _async_limits[key]:
occ-001073slime/rollout/on_policy_distillation.py:26
reward_func
async with aiohttp.ClientSession(**session_kwargs) as session:
occ-001074slime/rollout/on_policy_distillation.py:27
reward_func
async with session.post(args.rm_url, json=payload) as resp:
occ-001078slime/rollout/rm_hub/__init__.py:43
remote_rm
async with session.post(args.rm_url, json=payload) as resp:
occ-001132slime/rollout/sglang_rollout.py:329
generate_and_rm
async with state.semaphore:
occ-001182slime/rollout/sglang_streaming_rollout.py:125
generate_streaming
async with client.stream("POST", url, json=payload, headers=headers) as response:
occ-001183slime/rollout/sglang_streaming_rollout.py:127
generate_streaming
async for raw_line in response.aiter_lines():
occ-001353slime_plugins/rollout_buffer/rollout_buffer_example.py:140
get_rollout_data
async with aiohttp.ClientSession() as session:
occ-001354slime_plugins/rollout_buffer/rollout_buffer_example.py:142
get_rollout_data
async with session.post(
occ-001483tests/test_agent/test_adapters.py:170
test_anthropic_messages_nonstream_records_token_segments.run_case
async with FakeSGLangServer([[(-0.1, 101), (-0.2, 102)]]) as sglang:
occ-001492tests/test_agent/test_adapters.py:206
test_openai_chat_completions_nonstream_records_token_segments.run_case
async with FakeSGLangServer([[(-0.3, 201)]]) as sglang:
occ-001501tests/test_agent/test_adapters.py:240
test_anthropic_messages_streams_blocks.run_case
async with FakeSGLangServer([[(-0.1, 301)]]) as sglang:
occ-001511tests/test_agent/test_adapters.py:274
test_openai_chat_completions_streams_chunks_until_done.run_case
async with FakeSGLangServer([[(-0.1, 401)]]) as sglang:
occ-001520tests/test_agent/test_adapters.py:315
test_anthropic_multiturn_wire_roundtrip_and_token_capture.run_case
async with FakeSGLangServer([[(-0.5, 700), (-0.5, 701)], [(-0.4, 800)]]) as sglang:
occ-001531tests/test_agent/test_adapters.py:385
test_max_turns_per_sid_returns_429.run_case
async with FakeSGLangServer([[(-0.1, 501)], [(-0.1, 502)]]) as sglang:
occ-001541tests/test_agent/test_agent_rollout_cpu.py:27
<module>
import contextlib
occ-001543tests/test_agent/test_agent_rollout_cpu.py:58
<module>
@contextlib.asynccontextmanager
occ-001544tests/test_agent/test_agent_rollout_cpu.py:59
<module>
async def _timeout_shim(_delay):
occ-001545tests/test_agent/test_agent_rollout_cpu.py:60
_timeout_shim
yield
occ-001551tests/test_agent/test_agent_rollout_cpu.py:114
_anthropic_agent
async with aiohttp.ClientSession(trust_env=False) as sess:
occ-001552tests/test_agent/test_agent_rollout_cpu.py:116
_anthropic_agent
async with sess.post(
occ-001569tests/test_agent/test_agent_rollout_cpu.py:242
_codex_agent
async with aiohttp.ClientSession(trust_env=False) as sess:
occ-001570tests/test_agent/test_agent_rollout_cpu.py:244
_codex_agent
async with sess.post(
occ-002042tests/test_server_control.py:264
test_http_transport_preserves_streams_timeouts_and_connection_budget.check
async with httpx.AsyncClient(transport=http_utils._ShardedHTTPTransport(concurrency)) as client:
occ-002044tests/test_server_control.py:268
test_http_transport_preserves_streams_timeouts_and_connection_budget.check.request
async with client.stream("POST", url, content=body) as response:
occ-002118tests/test_streaming_rollout.py:6
<module>
from contextlib import asynccontextmanager, nullcontext
occ-002122tests/test_streaming_rollout.py:170
_http_stream
async def chunk_lines():
occ-002123tests/test_streaming_rollout.py:172
_http_stream.chunk_lines
yield "data: " + json.dumps(chunk)
occ-002124tests/test_streaming_rollout.py:176
_http_stream
@asynccontextmanager
occ-002125tests/test_streaming_rollout.py:177
_http_stream
async def stream(method, url, json, headers):
occ-002126tests/test_streaming_rollout.py:182
_http_stream.stream
yield response
occ-002131tests/test_streaming_rollout.py:332
test_server_abort_keeps_last_observed_prefix
async def lines():
occ-002132tests/test_streaming_rollout.py:334
test_server_abort_keeps_last_observed_prefix.lines
yield "data: " + json.dumps(_chunk("a", [[-0.1, 11, None]], 1))
occ-002138tests/test_streaming_rollout.py:358
test_stream_cancellation_closes_request_and_keeps_prefix
async def lines():
occ-002139tests/test_streaming_rollout.py:359
test_stream_cancellation_closes_request_and_keeps_prefix.lines
yield "data: " + json.dumps(
occ-002164tests/test_streaming_rollout.py:419
test_unrelated_stream_cancellation_propagates
async def lines():
occ-002169tests/test_streaming_rollout.py:422
test_unrelated_stream_cancellation_propagates.lines
yield "unreachable"

专题首页 · 上一节 · 下一节:11 · ContextVar:让并发任务保留各自的上下文 · 源码与教程对照 · 全部用法