测试报告 · 2026-04-21

DeepAgents × Jina DeepSearch
SSE 流式集成全流程报告

记录从 API 连通验证、流式机制探索、subagent 集成到最终可用方案的完整测试过程,含关键发现与结论。

deepagents 0.5.3
jina-deepsearch-v1 SSE
qwen3.5-35b-a3b / OpenRouter
stream_mode=messages
目录
1
API 连通性验证

三个 Jina API 的基本连通性验证结果:

API端点测试结果耗时关键发现
Search s.jina.ai ✓ 通过 1.9s 返回 JSON,内容含 Markdown 图片标签噪音
Reader r.jina.ai ✓ 通过 0.3s 提取 47,384 字符,速度极快
DeepSearch deepsearch.jina.ai ✓ 通过 62–179s SSE 流,2502–5742 chunks,citations 在最后 chunk
STEP 1 — jina_search ✓ 2 results in 1.9s [1] DeepSearch — https://jina.ai/deepsearch/ [2] Jina AI - Your Search Foundation — https://jina.ai/ STEP 2 — jina_reader ✓ Read 'DeepSearch' in 0.3s Content: 47,384 chars STEP 3 — jina_deepsearch SSE (reasoning_effort=low) HTTP 200 Content-Type: text/event-stream Chunks: 2502 Elapsed: 62.4s Think chars: 6325 Answer chars: 4220 Citations: 0 STEP 3b — DeepSearch (reasoning_effort=medium) Chunks: 5742 Elapsed: 179.3s Citations: 2 Annotations first appeared at chunk #5742 ← 最后一个 chunk
2
SSE 流式关键发现
响应结构

每个 SSE chunk 的结构:

{
  "choices": [{
    "delta": {
      "content": "...",    // 内容片段
      "type": "text",
      "annotations": [...]  // 仅最后 chunk
    }
  }]
}
三个关键坑
in_think 逐字符追踪 bug
原版 in_think 用 full_content.endswith("<think>") 逐字符判断,Answer chars 始终为 0。改为 regex 后修复。
Citations 在最后一个 chunk
low effort 不返回引用;medium 的 annotations 出现在第 5742(最后)个 chunk,需同时检查 deltachunk 两层。
<think> 推理块需剥离
响应头部包含 <think>…</think> 推理过程(6325 chars),用 re.sub 去除后才是最终答案(4220 chars)。
正确的 SSE 解析实现
thinking_done = False
think_buf = ""

for raw_line in resp.iter_lines():
    if not raw_line.startswith("data: "): continue
    data_str = raw_line[6:].strip()
    if data_str == "[DONE]": break

    chunk = json.loads(data_str)
    delta = chunk["choices"][0]["delta"]
    piece = delta.get("content") or ""

    if piece:
        full_content += piece
        if not thinking_done:
            think_buf += piece
            if "</think>" in think_buf:
                thinking_done = True
                after = think_buf.split("</think>", 1)[1]
                if after: stream_or_print(after)
        else:
            stream_or_print(piece)  # ← 实时输出

    # citations:检查 delta + chunk 两层
    for ann in (delta.get("annotations") or chunk.get("annotations") or []):
        if ann.get("type") == "url_citation":
            citations.append(ann.get("url_citation") or ann)
3
方案 A:同步 tool + subagent(验证可行性)

使用同步 @tool jina_deepsearch + jina-writer subagent,stream_mode="messages"

用户 → 主 Agent(model) → task("jina-writer") → [tools 节点] jina_deepsearch() 阻塞 168s │ return 完整字符串 主 Agent(model) 流式复述文章 → 用户
问题
  • 用户等待 168s 无任何输出
  • [tools] 节点阻塞,内容一次性推送
  • 主 agent 再次流式复述,内容重复
  • stream_mode="updates" 返回 Overwrite 对象,不可迭代(TypeError)
验证结论
  • stream_mode="messages" 正确,返回 (AIMessageChunk, metadata) 元组
  • metadata["langgraph_node"] 可识别节点(model / tools)
  • 文章内容正确,仅无法实时流式
  • 总耗时:168s
方案 A 最终代码(可用,非实时)
for chunk, metadata in agent.stream(
    {"messages": [{"role": "user", "content": query}]},
    stream_mode="messages",   # ← 正确模式,返回 (AIMessageChunk, metadata)
):
    node    = metadata.get("langgraph_node", "")
    content = getattr(chunk, "content", "") or ""
    if content:
        print(content, end="", flush=True)
4
方案 B:async tool + get_stream_writer

jina_deepsearch 改写为 async tool,在 SSE 接收循环内调用 get_stream_writer() 推送 token。

为什么必须用 async?

必要原因:httpx.AsyncClient 强制要求

逐 token 接收 SSE 而不阻塞事件循环,必须用异步 HTTP 客户端:

async with httpx.AsyncClient() as client:
    async with client.stream(...) as resp:
        async for line in resp.aiter_lines():
            ...  # ← 必须 await,必须 async 上下文

若改用同步 requests.iter_lines(),会阻塞整个事件循环,deepagents 框架的其他 coroutine 全部卡死。

附带原因:配合 agent.astream()

调用端使用 asyncio.run() + agent.astream()。LangGraph 执行异步图时会调用 tool.ainvoke()

  • async tool:直接在事件循环内并发执行
  • sync tool:丢进线程池(run_in_executor),多一层开销,且 get_stream_writer() 上下文可能隔离
一句话总结
async 是被 httpx.AsyncClient 强制要求的——想逐 token 接收 SSE 而不阻塞事件循环,就必须用异步 HTTP 客户端,进而整个 tool 必须声明为 async def
核心实现
from langgraph.config import get_stream_writer

@tool
async def jina_deepsearch_stream(query: str, reasoning_effort: str = "low") -> str:
    write = get_stream_writer()

    async with httpx.AsyncClient(timeout=300) as client:
        async with client.stream("POST", _DEEPSEARCH_URL, ...) as resp:
            async for raw_line in resp.aiter_lines():
                ...
                if thinking_done:
                    write(piece)   # ← 每个 token 推送到 custom stream

# 调用端
async for mode, data in agent.astream(
    {"messages": [...]},
    stream_mode=["custom", "messages"],
):
    if mode == "custom":
        print(data, end="", flush=True)  # ← 理想:实时 token

测试结果(带 subagent)

custom chunks : 0 ← subagent 中间层阻断了 custom stream 传播 message chunks: 314 Total time : 277.9s Streaming mode: messages (fallback)

测试结果(去掉 subagent,tool 直挂主 agent)

custom chunks : 2 ← custom 事件到达,但批量投递(非逐 token) message chunks: 314 Total time : 207.1s Streaming mode: custom (real-time) — 批量
5
get_stream_writer 穿透性测试

用最简 toy tool 验证 get_stream_writer() 的实际行为:

@tool
async def streaming_counter(n: int = 5) -> str:
    write = get_stream_writer()
    for i in range(n):
        write(f"token_{i} ")
        await asyncio.sleep(0.1)
    return "done"

# 结果
custom events: 0   ← deepagents 工具执行层不透传 custom stream
结论:get_stream_writer() 在 deepagents 中不可用于逐 token 流式
deepagents 的 create_deep_agent 内部使用 LangGraph prebuilt 图,tool 节点在独立执行上下文中运行,get_stream_writer() 写入的 custom channel 不会传播到外层 astream。这是 deepagents 框架层的限制,与 tool 是否 async 无关。
测试场景custom chunks逐 token?原因
sync tool + subagent0同步阻塞 + subagent 隔离
async tool + subagent0subagent 中间层阻断 custom stream
async tool 直挂主 agent2(批量)tool 执行完才批量投递
toy counter tool(无 DeepSearch)0deepagents 框架层不透传
6
最终方案:stdout 直��实现实时输出

绕过 LangGraph custom stream,在 async tool 的 SSE 接收循环内直接 sys.stdout.write(), 实现终端实时可见的逐 token 输出。

用户发出请求 主 Agent 决策 → 调用 jina_deepsearch_stream tool ↓ async SSE 循环开始 Jina DeepSearch API ─── SSE chunk ──▶ async for aiter_lines() ↓ 每个 token(</think> 之后) sys.stdout.write(piece) ← 实时打印到终端 write(piece) ← 同时推 custom stream(批量) ↓ SSE [DONE],return 完整文章 主 Agent 收到 tool 结果 → 流式输出最终回复(messages stream)
✅ 实际效果
  • 终端看到 DeepSearch 答案实时逐字出现(真正实时)
  • 主 agent 再次流式输出完整文章(messages stream)
  • total time: 207s,比方案 A 快 20%
  • 文章质量高,含参考文献
⚠️ 局限
  • stdout 是副作用,Web 服务场景不适用
  • 内容被输出两次(DeepSearch + 主 agent ��述)
  • Web 服务需直接暴露 DeepSearch SSE 端点
7
完整可用代码

jina_tools.py — async streaming tool

import asyncio, json, os, re, sys
from typing import Annotated
import httpx
from langchain_core.tools import tool
from langgraph.config import get_stream_writer

@tool
async def jina_deepsearch_stream(
    query: Annotated[str, "Research question"],
    reasoning_effort: Annotated[str, "low/medium/high"] = "low",
) -> str:
    write = get_stream_writer()
    thinking_done = False
    think_buf = ""
    full_content = ""
    citations = []

    async with httpx.AsyncClient(timeout=300) as client:
        async with client.stream("POST", _DEEPSEARCH_URL,
            headers={
                "Authorization": f"Bearer {JINA_API_KEY}",
                "Accept": "text/event-stream",
                "Content-Type": "application/json",
            },
            json={
                "model": "jina-deepsearch-v1",
                "messages": [{"role": "user", "content": query}],
                "stream": True,
                "reasoning_effort": reasoning_effort,
                "no_direct_answer": True,
            }) as resp:
            resp.raise_for_status()
            async for raw_line in resp.aiter_lines():
                if not raw_line.startswith("data: "): continue
                data_str = raw_line[6:].strip()
                if data_str == "[DONE]": break
                chunk = json.loads(data_str)
                delta = (chunk.get("choices") or [{}])[0].get("delta", {})
                piece = delta.get("content") or ""
                if piece:
                    full_content += piece
                    if not thinking_done:
                        think_buf += piece
                        if "</think>" in think_buf:
                            thinking_done = True
                            after = think_buf.split("</think>", 1)[1]
                            if after:
                                sys.stdout.write(after); sys.stdout.flush()
                                write(after)
                    else:
                        sys.stdout.write(piece); sys.stdout.flush()
                        write(piece)
                # collect citations
                for ann in (delta.get("annotations") or chunk.get("annotations") or []):
                    if isinstance(ann, dict) and ann.get("type") == "url_citation":
                        uc = ann.get("url_citation") or ann
                        if uc.get("url"): citations.append(uc)

    answer = re.sub(r"<think>.*?</think>", "", full_content, flags=re.DOTALL).strip()
    for i, c in enumerate(_dedupe_citations(citations), 1):
        answer += f"\n[{i}] {c.get('title','')} — {c.get('url','')}"
    return answer

test_agent_stream_b2.py — 调用端

import asyncio, os, sys, time
from langchain_openai import ChatOpenAI
from deepagents import create_deep_agent
from jina_tools import jina_deepsearch_stream, jina_search, jina_reader

model = ChatOpenAI(
    model="qwen3.5-35b-a3b",
    base_url="https://openrouter.yangzhiqiang.tech/api/v1",
    api_key="sk-123",
    temperature=0, streaming=True,
)

agent = create_deep_agent(
    model=model,
    tools=[jina_deepsearch_stream, jina_search, jina_reader],  # 直挂主 agent
    system_prompt="Use jina_deepsearch_stream to research then write the article.",
    subagents=[],  # 无 subagent,避免 custom stream 被阻断
)

async def main():
    async for mode, data in agent.astream(
        {"messages": [{"role": "user", "content": QUERY}]},
        stream_mode=["custom", "messages"],
    ):
        if mode == "custom":
            print(data, end="", flush=True)  # 批量到达,非逐 token
        elif mode == "messages":
            chunk, meta = data
            if meta.get("langgraph_node") == "model":
                print(getattr(chunk, "content", ""), end="", flush=True)

asyncio.run(main())
8
结论与使用建议
方案实现复杂度实时性适用场景
方案 A
sync tool + subagent
简单 无(等待后批量) API 服务、内容质量优先、等待时间可接受
方案 B1
async tool + subagent
中等 无(subagent 阻断) 不推荐,subagent 层阻断 custom stream
方案 B2
async tool + 直挂主 agent
中等 stdout 实时 / custom 批量 终端 CLI 工具、开发调试环境
方案 C(推荐 Web)
直接暴露 Jina SSE 端点
中等 真正逐 token Web 服务、前端直连、需要最低延迟
核心结论
deepagents 框架内无法实现 DeepSearch 的真正逐 token 流式输出。 原因是 create_deep_agent 的 tool 节点在独立执行上下文中运行, get_stream_writer() 写入的 custom channel 不会穿透到外层 astream。 对 CLI 场景,sys.stdout.write() 是当前最实用的实时输出方案。 对 Web 服务场景,应将 Jina DeepSearch SSE 直接透传给前端。
方案 A 推荐配置(生产可用)
stream_mode="messages" + 主 agent 对文章 token 流式输出, 等待期间展示进度提示(spinner)。文章质量与完整引用保证由 DeepSearch 负责, LLM 最终输出有明显的流式体验(主 agent 合成阶段)。

文件清单

文件说明
jina_tools.py三个同步工具 + jina_deepsearch_stream async 工具 + 两个 subagent 定义
test_jina_stream.pyAPI 连通性基础测试
test_jina_fixed.py修复版测试(think 追踪 + citation 诊断 + @tool 兼容)
test_agent_simple.py方案 A:sync tool + subagent + stream_mode=messages
test_agent_stream_b.py方案 B1:async tool + subagent(custom stream 被阻断)
test_agent_stream_b2.py方案 B2:async tool 直挂主 agent + stdout 实时输出
测试环境:deepagents 0.5.3 · LangGraph 1.1.8 · Python 3.12 · 报告日期:2026-04-21