Explorar el Código

add chapter 14

jjyaoao hace 8 meses
padre
commit
0e3e27abbd
Se han modificado 41 ficheros con 2391 adiciones y 1865 borrados
  1. 3 8
      code/chapter14/helloagents-deepresearch/backend/pyproject.toml
  2. 18 0
      code/chapter14/helloagents-deepresearch/backend/src/__init__.py
  3. 9 10
      code/chapter14/helloagents-deepresearch/backend/src/agent.py
  4. 2 1
      code/chapter14/helloagents-deepresearch/backend/src/config.py
  5. 0 16
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/__init__.py
  6. 0 8
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/agent.py
  7. 0 11
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/api.py
  8. 0 2
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/__init__.py
  9. 0 10
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/agents/__init__.py
  10. 0 353
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/agents/tool_aware_agent.py
  11. 0 8
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/api/__init__.py
  12. 0 2
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/api/routes/__init__.py
  13. 0 11
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/models/__init__.py
  14. 0 10
      code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/state.py
  15. 14 2
      code/chapter14/helloagents-deepresearch/backend/src/main.py
  16. 1 0
      code/chapter14/helloagents-deepresearch/backend/src/models.py
  17. 0 0
      code/chapter14/helloagents-deepresearch/backend/src/prompts.py
  18. 0 0
      code/chapter14/helloagents-deepresearch/backend/src/services/__init__.py
  19. 1 1
      code/chapter14/helloagents-deepresearch/backend/src/services/notes.py
  20. 6 5
      code/chapter14/helloagents-deepresearch/backend/src/services/planner.py
  21. 6 5
      code/chapter14/helloagents-deepresearch/backend/src/services/reporter.py
  22. 2 2
      code/chapter14/helloagents-deepresearch/backend/src/services/search.py
  23. 7 6
      code/chapter14/helloagents-deepresearch/backend/src/services/summarizer.py
  24. 0 0
      code/chapter14/helloagents-deepresearch/backend/src/services/text_processing.py
  25. 1 1
      code/chapter14/helloagents-deepresearch/backend/src/services/tool_events.py
  26. 0 0
      code/chapter14/helloagents-deepresearch/backend/src/utils.py
  27. 4 909
      code/chapter14/helloagents-deepresearch/backend/uv.lock
  28. 288 7
      code/chapter14/helloagents-deepresearch/frontend/src/App.vue
  29. 2029 477
      docs/chapter14/第十四章 自动化深度研究智能体.md
  30. BIN
      docs/images/14-figures/14-1.png
  31. BIN
      docs/images/14-figures/14-10.png
  32. BIN
      docs/images/14-figures/14-2.png
  33. BIN
      docs/images/14-figures/14-3.png
  34. BIN
      docs/images/14-figures/14-4.png
  35. BIN
      docs/images/14-figures/14-5.png
  36. BIN
      docs/images/14-figures/14-6.png
  37. BIN
      docs/images/14-figures/14-7.png
  38. BIN
      docs/images/14-figures/14-8.png
  39. BIN
      docs/images/14-figures/14-9.png
  40. BIN
      docs/images/14-figures/14-table-1.png
  41. BIN
      docs/images/14-figures/14-table-2.png

+ 3 - 8
code/chapter14/helloagents-deepresearch/backend/pyproject.toml

@@ -10,17 +10,12 @@ license = { text = "MIT" }
 requires-python = ">=3.10"
 dependencies = [
     "fastapi>=0.115.0",
-    "hello-agents>=0.2.4",
+    "hello-agents>=0.2.8",
     "tavily-python>=0.5.0",
-    "httpx>=0.28.1",
-    "markdownify>=0.11.0",
     "python-dotenv==1.0.1",
     "requests>=2.31.0",
-    "langsmith>=0.1.38",
     "openai>=1.12.0",
     "uvicorn[standard]>=0.32.0",
-    "huggingface-hub>=0.35.3",
-    "langchain-community>=0.4",
     "ddgs>=9.6.1",
     "loguru>=0.7.3",
 ]
@@ -33,10 +28,10 @@ requires = ["setuptools>=73.0.0", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [tool.setuptools]
-packages = ["deep_researcher"]
+packages = ["src"]
 
 [tool.setuptools.package-dir]
-"deep_researcher" = "src/deep_researcher"
+"" = "src"
 
 [tool.setuptools.package-data]
 "*" = ["py.typed"]

+ 18 - 0
code/chapter14/helloagents-deepresearch/backend/src/__init__.py

@@ -0,0 +1,18 @@
+"""HelloAgents Deep Research - A deep research assistant powered by HelloAgents."""
+
+__version__ = "0.0.1"
+
+from .agent import DeepResearchAgent
+from .config import Configuration, SearchAPI
+from .models import SummaryState, SummaryStateInput, SummaryStateOutput, TodoItem
+
+__all__ = [
+    "DeepResearchAgent",
+    "Configuration",
+    "SearchAPI",
+    "SummaryState",
+    "SummaryStateInput",
+    "SummaryStateOutput",
+    "TodoItem",
+]
+

+ 9 - 10
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/agents/deep_research_agent.py → code/chapter14/helloagents-deepresearch/backend/src/agent.py

@@ -9,23 +9,22 @@ from queue import Empty, Queue
 from threading import Lock, Thread
 from typing import Any, Callable, Iterator
 
-from hello_agents import HelloAgentsLLM
+from hello_agents import HelloAgentsLLM, ToolAwareSimpleAgent
 from hello_agents.tools import ToolRegistry
 from hello_agents.tools.builtin.note_tool import NoteTool
 
-from ...configuration import Configuration
-from ...prompts import (
+from config import Configuration
+from prompts import (
     report_writer_instructions,
     task_summarizer_instructions,
     todo_planner_system_prompt,
 )
-from ..models import SummaryState, SummaryStateOutput, TodoItem
-from ..services.planner_service import PlanningService
-from ..services.reporting_service import ReportingService
-from ..services.search_service import dispatch_search, prepare_research_context
-from ..services.summarization_service import SummarizationService
-from ..services.tool_events import ToolCallTracker
-from .tool_aware_agent import ToolAwareSimpleAgent
+from models import SummaryState, SummaryStateOutput, TodoItem
+from services.planner import PlanningService
+from services.reporter import ReportingService
+from services.search import dispatch_search, prepare_research_context
+from services.summarizer import SummarizationService
+from services.tool_events import ToolCallTracker
 
 logger = logging.getLogger(__name__)
 

+ 2 - 1
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/configuration.py → code/chapter14/helloagents-deepresearch/backend/src/config.py

@@ -42,7 +42,7 @@ class Configuration(BaseModel):
         description="Whether to store task progress in NoteTool",
     )
     notes_workspace: str = Field(
-        default="./notes/deep_research",
+        default="./notes",
         title="Notes Workspace",
         description="Directory for NoteTool to persist task notes",
     )
@@ -140,3 +140,4 @@ class Configuration(BaseModel):
         """Best-effort resolution of the model identifier to use."""
 
         return self.llm_model_id or self.local_llm
+

+ 0 - 16
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/__init__.py

@@ -1,16 +0,0 @@
-"""HelloAgents deep researcher package."""
-
-version = "0.0.1"
-
-from .agent import DeepResearchAgent, run_deep_research
-from .configuration import Configuration, SearchAPI
-from .state import SummaryState, SummaryStateOutput
-
-__all__ = [
-    "DeepResearchAgent",
-    "run_deep_research",
-    "Configuration",
-    "SearchAPI",
-    "SummaryState",
-    "SummaryStateOutput",
-]

+ 0 - 8
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/agent.py

@@ -1,8 +0,0 @@
-"""Backward-compatible entry points for the deep researcher agent."""
-
-from __future__ import annotations
-
-from .app.agents.deep_research_agent import DeepResearchAgent, run_deep_research
-
-__all__ = ["DeepResearchAgent", "run_deep_research"]
-

+ 0 - 11
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/api.py

@@ -1,11 +0,0 @@
-"""Backward-compatible API exports."""
-
-from __future__ import annotations
-
-from .app.api.main import app, create_app
-
-__all__ = [
-    "app",
-    "create_app",
-]
-

+ 0 - 2
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/__init__.py

@@ -1,2 +0,0 @@
-"""Application layer for the deep researcher."""
-

+ 0 - 10
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/agents/__init__.py

@@ -1,10 +0,0 @@
-"""Agent implementations for the deep researcher."""
-
-from .deep_research_agent import DeepResearchAgent, run_deep_research
-from .tool_aware_agent import ToolAwareSimpleAgent
-
-__all__ = [
-    "DeepResearchAgent",
-    "run_deep_research",
-    "ToolAwareSimpleAgent",
-]

+ 0 - 353
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/agents/tool_aware_agent.py

@@ -1,353 +0,0 @@
-"""Wrapper around HelloAgents SimpleAgent that records tool calls."""
-
-from __future__ import annotations
-
-import ast
-import json
-import logging
-from collections.abc import Iterator
-from typing import Any, Callable, Optional
-
-from hello_agents import SimpleAgent
-from hello_agents.core.message import Message
-from hello_agents.tools import ToolRegistry
-
-logger = logging.getLogger(__name__)
-
-
-class ToolAwareSimpleAgent(SimpleAgent):
-    """SimpleAgent 子类,记录工具调用情况。"""
-
-    def __init__(
-        self,
-        *args: Any,
-        tool_call_listener: Optional[Callable[[dict[str, Any]], None]] = None,
-        **kwargs: Any,
-    ) -> None:
-        super().__init__(*args, **kwargs)
-        self._tool_call_listener = tool_call_listener
-
-    def _execute_tool_call(self, tool_name: str, parameters: str) -> str:  # type: ignore[override]
-        if not self.tool_registry:
-            return "❌ 错误:未配置工具注册表"
-
-        try:
-            tool = self.tool_registry.get_tool(tool_name)
-            if not tool:
-                return f"❌ 错误:未找到工具 '{tool_name}'"
-
-            parsed_parameters = self._parse_tool_parameters(tool_name, parameters)
-            parsed_parameters = self._sanitize_parameters(parsed_parameters)
-
-            result = tool.run(parsed_parameters)
-            formatted_result = f"🔧 工具 {tool_name} 执行结果:\n{result}"
-        except Exception as exc:  # pragma: no cover - tool failures回退
-            parsed_parameters = {}
-            formatted_result = f"❌ 工具调用失败:{exc}"
-
-        if self._tool_call_listener:
-            try:
-                self._tool_call_listener(
-                    {
-                        "agent_name": self.name,
-                        "tool_name": tool_name,
-                        "raw_parameters": parameters,
-                        "parsed_parameters": parsed_parameters,
-                        "result": formatted_result,
-                    }
-                )
-            except Exception:  # pragma: no cover - 防御性兜底
-                logger.exception("Tool call listener failed")
-
-        return formatted_result
-
-    def _parse_tool_calls(self, text: str) -> list:  # type: ignore[override]
-        marker = "[TOOL_CALL:"
-        calls: list = []
-        start = 0
-
-        while True:
-            begin = text.find(marker, start)
-            if begin == -1:
-                break
-
-            tool_start = begin + len(marker)
-            colon = text.find(":", tool_start)
-            if colon == -1:
-                break
-
-            tool_name = text[tool_start:colon].strip()
-            body_start = colon + 1
-            pos = body_start
-            depth = 0
-            in_string = False
-            string_quote = ""
-
-            while pos < len(text):
-                char = text[pos]
-
-                if char in {'"', "'"}:
-                    if not in_string:
-                        in_string = True
-                        string_quote = char
-                    elif string_quote == char and text[pos - 1] != "\\":
-                        in_string = False
-
-                if not in_string:
-                    if char == '[':
-                        depth += 1
-                    elif char == ']':
-                        if depth == 0:
-                            body = text[body_start:pos].strip()
-                            original = text[begin : pos + 1]
-                            calls.append(
-                                {
-                                    "tool_name": tool_name,
-                                    "parameters": body,
-                                    "original": original,
-                                }
-                            )
-                            start = pos + 1
-                            break
-                        else:
-                            depth -= 1
-
-                pos += 1
-            else:
-                break
-
-        return calls
-
-    @staticmethod
-    def _find_tool_call_end(text: str, start_index: int) -> int:
-        marker = "[TOOL_CALL:"
-        tool_start = start_index + len(marker)
-        colon = text.find(":", tool_start)
-        if colon == -1:
-            return -1
-
-        body_start = colon + 1
-        pos = body_start
-        depth = 0
-        in_string = False
-        string_quote = ""
-
-        while pos < len(text):
-            char = text[pos]
-
-            if char in {'"', "'"}:
-                if not in_string:
-                    in_string = True
-                    string_quote = char
-                elif string_quote == char and text[pos - 1] != "\\":
-                    in_string = False
-
-            if not in_string:
-                if char == '[':
-                    depth += 1
-                elif char == ']':
-                    if depth == 0:
-                        return pos
-                    depth -= 1
-
-            pos += 1
-
-        return -1
-
-    @staticmethod
-    def attach_registry(agent: "ToolAwareSimpleAgent", registry: ToolRegistry | None) -> None:
-        """Helper to attach a tool registry if provided."""
-
-        if registry:
-            agent.tool_registry = registry
-            agent.enable_tool_calling = True
-
-    @staticmethod
-    def _sanitize_parameters(parameters: dict[str, Any]) -> dict[str, Any]:
-        sanitized: dict[str, Any] = {}
-        for key, value in parameters.items():
-            if isinstance(value, (int, float, bool, list, dict)):
-                sanitized[key] = value
-                continue
-
-            if isinstance(value, str):
-                normalized = ToolAwareSimpleAgent._normalize_string(value)
-
-                if key == "task_id":
-                    try:
-                        sanitized[key] = int(normalized)
-                        continue
-                    except ValueError:
-                        pass
-
-                if key == "tags":
-                    parsed_tags = ToolAwareSimpleAgent._coerce_sequence(normalized)
-                    if isinstance(parsed_tags, list):
-                        sanitized[key] = parsed_tags
-                        continue
-                    if normalized:
-                        sanitized[key] = [item.strip() for item in normalized.split(",") if item.strip()]
-                        continue
-
-                if key in {"note_type", "action", "title", "content", "note_id"}:
-                    sanitized[key] = normalized
-                    continue
-
-                sanitized[key] = normalized
-                continue
-
-            sanitized[key] = value
-
-        return sanitized
-
-    @staticmethod
-    def _normalize_string(value: str) -> str:
-        trimmed = value.strip()
-
-        if trimmed and trimmed[0] in {'"', "'"} and trimmed.count(trimmed[0]) == 1:
-            trimmed = trimmed[1:]
-        if trimmed and trimmed[-1] in {'"', "'"} and trimmed.count(trimmed[-1]) == 1:
-            trimmed = trimmed[:-1]
-
-        if trimmed and trimmed[0] in {'"', "'"} and trimmed[-1] == trimmed[0]:
-            trimmed = trimmed[1:-1]
-
-        if trimmed and trimmed[0] in {'[', '('} and trimmed[-1] not in {']', ')'}:
-            closing = ']' if trimmed[0] == '[' else ')'
-            trimmed = f"{trimmed}{closing}"
-
-        return trimmed.strip()
-
-    @staticmethod
-    def _coerce_sequence(value: str) -> Any:
-        if not value:
-            return None
-
-        candidates = [value]
-        if value.startswith("[") and not value.endswith("]"):
-            candidates.append(f"{value}]")
-        if value.startswith("(") and not value.endswith(")"):
-            candidates.append(f"{value})")
-
-        for candidate in candidates:
-            for loader in (json.loads, ast.literal_eval):
-                try:
-                    parsed = loader(candidate)
-                except Exception:
-                    continue
-                if isinstance(parsed, list):
-                    return parsed
-
-        return None
-
-    def stream_run(self, input_text: str, max_tool_iterations: int = 3, **kwargs: Any) -> Iterator[str]:  # type: ignore[override]
-        """Stream assistant output while supporting tool calls mid-generation."""
-
-        messages: list[dict[str, Any]] = []
-        enhanced_system_prompt = self._get_enhanced_system_prompt()
-        messages.append({"role": "system", "content": enhanced_system_prompt})
-
-        for msg in self._history:
-            messages.append({"role": msg.role, "content": msg.content})
-
-        messages.append({"role": "user", "content": input_text})
-
-        final_segments: list[str] = []
-        final_response_text = ""
-        current_iteration = 0
-
-        marker = "[TOOL_CALL:"
-
-        while current_iteration < max_tool_iterations:
-            residual = ""
-            segments_this_round: list[str] = []
-            tool_call_texts: list[str] = []
-
-            def process_residual(final_pass: bool = False) -> Iterator[str]:
-                nonlocal residual
-                while True:
-                    start = residual.find(marker)
-                    if start == -1:
-                        safe_len = len(residual) if final_pass else max(0, len(residual) - (len(marker) - 1))
-                        if safe_len > 0:
-                            segment = residual[:safe_len]
-                            residual = residual[safe_len:]
-                            yield segment
-                        break
-
-                    if start > 0:
-                        segment = residual[:start]
-                        residual = residual[start:]
-                        if segment:
-                            yield segment
-                        continue
-
-                    end = self._find_tool_call_end(residual, 0)
-                    if end == -1:
-                        break
-
-                    tool_call_texts.append(residual[: end + 1])
-                    residual = residual[end + 1 :]
-
-            for chunk in self.llm.stream_invoke(messages, **kwargs):
-                if not chunk:
-                    continue
-
-                residual += chunk
-
-                for segment in process_residual():
-                    if not segment:
-                        continue
-                    segments_this_round.append(segment)
-                    final_segments.append(segment)
-                    yield segment
-
-            for segment in process_residual(final_pass=True):
-                if not segment:
-                    continue
-                segments_this_round.append(segment)
-                final_segments.append(segment)
-                yield segment
-
-            clean_response = "".join(segments_this_round)
-            tool_calls: list[dict[str, Any]] = []
-
-            for call_text in tool_call_texts:
-                tool_calls.extend(self._parse_tool_calls(call_text))
-
-            if tool_calls:
-                messages.append({"role": "assistant", "content": clean_response})
-
-                tool_results = []
-                for call in tool_calls:
-                    result = self._execute_tool_call(call["tool_name"], call["parameters"])
-                    tool_results.append(result)
-
-                tool_results_text = "\n\n".join(tool_results)
-                messages.append(
-                    {
-                        "role": "user",
-                        "content": (
-                            "工具执行结果:\n"
-                            f"{tool_results_text}\n\n"
-                            "请基于这些结果给出完整的回答。"
-                        ),
-                    }
-                )
-
-                current_iteration += 1
-                continue
-
-            final_response_text = clean_response
-            break
-
-        if current_iteration >= max_tool_iterations and not final_response_text:
-            fallback_response = self.llm.invoke(messages, **kwargs)
-            final_segments.append(fallback_response)
-            final_response_text = fallback_response
-            yield fallback_response
-
-        stored_response = final_response_text or "".join(final_segments)
-
-        self.add_message(Message(input_text, "user"))
-        self.add_message(Message(stored_response, "assistant"))

+ 0 - 8
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/api/__init__.py

@@ -1,8 +0,0 @@
-"""FastAPI integration for the deep researcher."""
-
-from .main import app, create_app
-
-__all__ = [
-    "app",
-    "create_app",
-]

+ 0 - 2
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/api/routes/__init__.py

@@ -1,2 +0,0 @@
-"""Route definitions for the deep researcher HTTP API."""
-

+ 0 - 11
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/models/__init__.py

@@ -1,11 +0,0 @@
-"""Shared data models used across the application."""
-
-from .state import SummaryState, SummaryStateInput, SummaryStateOutput, TodoItem
-
-__all__ = [
-    "SummaryState",
-    "SummaryStateInput",
-    "SummaryStateOutput",
-    "TodoItem",
-]
-

+ 0 - 10
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/state.py

@@ -1,10 +0,0 @@
-"""Backward-compatible exports of state models."""
-
-from .app.models.state import SummaryState, SummaryStateInput, SummaryStateOutput, TodoItem
-
-__all__ = [
-    "SummaryState",
-    "SummaryStateInput",
-    "SummaryStateOutput",
-    "TodoItem",
-]

+ 14 - 2
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/api/main.py → code/chapter14/helloagents-deepresearch/backend/src/main.py

@@ -12,8 +12,8 @@ from fastapi.responses import StreamingResponse
 from loguru import logger
 from pydantic import BaseModel, Field
 
-from ...configuration import Configuration, SearchAPI
-from ..agents.deep_research_agent import DeepResearchAgent
+from config import Configuration, SearchAPI
+from agent import DeepResearchAgent
 
 # 添加控制台日志处理程序
 logger.add(
@@ -176,3 +176,15 @@ def create_app() -> FastAPI:
 
 
 app = create_app()
+
+
+if __name__ == "__main__":
+    import uvicorn
+
+    uvicorn.run(
+        "main:app",
+        host="0.0.0.0",
+        port=8000,
+        reload=True,
+        log_level="info"
+    )

+ 1 - 0
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/models/state.py → code/chapter14/helloagents-deepresearch/backend/src/models.py

@@ -48,3 +48,4 @@ class SummaryStateOutput:
     running_summary: str = field(default=None)  # Backward-compatible文本
     report_markdown: Optional[str] = field(default=None)
     todo_items: List[TodoItem] = field(default_factory=list)
+

+ 0 - 0
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/prompts.py → code/chapter14/helloagents-deepresearch/backend/src/prompts.py


+ 0 - 0
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/__init__.py → code/chapter14/helloagents-deepresearch/backend/src/services/__init__.py


+ 1 - 1
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/notes_service.py → code/chapter14/helloagents-deepresearch/backend/src/services/notes.py

@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import json
 
-from ..models import TodoItem
+from models import TodoItem
 
 
 def build_note_guidance(task: TodoItem) -> str:

+ 6 - 5
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/planner_service.py → code/chapter14/helloagents-deepresearch/backend/src/services/planner.py

@@ -7,11 +7,12 @@ import logging
 import re
 from typing import Any, List, Optional
 
-from ..models import SummaryState, TodoItem
-from ...configuration import Configuration
-from ...prompts import get_current_date, todo_planner_instructions
-from ...utils import strip_thinking_tokens
-from ..agents.tool_aware_agent import ToolAwareSimpleAgent
+from hello_agents import ToolAwareSimpleAgent
+
+from models import SummaryState, TodoItem
+from config import Configuration
+from prompts import get_current_date, todo_planner_instructions
+from utils import strip_thinking_tokens
 
 logger = logging.getLogger(__name__)
 

+ 6 - 5
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/reporting_service.py → code/chapter14/helloagents-deepresearch/backend/src/services/reporter.py

@@ -4,11 +4,12 @@ from __future__ import annotations
 
 import json
 
-from ..models import SummaryState
-from ...configuration import Configuration
-from ...utils import strip_thinking_tokens
-from ..agents.tool_aware_agent import ToolAwareSimpleAgent
-from .text_processing import strip_tool_calls
+from hello_agents import ToolAwareSimpleAgent
+
+from models import SummaryState
+from config import Configuration
+from utils import strip_thinking_tokens
+from services.text_processing import strip_tool_calls
 
 
 class ReportingService:

+ 2 - 2
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/search_service.py → code/chapter14/helloagents-deepresearch/backend/src/services/search.py

@@ -7,8 +7,8 @@ from typing import Any, Optional, Tuple
 
 from hello_agents.tools import SearchTool
 
-from ...configuration import Configuration
-from ...utils import (
+from config import Configuration
+from utils import (
     deduplicate_and_format_sources,
     format_sources,
     get_config_value,

+ 7 - 6
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/summarization_service.py → code/chapter14/helloagents-deepresearch/backend/src/services/summarizer.py

@@ -5,12 +5,13 @@ from __future__ import annotations
 from collections.abc import Callable, Iterator
 from typing import Tuple
 
-from ..models import SummaryState, TodoItem
-from ...configuration import Configuration
-from ...utils import strip_thinking_tokens
-from ..agents.tool_aware_agent import ToolAwareSimpleAgent
-from .notes_service import build_note_guidance
-from .text_processing import strip_tool_calls
+from hello_agents import ToolAwareSimpleAgent
+
+from models import SummaryState, TodoItem
+from config import Configuration
+from utils import strip_thinking_tokens
+from services.notes import build_note_guidance
+from services.text_processing import strip_tool_calls
 
 
 class SummarizationService:

+ 0 - 0
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/text_processing.py → code/chapter14/helloagents-deepresearch/backend/src/services/text_processing.py


+ 1 - 1
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/app/services/tool_events.py → code/chapter14/helloagents-deepresearch/backend/src/services/tool_events.py

@@ -9,7 +9,7 @@ from pathlib import Path
 from threading import Lock
 from typing import Any, Callable, Optional
 
-from ..models import SummaryState, TodoItem
+from models import SummaryState, TodoItem
 
 logger = logging.getLogger(__name__)
 

+ 0 - 0
code/chapter14/helloagents-deepresearch/backend/src/deep_researcher/utils.py → code/chapter14/helloagents-deepresearch/backend/src/utils.py


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 909
code/chapter14/helloagents-deepresearch/backend/uv.lock


+ 288 - 7
code/chapter14/helloagents-deepresearch/frontend/src/App.vue

@@ -1,13 +1,14 @@
 <template>
-  <main class="app-shell">
+  <main class="app-shell" :class="{ expanded: isExpanded }">
     <div class="aurora" aria-hidden="true">
       <span></span>
       <span></span>
       <span></span>
     </div>
 
-    <div class="layout">
-      <section class="panel panel-form">
+    <!-- 初始状态:居中输入卡片 -->
+    <div v-if="!isExpanded" class="layout layout-centered">
+      <section class="panel panel-form panel-centered">
         <header class="panel-head">
           <div class="logo">
             <svg viewBox="0 0 24 24" aria-hidden="true">
@@ -86,8 +87,54 @@
           正在收集线索与证据,实时进展见右侧区域。
         </p>
       </section>
+    </div>
 
-            <section
+    <!-- 全屏状态:左右分栏布局 -->
+    <div v-else class="layout layout-fullscreen">
+      <!-- 左侧:研究信息 -->
+      <aside class="sidebar">
+        <div class="sidebar-header">
+          <button class="back-btn" @click="goBack" :disabled="loading">
+            <svg viewBox="0 0 24 24" width="20" height="20">
+              <path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+            返回
+          </button>
+          <h2>🔍 深度研究助手</h2>
+        </div>
+
+        <div class="research-info">
+          <div class="info-item">
+            <label>研究主题</label>
+            <p class="topic-display">{{ form.topic }}</p>
+          </div>
+
+          <div class="info-item" v-if="form.searchApi">
+            <label>搜索引擎</label>
+            <p>{{ form.searchApi }}</p>
+          </div>
+
+          <div class="info-item" v-if="totalTasks > 0">
+            <label>研究进度</label>
+            <div class="progress-bar">
+              <div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></div>
+            </div>
+            <p class="progress-text">{{ completedTasks }} / {{ totalTasks }} 任务完成</p>
+          </div>
+        </div>
+
+        <div class="sidebar-actions">
+          <button class="new-research-btn" @click="startNewResearch">
+            <svg viewBox="0 0 24 24" width="18" height="18">
+              <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
+            </svg>
+            开始新研究
+          </button>
+        </div>
+      </aside>
+
+      <!-- 右侧:研究结果 -->
+      <section
         class="panel panel-result"
         v-if="todoTasks.length || reportMarkdown || progressLogs.length"
       >
@@ -338,6 +385,7 @@ const loading = ref(false);
 const error = ref("");
 const progressLogs = ref<string[]>([]);
 const logsCollapsed = ref(false);
+const isExpanded = ref(false);
 
 const todoTasks = ref<TodoTaskView[]>([]);
 const activeTaskId = ref<number | null>(null);
@@ -633,6 +681,7 @@ const handleSubmit = async () => {
 
   loading.value = true;
   error.value = "";
+  isExpanded.value = true;
   resetWorkflowState();
 
   const controller = new AbortController();
@@ -917,6 +966,23 @@ const cancelResearch = () => {
   currentController.abort();
 };
 
+const goBack = () => {
+  if (loading.value) {
+    return; // 研究进行中不允许返回
+  }
+  isExpanded.value = false;
+};
+
+const startNewResearch = () => {
+  if (loading.value) {
+    cancelResearch();
+  }
+  resetWorkflowState();
+  isExpanded.value = false;
+  form.topic = "";
+  form.searchApi = "";
+};
+
 onBeforeUnmount(() => {
   if (currentController) {
     currentController.abort();
@@ -933,10 +999,17 @@ onBeforeUnmount(() => {
   padding: 72px 24px;
   display: flex;
   justify-content: center;
+  align-items: center;
   background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
   color: #1f2937;
   overflow: hidden;
   box-sizing: border-box;
+  transition: padding 0.4s ease;
+}
+
+.app-shell.expanded {
+  padding: 0;
+  align-items: stretch;
 }
 
 .aurora {
@@ -979,12 +1052,24 @@ onBeforeUnmount(() => {
 
 .layout {
   position: relative;
-  width: min(1280px, 100%);
+  width: 100%;
   display: flex;
-  flex-wrap: wrap;
   gap: 24px;
   z-index: 1;
-  align-items: flex-start;
+  transition: all 0.4s ease;
+}
+
+.layout-centered {
+  max-width: 600px;
+  justify-content: center;
+  align-items: center;
+}
+
+.layout-fullscreen {
+  height: 100vh;
+  max-width: 100%;
+  gap: 0;
+  align-items: stretch;
 }
 
 .panel {
@@ -1003,6 +1088,20 @@ onBeforeUnmount(() => {
   max-width: 420px;
 }
 
+.panel-centered {
+  width: 100%;
+  max-width: 600px;
+  padding: 40px;
+  box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
+  transform: scale(1);
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.panel-centered:hover {
+  transform: scale(1.02);
+  box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
+}
+
 .panel-result {
   min-width: 360px;
   flex: 2 1 420px;
@@ -2020,4 +2119,186 @@ select:focus {
     font-size: 24px;
   }
 }
+
+/* 侧边栏样式 */
+.sidebar {
+  width: 400px;
+  min-width: 400px;
+  height: 100vh;
+  background: rgba(255, 255, 255, 0.98);
+  border-right: 1px solid rgba(148, 163, 184, 0.2);
+  padding: 32px 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  overflow-y: auto;
+  box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
+}
+
+.sidebar-header {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.sidebar-header h2 {
+  font-size: 24px;
+  font-weight: 700;
+  margin: 0;
+  color: #1f2937;
+}
+
+.back-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 16px;
+  background: transparent;
+  border: 1px solid rgba(148, 163, 184, 0.3);
+  border-radius: 12px;
+  color: #64748b;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  width: fit-content;
+}
+
+.back-btn:hover:not(:disabled) {
+  background: rgba(59, 130, 246, 0.1);
+  border-color: #3b82f6;
+  color: #3b82f6;
+}
+
+.back-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.research-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.info-item {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.info-item label {
+  font-size: 12px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  color: #64748b;
+}
+
+.info-item p {
+  margin: 0;
+  font-size: 14px;
+  color: #1f2937;
+  line-height: 1.6;
+}
+
+.topic-display {
+  font-size: 16px !important;
+  font-weight: 600;
+  color: #0f172a !important;
+  padding: 12px;
+  background: rgba(59, 130, 246, 0.05);
+  border-radius: 8px;
+  border-left: 3px solid #3b82f6;
+}
+
+.progress-bar {
+  width: 100%;
+  height: 8px;
+  background: rgba(148, 163, 184, 0.2);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
+  border-radius: 4px;
+  transition: width 0.5s ease;
+}
+
+.progress-text {
+  font-size: 13px !important;
+  color: #64748b !important;
+  font-weight: 500;
+}
+
+.sidebar-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-top: 16px;
+  border-top: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.new-research-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 14px 20px;
+  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
+  border: none;
+  border-radius: 12px;
+  color: white;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
+}
+
+.new-research-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
+}
+
+.new-research-btn:active {
+  transform: translateY(0);
+}
+
+/* 全屏状态下的结果面板 */
+.layout-fullscreen .panel-result {
+  flex: 1;
+  height: 100vh;
+  border-radius: 0;
+  border: none;
+  overflow-y: auto;
+  max-width: none;
+}
+
+@media (max-width: 1024px) {
+  .sidebar {
+    width: 320px;
+    min-width: 320px;
+  }
+}
+
+@media (max-width: 768px) {
+  .layout-fullscreen {
+    flex-direction: column;
+  }
+
+  .sidebar {
+    width: 100%;
+    min-width: 100%;
+    height: auto;
+    max-height: 40vh;
+  }
+
+  .layout-fullscreen .panel-result {
+    height: 60vh;
+  }
+}
 </style>

+ 2029 - 477
docs/chapter14/第十四章 自动化深度研究智能体.md

@@ -1,7 +1,6 @@
 # 第十四章 自动化深度研究智能体
 
-在第十三章的旅行助手项目中,我们体验了如何将HelloAgents应用于一个复杂但闭环的多智能体产品。本章我们继续向前,聚焦「知识密集型应用」:
-<strong>构建一个能够自动化执行深度研究任务的智能体体系。</strong>
+在第十三章的旅行助手项目中,我们体验了如何将HelloAgents应用于一个多智能体产品。本章我们继续向前,聚焦「知识密集型应用」:<strong>构建一个能够自动化执行深度研究任务的智能体助手。</strong>
 
 相比旅行规划,深度研究的难点在于信息的不断发散、事实的快速更新以及用户对引用来源的高要求。为了交付可信的研究报告,我们需要让智能体具备三个核心能力:
 
@@ -11,590 +10,2143 @@
 
 <strong>(3)反思与总结</strong>:依据阶段结果识别知识空白,决定是否继续检索,并生成结构化总结。
 
-本章的目标是带你深入理解 `helloagents-deepresearch` 项目,掌握其架构、运行方式与扩展技巧,最终能够独立运营一套本地可控的研究助手。
-
-
-## 14.1 项目概述与场景定位
-
-### 14.1.1 为什么需要自动化深度研究智能体
-
-当话题复杂且信息分散时,单纯依赖搜索引擎难以获得结构化洞见。常见的痛点包括:
-
-- 信息碎片化:结果散落在不同站点,需要手动筛选、比对可靠性。
-- 维度不平衡:容易沉迷于同类观点,忽略实现细节、对比数据或最新进展。
-- 时间成本高:人工整理引用、撰写总结耗时,且难以复盘全过程。
-
-自动化深度研究智能体的优势在于它能够:
-
-- 以智能体循环执行「生成查询→检索→总结→反思」流程,保证主题覆盖度。
-- 在本地运行(Ollama、LMStudio),保护敏感主题,同时避免外部服务波动。
-- 输出带引用的Markdown报告,便于复盘、分享与二次编辑。
-
-### 14.1.2 整体能力与用户价值
-
-`helloagents-deepresearch` 项目将后端HelloAgents智能体、HelloAgents 内置工具体系与前端可视化界面结合,形成「输入主题→实时观察→获取总结」的闭环体验。整体亮点如下:
-
-- <strong>多提供者模型接入</strong>:支持 Ollama、LMStudio,或通过 `LLM_PROVIDER=custom` 自定义任意 OpenAI 兼容服务,自主选择推理能力与成本。
-- <strong>多搜索源融合</strong>:直接复用 HelloAgents 的 `SearchTool`,内置 Tavily、SerpApi、DuckDuckGo、SearXNG、Perplexity 以及高级混合策略,可按需切换。
-- <strong>流式反馈</strong>:后端通过 Server-Sent Events 推送各阶段结果,前端即时展示时间线、最新来源和逐字更新的总结。
-- <strong>配置优先级清晰</strong>:环境变量、代码默认值分层管理,方便调试与部署。
-系统采用经典的<strong>前后端分离架构</strong>,分为四个层次,如图14.1所示
-
-```mermaid
-graph LR
-  U((用户))
-  subgraph FE[前端(Vue 3 + TypeScript)]
-    UI[App.vue 研究界面]
-    FEAPI[services/api.ts\nrunResearchStream]
-  end
-  subgraph BE[后端(FastAPI DeepResearcher)]
-    API[/POST /research + /research/stream/]
-    Config[Configuration.from_env]
-    subgraph Workflow[DeepResearchAgent 工作流]
-      Planner{{PlanningService\n任务规划}}
-      Search{{dispatch_search\n(SearchTool)}}
-      Summarizer{{SummarizationService\n任务总结}}
-      Reporter{{ReportingService\n报告整合}}
-      Tracker[[ToolCallTracker\n工具事件]]
-    end
-  end
-  subgraph LLMTools[LLM + 工具层]
-    LLM[(HelloAgentsLLM)]
-    NoteTool[(NoteTool\nToolRegistry)]
-  end
-  subgraph External[外部依赖]
-    SearchAPI[(HelloAgents SearchTool\nTavily · SerpApi · DuckDuckGo ·\nSearXNG · Perplexity · Advanced)]
-    Notes[(本地笔记\nnotes_workspace)]
-  end
-
-  U -->|交互| UI
-  UI -->|提交 topic/search_api| FEAPI
-  FEAPI -->|POST 请求| API
-  API -->|构建配置| Config
-  Config -->|实例化| Workflow
-  Workflow -->|加载模型| LLM
-  Workflow -->|注册工具| NoteTool
-  LLM --> Planner
-  LLM --> Summarizer
-  LLM --> Reporter
-  Planner -->|TODO 列表| Workflow
-  Workflow -->|SSE todo_list| API
-  Workflow --> Search
-  Search -->|调用| SearchAPI
-  SearchAPI --> Search
-  Search --> Summarizer
-  Summarizer -->|摘要片段| Workflow
-  Planner --> Tracker
-  Summarizer --> Tracker
-  Reporter --> Tracker
-  NoteTool --> Tracker
-  NoteTool -->|读写| Notes
-  Notes --> NoteTool
-  Workflow --> Reporter
-  Reporter -->|最终报告| Workflow
-  Workflow -->|final_report/done SSE| API
-  Tracker -->|tool_call SSE| API
-  API -->|Server-Sent Events| FEAPI
-  FEAPI -->|更新状态/日志| UI
-  UI -->|结果呈现| U
-```
-
-图14.1 整体架构图
-
-下一节我们会详细拆解技术架构与数据流转,理解智能体在每一轮循环中的决策与协作方式。
-
-## 14.2 技术架构与流程
-
-### 14.2.1 智能体循环的核心机制
-
-`helloagents-deepresearch` 的核心仍然是 `DeepResearchAgent`,但它不再让单个智能体“单线程”执行所有步骤,而是复用前几章学到的能力组合成一个协作团队:
-
-- <strong>研究规划专家</strong>(`PlanningService` 驱动的 `ToolAwareSimpleAgent`)——由第七章扩展过的 SimpleAgent 模板派生,专门负责把主题拆解成结构化任务。
-- <strong>任务总结专家</strong>(`SummarizationService`)——在每个任务的上下文就绪后撰写总结,并通过第九章的 `NoteTool` 进行笔记读写。
-- <strong>报告撰写专家</strong>(`ReportingService`)——整合所有任务笔记,产出最终报告并落盘。
-
-三位专家共享同一套工具注册表,其中既包括我们在第七章实现的多源搜索工具,也包括第九章整合的笔记管理能力。
-
-新版 `DeepResearchAgent` 的执行顺序如下:
-
-1. `PlanningService` 启动「研究规划专家」,由模型一次性列出 3~5 条任务,每条任务都会绑定唯一的 `task_id`、笔记标签与 `stream_token`。
-2. `run_stream` 为每条任务创建后台线程,线程内部依次完成搜索、总结、笔记写入,并通过队列把 SSE 事件推回主线程。
-3. `ReportingService` 等待所有任务完成后,再触发「报告撰写专家」,生成最终 Markdown 并固化到结论笔记中。
-
-以下代码片段(`backend/src/deep_researcher/app/agents/deep_research_agent.py:126` 起)展示了并行执行的核心结构:
+## 14.1 项目概述与架构设计
 
-```python
-channel_map = {task.id: {"step": index, "token": f"task_{task.id}"}
-               for index, task in enumerate(state.todo_items, start=1)}
+### 14.1.1 为什么需要深度研究助手
 
-def worker(task: TodoItem, step: int) -> None:
-    try:
-        enqueue({
-            "type": "task_status",
-            "task_id": task.id,
-            "status": "in_progress",
-            "title": task.title,
-        })
-        for event in self._execute_task(state, task, emit_stream=True, step=step):
-            enqueue(event)
-    finally:
-        enqueue({"type": "__task_done__", "task_id": task.id})
-
-threads = [Thread(target=worker, args=(task, channel_map[task.id]["step"]))
-           for task in state.todo_items]
-for thread in threads:
-    thread.start()
-```
-
-`enqueue` 会自动补全 `step`、`stream_token` 等字段,保证前端可以按任务维度渲染事件。得益于这种结构,多个任务可以同时检索、总结,再通过主线程串行输出,既不会丢事件,也避免了线程安全问题。
-
-下图给出了各智能体协同完成研究流程的职责分层与数据流向(图14.2):
-
-```mermaid
-graph TD
-    TOPIC["研究主题输入"] --> DRA["DeepResearchAgent<br/>协调器"]
-    subgraph Shared["共享资源"]
-        CFG["Configuration"]
-        LLM["LLM 实例"]
-        NOTE["NoteTool / ToolRegistry"]
-        TRACKER["ToolCallTracker"]
-    end
-    CFG --> DRA
-    LLM --> DRA
-    NOTE -.注册.- DRA
-    TRACKER --> DRA
-    DRA --> STATE["SummaryState / TodoItem"]
-
-    subgraph Planning["规划阶段"]
-        PLAN_AGENT["研究规划专家<br/>ToolAwareSimpleAgent"]
-        PLAN_SERVICE["PlanningService"]
-    end
-    DRA --> PLAN_AGENT
-    PLAN_AGENT --> PLAN_SERVICE
-    PLAN_SERVICE -->|Todo 列表| STATE
-
-    subgraph Execution["任务执行"]
-        STATE --> TASK["TodoItem"]
-        TASK --> SEARCH["dispatch_search"]
-        SEARCH --> BACKENDS["搜索 API<br/>Tavily / Perplexity / DuckDuckGo / SearXNG / Advanced"]
-        SEARCH --> CONTEXT["prepare_research_context"]
-        CONTEXT --> SUMM_SERVICE["SummarizationService"]
-        DRA --> SUMM_AGENT["任务总结专家<br/>ToolAwareSimpleAgent"]
-        SUMM_AGENT --> SUMM_SERVICE
-        SUMM_SERVICE --> STATE
-        SUMM_SERVICE --> TRACKER
-    end
-
-    subgraph Reporting["报告阶段"]
-        REPORT_AGENT["报告撰写专家<br/>ToolAwareSimpleAgent"]
-        REPORT_SERVICE["ReportingService"]
-    end
-    DRA --> REPORT_AGENT
-    REPORT_AGENT --> REPORT_SERVICE
-    STATE --> REPORT_SERVICE
-    REPORT_SERVICE --> REPORT["最终报告 Markdown"]
-    REPORT --> STATE
-    REPORT --> NOTE
-    TRACKER --> STATE
-    REPORT -.SSE.-> OUTPUT["final_report / done"]
-    TRACKER -.SSE.-> OUTPUT
-```
-
-图14.2 Agents架构图
-
-
-### 14.2.2 后端组件与配置流
-
-深度研究智能体通过 FastAPI 暴露 `/research` 与 `/research/stream` 接口,主要模块如下(文件均位于 `backend/src/deep_researcher/app/` 下):
-
-- `agents/deep_research_agent.py`:顶层协调者,负责任务规划、并行执行与报告沉淀。
-- `services/planner_service.py`、`summarization_service.py`、`reporting_service.py`:分别封装计划、总结、报告逻辑,内部都复用了 `ToolAwareSimpleAgent`。
-- `services/tool_events.py`:跟踪 `note` 工具调用,把事件转换成 SSE,可见第九章 `NoteTool` 的集成成果。
-- `services/search_service.py`:作为 HelloAgents `SearchTool` 的薄包装,将结构化搜索结果与上下文整理给下游 Agent。
-- `configuration.py`、`api.py`:负责配置加载、HelloAgentsLLM 初始化与 HTTP 层的流式推送。
-
-数据流转顺序为:
-
-1. 前端提交主题、迭代次数、搜索 API(可选)。
-2. FastAPI 解析请求,合并配置并实例化 `DeepResearchAgent`。
-3. 智能体并行执行所有任务,并持续通过 SSE 推送 `tool_call`、`task_summary_chunk`、`sources`、`report_note` 等事件。
-4. 客户端结束时收到携带 `note_id` 的 `final_report` 与 `done` 事件,可直接展示、跳转或二次编辑。
+在信息爆炸的时代,我们每天都需要快速了解新的技术、概念或事件。传统的研究方式有几个痛点。首先是**信息过载**。搜索引擎返回成千上万的结果,你需要逐个点开链接,阅读大量内容,才能找到有用的信息。其次是**缺少结构**。即使找到了相关信息,这些信息往往是碎片化的,缺少系统性的组织。最后是**重复劳动**。每次研究新主题时,都需要重复"搜索→阅读→总结→整理"的过程。
 
-```python
-class Configuration(BaseModel):
-    max_web_research_loops: int = Field(default=3)
-    local_llm: str = Field(default="llama3.2")
-    llm_provider: str = Field(default="ollama")
-    search_api: SearchAPI = Field(default=SearchAPI.DUCKDUCKGO)
-    enable_notes: bool = Field(default=True)
-    notes_workspace: str = Field(default="./notes/deep_research")
-    fetch_full_page: bool = Field(default=True)
-    ollama_base_url: str = Field(default="http://localhost:11434")
-    lmstudio_base_url: str = Field(default="http://localhost:1234/v1")
-    strip_thinking_tokens: bool = Field(default=True)
-    use_tool_calling: bool = Field(default=False)
-    llm_api_key: Optional[str] = None
-    llm_base_url: Optional[str] = None
-    llm_model_id: Optional[str] = None
-
-    @classmethod
-    def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
-        raw_values: dict[str, Any] = {}
-
-        for field_name in cls.model_fields.keys():
-            env_key = field_name.upper()
-            if env_key in os.environ:
-                raw_values[field_name] = os.environ[env_key]
-
-        env_aliases = {
-            "local_llm": os.getenv("LOCAL_LLM"),
-            "llm_provider": os.getenv("LLM_PROVIDER"),
-            "llm_api_key": os.getenv("LLM_API_KEY"),
-            "llm_model_id": os.getenv("LLM_MODEL_ID"),
-            "llm_base_url": os.getenv("LLM_BASE_URL"),
-            "lmstudio_base_url": os.getenv("LMSTUDIO_BASE_URL"),
-            "ollama_base_url": os.getenv("OLLAMA_BASE_URL"),
-            "max_web_research_loops": os.getenv("MAX_WEB_RESEARCH_LOOPS"),
-            "fetch_full_page": os.getenv("FETCH_FULL_PAGE"),
-            "strip_thinking_tokens": os.getenv("STRIP_THINKING_TOKENS"),
-            "use_tool_calling": os.getenv("USE_TOOL_CALLING"),
-            "search_api": os.getenv("SEARCH_API"),
-            "enable_notes": os.getenv("ENABLE_NOTES"),
-            "notes_workspace": os.getenv("NOTES_WORKSPACE"),
-        }
+这就是深度研究助手需要解决的问题。它不仅仅是一个搜索工具,而是一个能够自主规划、执行和总结的研究助手。
+
+**深度研究助手的核心价值:**
+
+1. **节省时间**:将1-2小时的研究工作压缩到5-10分钟
+2. **提高质量**:系统化的研究流程,避免遗漏重要信息
+3. **可追溯**:记录所有搜索结果和来源,方便验证和引用
+4. **可扩展**:可以轻松添加新的搜索引擎、数据源和分析工具
+
+### 14.1.2 技术架构概览
+
+此次系统仍然采用经典的**前后端分离架构**,如图14.1所示。
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-1.png" alt="" width="85%"/>
+  <p>图 14.1 深度研究助手技术架构</p>
+</div>
+
+系统分为四层架构设计:
+
+**前端层 (Vue3+TypeScript)**:全屏模态对话框UI、Markdown结果可视化
+
+**后端层 (FastAPI)**:API路由(`/research/stream`)
 
-        for key, value in env_aliases.items():
-            if value is not None:
-                raw_values.setdefault(key, value)
+**智能体层 (HelloAgents)**:三个专门Agent(TODO Planner、Task Summarizer、Report Writer)+ 两个核心工具(SearchTool、NoteTool)
 
-        if overrides:
-            for key, value in overrides.items():
-                if value is not None:
-                    raw_values[key] = value
+**外部服务层**:搜索引擎+ LLM提供商
+
+让我们看看一个完整的研究请求是如何在系统中流转的,如图14.2所示:
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-2.png" alt="" width="85%"/>
+  <p>图 14.2 深度研究助手数据流转过程</p>
+</div>
+
+1. **用户输入**:用户在前端输入研究主题
+2. **前端发送**:前端通过SSE连接到`/research/stream`
+3. **后端接收**:FastAPI接收请求,创建研究状态
+4. **规划阶段**:调用研究规划Agent,分解为3个子任务
+5. **执行阶段**:逐个执行每个子任务
+   - 使用SearchTool搜索
+   - 调用任务总结Agent总结
+   - 使用NoteTool记录结果
+6. **报告阶段**:调用报告生成Agent,整合所有总结
+7. **流式返回**:通过SSE推送进度和结果到前端
+8. **前端展示**:前端实时更新任务状态、进度条、日志、报告
+
+项目的目录结构如下:
 
-        return cls(**raw_values)
+```
+helloagents-deepresearch/
+├── backend/                    # 后端代码
+│   ├── src/
+│   │   ├── agent.py           # 核心协调器
+│   │   ├── main.py            # FastAPI入口
+│   │   ├── models.py          # 数据模型
+│   │   ├── prompts.py         # Prompt模板
+│   │   ├── config.py          # 配置管理
+│   │   └── services/          # 服务层
+│   │       ├── planner.py     # 规划服务
+│   │       ├── summarizer.py  # 总结服务
+│   │       ├── reporter.py    # 报告服务
+│   │       └── search.py      # 搜索服务
+│   ├── .env                   # 环境变量
+│   ├── pyproject.toml         # 依赖管理
+│   └── workspace/             # 研究笔记
+│
+└── frontend/                   # 前端代码
+    ├── src/
+    │   ├── App.vue            # 主组件
+    │   ├── components/        # UI组件
+    │   │   └── ResearchModal.vue
+    │   └── composables/       # 组合式函数
+    │       └── useResearch.ts
+    ├── package.json           # npm依赖
+    └── vite.config.ts         # 构建配置
 ```
 
-配置解析逻辑(`backend/src/deep_researcher/configuration.py:18`)先读取所有大写环境变量,再应用显式别名,最后才合并 API 请求的覆盖值。需要注意的是,从本章起 HelloAgents 原生 `HelloAgentsLLM` 已支持 `LLM_PROVIDER=custom`:只要同时提供 `LLM_BASE_URL` 与 `LLM_API_KEY` 即可连接任意 OpenAI 兼容服务,而 `LOCAL_LLM`/`OLLAMA_BASE_URL` 等字段继续服务于本地模型场景。实际运行时意味着:
+### 14.1.3 快速体验:5分钟运行项目
 
-- `.env` 或系统环境变量拥有最高优先级,便于本地调试和生产部署。
-- LangGraph/UI 提交的临时参数通过 `overrides` 注入,不会修改全局环境。
-- 字段均由 Pydantic 校验,错误值会在启动阶段被捕获,避免无效配置导致的运行时故障。
+在深入学习实现细节之前,让我们先把项目跑起来,看看最终的效果。这样你会对整个系统有一个直观的认识。
 
-### 14.2.3 自定义多源搜索工具
+你可以通过以下命令检查版本:
 
-为了适应不同团队的检索与合规要求,本项目直接复用 HelloAgents 框架的 `SearchTool`。这一工具现已支持 Tavily、SerpApi、DuckDuckGo、SearXNG、Perplexity 以及「advanced」混合策略,并且同一份结构化返回可以被多个 Agent 共享。因此在深度研究项目里,我们只需要按需配置即可:
+```bash
+python --version  # 应该显示 Python 3.10.x 或更高
+node --version    # 应该显示 v16.x.x 或更高
+npm --version     # 应该显示 8.x.x 或更高
+```
 
-1. **选择后端**:将 `SEARCH_API` 设为 `tavily`、`serpapi`、`duckduckgo`、`searxng`、`perplexity` 或 `advanced`。其中 `hybrid` 与 `advanced` 等价,都会优先尝试 Tavily/SerpApi,再降级到 DuckDuckGo。
-2. **配置密钥**:根据选定后端设置 `TAVILY_API_KEY`、`SERPAPI_API_KEY`、`PERPLEXITY_API_KEY` 等环境变量;若只想使用无密钥的 DuckDuckGo/SearXNG,可不设置。
-3. **结构化输出**:`SearchTool` 默认返回友好的文本描述,当我们在 `services/search_service.py` 中传入 `mode=structured` 时,会得到统一的 `{"results": [...], "answer": ..., "notices": [...]}` 结构,方便后续做去重、裁剪、引用。
-4. **深度定制(可选)**:如果还需要扩展新的搜索后端,可以在 HelloAgents 仓库内直接继承 `SearchTool` 并覆盖 `_search_xxx` 方法,或者提交 PR 将新后端合入框架。由于深度研究项目只是框架的“薄封装”,一旦上游合并,所有下游应用都会自动获得能力升级。
+(1)启动后端
 
-这种设计让学习者无需在项目中维护额外的搜索适配器,即可通过配置或框架升级获得最新能力;同时也保留了在企业环境内扩展私有搜索源的灵活度。
+```bash
+# 1. 进入后端目录
+cd helloagents-deepresearch/backend
 
-> 提示:配置项 `ENABLE_NOTES` 默认为 `true`。当启用时,后端会为每个任务自动同步一份 Markdown 笔记(目录由 `NOTES_WORKSPACE` 指定),并把 `note` 工具挂载到所有 Agent,方便它们在需要时对笔记做增删改查。流式事件会附带 `note_id`,前端可据此展示或跳转对应笔记。
+# 2. 安装依赖
+# 方式1:使用uv(推荐,更快的Python包管理器)
+uv sync
 
-### 14.2.4 前端交互与流式反馈
+# 方式2:使用pip
+pip install -e .
 
-前端基于 Vue 3 + Vite,核心页面是 `App.vue`,其职责包括:
+# 3. 配置环境变量
+cp .env.example .env
 
-- 通过 `runResearchStream` 调用 `/research/stream`,逐条解析 SSE 数据,并依据 `task_id` / `stream_token` 把事件路由到对应任务卡片。
-- 监听 `tool_call` 与 `report_note` 事件,实时展示笔记更新与最终报告位置。
-- 支持用户在研究进行时主动取消请求(`AbortController`),确保界面响应及时。
-- 提供表单化配置入口(迭代次数、搜索引擎选择),与后端的覆盖策略一一对应。
+# 4. 编辑.env文件,填入你的API密钥
+# 使用你喜欢的编辑器打开.env文件
+# 至少需要配置:
+# - LLM_PROVIDER(如 openai、deepseek、qwen)
+# - LLM_API_KEY(你的LLM API密钥)
+# - SEARCH_API(如 duckduckgo、tavily)
 
-UI 设计强调“实时性”与“可追溯性”:时间线用于说明状态变化,来源列表/历史面板展示引用详情,摘要与报告区域则会在收到 `task_summary_chunk`、`report_note` 等事件时滚动高亮,提示用户关注最新内容。
+# 5. 启动后端
+python src/main.py
+```
 
-```typescript
-export async function runResearchStream(
-  payload: ResearchRequest,
-  onEvent: (event: ResearchStreamEvent) => void,
-  options: StreamOptions = {}
-): Promise<void> {
-  const response = await fetch(`${baseURL}/research/stream`, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      Accept: "text/event-stream"
-    },
-    body: JSON.stringify(payload),
-    signal: options.signal
-  });
-
-  if (!response.ok) {
-    const errorText = await response.text().catch(() => "");
-    throw new Error(errorText || `研究请求失败,状态码:${response.status}`);
-  }
+如果一切正常,你会看到类似的输出:
 
-  const body = response.body;
-  if (!body) {
-    throw new Error("浏览器不支持流式响应,无法获取研究进度");
-  }
+```
+INFO:     Started server process [12345]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
 
-  const reader = body.getReader();
-  const decoder = new TextDecoder("utf-8");
-  let buffer = "";
+(2)启动前端
 
-  while (true) {
-    const { value, done } = await reader.read();
-    buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
+打开一个新的终端窗口:
 
-    let boundary = buffer.indexOf("\n\n");
-    while (boundary !== -1) {
-      const rawEvent = buffer.slice(0, boundary).trim();
-      buffer = buffer.slice(boundary + 2);
+```bash
+# 1. 进入前端目录
+cd helloagents-deepresearch/frontend
 
-      if (rawEvent.startsWith("data:")) {
-        const dataPayload = rawEvent.slice(5).trim();
-        if (dataPayload) {
-          const event = JSON.parse(dataPayload) as ResearchStreamEvent;
-          onEvent(event);
+# 2. 安装依赖
+npm install
 
-          if (event.type === "error" || event.type === "done") {
-            return;
-          }
-        }
-      }
+# 3. 启动前端
+npm run dev
+```
 
-      boundary = buffer.indexOf("\n\n");
-    }
+如果一切正常,你会看到类似的输出:
 
-    if (done) {
-      if (buffer.trim()) {
-        const rawEvent = buffer.trim();
-        if (rawEvent.startsWith("data:")) {
-          const event = JSON.parse(rawEvent.slice(5).trim()) as ResearchStreamEvent;
-          onEvent(event);
-        }
-      }
-      break;
-    }
-  }
-}
 ```
+  VITE v5.0.0  ready in 500 ms
 
-这段 TypeScript 实现(`frontend/src/services/api.ts:1`)通过原生 `ReadableStream` 持续解析 SSE:
+  ➜  Local:   http://localhost:5174/
+  ➜  Network: use --host to expose
+  ➜  press h + enter to show help
+```
 
-- 逐块累积缓冲区并按 `\n\n` 分割事件,兼容浏览器对流式分片的差异化处理。
-- 一旦收到 `error` 或 `done` 事件便主动收尾,防止前端挂起。
-- 支持传入 `AbortController`,与界面上的“取消研究”按钮形成闭环,用户体验更加可控。
+(3)开始研究
 
-![14.3 UI交互图](docs/images/14-figures/14-3.png)
-图14.3 UI交互图
+打开浏览器访问 `http://localhost:5174`,你会看到一个居中的输入卡片,如图14.3所示。输入研究主题,例如`Datawhale是一个什么样的组织?`,选择搜索引擎(如果配置了多个),点击"开始研究"按钮。
 
-## 14.3 快速上手指南
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-3.png" alt="" width="85%"/>
+  <p>图 14.3 深度研究助手搜索页面</p>
+</div>
 
-为了在本地体验深度研究智能体,我们建议分三步完成:准备运行环境、配置模型与搜索服务、分别启动后端与前端。以下流程基于 `code/chapter14/helloagents-deepresearch/` 目录。
+如图14.4所示,系统会自动展开为全屏,左侧显示研究信息,右侧实时显示研究进度和结果。整个研究过程大约需要1-3分钟,取决于主题的复杂度和搜索引擎的响应速度
 
-### 14.3.1 环境要求与依赖安装
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-4.png" alt="" width="85%"/>
+  <p>图 14.4 深度研究助手展开研究</p>
+</div>
 
-- Python ≥ 3.11(后端使用 `pyproject.toml` 配合 `uv` 或 `pip` 安装依赖)
-- Node.js ≥ 18.0(建议 20+)与 npm ≥ 9.0
-- 至少一种本地大模型服务:Ollama 或 LMStudio或API供应商
+研究完成后,你会看到:
 
-推荐使用虚拟环境隔离依赖:
+- **任务列表**:显示所有子任务及其状态
+- **进度日志**:显示研究过程中的所有操作
+- **最终报告**:结构化的Markdown报告,包含所有子任务的总结和来源引用
 
-```bash
-cd code/chapter14/helloagents-deepresearch/backend
-python -m venv .venv
-source .venv/bin/activate  
+现在你已经成功运行了深度研究助手,对系统有了直观的认识。
+
+## 14.2 TODO驱动的研究范式
+
+### 14.2.1 什么是TODO驱动的研究
+
+传统的搜索引擎只能回答单个问题,而深度研究需要回答一系列相关的问题。TODO驱动的研究范式将复杂的研究主题分解为多个子任务(TODO),逐个执行并整合结果。
+
+这种范式的核心思想是:**将"研究"这个复杂任务转化为"规划→执行→整合"的流程**。
+
+让我们通过一个例子来理解这个转变。假设你想研究"Datawhale是一个什么样的组织?",传统的搜索方式是:
+
+```
+用户输入:Datawhale是一个什么样的组织?
+搜索引擎:返回10-20个链接
+用户:逐个点开链接,阅读内容,记录笔记
+结果:碎片化的信息,缺少系统性
 ```
 
-随后安装 Python 依赖:
+这种方式的问题在于每个链接只涵盖主题的一个方面、缺少系统性结构,需要手动整理和总结。
 
-```bash
-uv sync  # 若未安装 uv,可先使用pip install uv 来进行安装
+**TODO驱动方式:系统化研究**
+
+```
+用户输入:Datawhale是一个什么样的组织?
+
+系统规划:
+  ├─ TODO 1:Datawhale的基本信息(组织定位)
+  ├─ TODO 2:Datawhale的主要项目(核心内容)
+  ├─ TODO 3:Datawhale的社区文化(价值观)
+  └─ TODO 4:Datawhale的影响力(社会贡献)
+
+系统执行:
+  对每个TODO:
+    1. 搜索相关资料
+    2. 总结关键信息
+    3. 记录来源引用
+
+系统整合:
+  生成结构化报告:
+    ├─ 第一部分:组织定位(来自TODO 1)
+    ├─ 第二部分:核心内容(来自TODO 2)
+    ├─ 第三部分:价值观(来自TODO 3)
+    ├─ 第四部分:社会贡献(来自TODO 4)
+    └─ 参考文献:所有来源引用
 ```
 
-前端部分进入 `frontend` 目录执行 `npm install` 即可。
+这种方式的优势在于将复杂主题分解为清晰的子问题,每个子任务的搜索结果和总结都被记录下来,方便追溯。同时,系统化的研究流程避免了遗漏重要信息,可以轻松添加新的子任务或调整执行顺序
 
-### 14.3.2 配置本地模型与搜索服务
+一个完整的TODO驱动研究系统包含三个核心要素:
 
-后端默认读取同目录下的 `.env` 文件。参考步骤:
+**(1)智能规划器(TODO Planner)**:负责将研究主题分解为子任务。一个好的规划器需要理解主题的关键方面和研究目标,将主题分解为3-5个子任务(太少覆盖不全,太多会冗余),并为每个子任务设计合适的搜索查询。
 
-```bash
-cp .env.example .env
+**(2)任务执行器(Task Executor)**:负责执行每个子任务。执行器需要使用搜索引擎获取相关资料,提取关键信息并去除冗余内容,同时保存所有来源引用以方便验证。
+
+**(3)报告生成器(Report Writer)**:负责整合所有子任务的结果。生成器需要按照逻辑顺序组织内容,合并重复的信息,并为每个观点添加来源引用。
+
+在我们的案例里,TODO驱动的研究流程如图14.5所示:
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-5.png" alt="" width="85%"/>
+  <p>图 14.5 TODO驱动的研究流程</p>
+</div>
+
+
+整个流程是线性的,但每个阶段都有明确的输入和输出。这种设计使得系统易于理解和调试。
+
+### 14.2.2 三阶段研究流程
+
+TODO驱动的研究流程分为三个阶段:规划(Planning)、执行(Execution)、报告(Reporting)。每个阶段都有专门的Agent负责。
+
+**(1)阶段1:规划**
+
+规划阶段的目标是将研究主题分解为3-5个子任务。系统接收研究主题和当前日期作为输入,输出JSON格式的子任务列表。每个子任务包含三个字段:title(任务标题)、intent(研究意图)和query(搜索查询)。
+
+研究规划Agent会根据主题特点采用不同的分解策略,通常从基础概念入手,然后了解技术现状、实际应用和发展趋势,必要时还会进行对比分析。例如,对于"Datawhale是一个什么样的组织?",规划Agent可能生成以下子任务:
+
+```json
+[
+  {
+    "title": "Datawhale的基本信息",
+    "intent": "了解Datawhale的组织定位、成立时间、发展历程",
+    "query": "Datawhale organization introduction history 2024"
+  },
+  {
+    "title": "Datawhale的主要项目",
+    "intent": "了解Datawhale的核心开源项目和教程",
+    "query": "Datawhale projects tutorials open source 2024"
+  },
+......
+]
 ```
 
-根据自身环境设置以下核心变量:
+一个好的规划应该覆盖全面、逻辑清晰、查询精准、条目数量适中。
 
-- 模型提供者:`LLM_PROVIDER=ollama`、`LLM_PROVIDER=lmstudio` 或 `LLM_PROVIDER=custom`
-- 模型名称:`LOCAL_LLM=deepseek-r1:8b`(Ollama)/`LOCAL_LLM=qwen_qwq-32b`(LMStudio)或云端场景下的 `LLM_MODEL_ID`
-- 服务地址:`OLLAMA_BASE_URL`、`LMSTUDIO_BASE_URL` 或通用 `LLM_BASE_URL`
-- 是否开启工具调用:`USE_TOOL_CALLING=true`
-- 搜索服务:`SEARCH_API=duckduckgo`、`tavily`、`perplexity`、`searxng`
+**(2)阶段2:执行**
 
-若选择 Tavily、Perplexity 或自建 SearXNG,请额外填写对应的 `TAVILY_API_KEY`、`PERPLEXITY_API_KEY` 或 `SEARXNG_URL`。
+执行阶段逐个执行每个子任务,搜索并总结相关资料。系统接收子任务列表和搜索引擎配置作为输入,输出每个子任务的总结(Markdown格式)和来源引用列表。执行流程如下:
 
-```bash
-# backend/.env 示例片段
-SEARCH_API='duckduckgo'
-LLM_PROVIDER=custom
-LLM_MODEL_ID=your-model-name
-LLM_API_KEY=your-api-key-here
-LLM_BASE_URL=your-api-base-url
-USE_TOOL_CALLING=true
-MAX_WEB_RESEARCH_LOOPS=3
-FETCH_FULL_PAGE=True
-# 切换至本地模型时,只需启用以下注释行:
-# LLM_PROVIDER=ollama
-# LOCAL_LLM=llama3.2
-# OLLAMA_BASE_URL=http://localhost:11434
+对于每个子任务,执行器会:
+
+1. **搜索资料**:使用配置的搜索引擎执行搜索
+
+   ```python
+   search_results = search_tool.run({
+       "input": task.query,
+       "backend": "tavily",
+       "mode": "structured",
+       "max_results": 5
+   })
+   ```
+
+2. **获取搜索结果**:提取标题、URL、摘要
+
+   ```json
+   {
+     "results": [
+       {
+         "title": "What is a Multimodal Model?",
+         "url": "https://example.com/multimodal-model",
+         "snippet": "A multimodal model is an AI model that can process multiple types of data..."
+       },
+       ...
+     ]
+   }
+   ```
+
+3. **调用总结Agent**:总结搜索结果
+
+   ```python
+   summary = summarizer_agent.run(
+       task=task,
+       search_results=search_results
+   )
+   ```
+
+4. **记录总结和来源**:保存到NoteTool
+
+   ```python
+   note_tool.run({
+       "action": "create",
+       "title": task.title,
+       "content": f"## {task.title}\n\n{summary}\n\n## 来源\n{sources}",
+       "tags": ["research", "summary"]
+   })
+   ```
+
+任务总结Agent会从每个搜索结果中提取核心观点,合并相似信息,保留重要的数字、日期、名称等关键数据,并为每个观点添加来源引用。例如,对于"Datawhale的基本信息"的搜索结果,总结Agent可能生成:
+
+```markdown
+## Datawhale的基本信息
+
+Datawhale是一个专注于数据科学与AI领域的开源组织,成立于2018年[1]。组织的核心使命是"for the learner,和学习者一起成长",致力于构建一个纯粹的学习社区[2]。
+
+**核心定位:**
+
+1. **开源教育平台**:提供高质量的AI和数据科学学习资源[1]
+2. **学习者社区**:汇聚了数万名AI学习者和实践者[3]
+3. **知识共享**:倡导开源精神,所有内容完全免费开放[2]
+
+**发展历程:**
+
+- **2018年**:Datawhale成立,发布首个开源教程[1]
+- **2020年**:成为国内领先的AI学习社区之一[3]
+- **2024年**:累计发布50+开源项目,影响10万+学习者[4]
+
+## 来源
+
+[1] https://github.com/datawhalechina
+[2] https://datawhale.club/about
+[3] https://www.zhihu.com/org/datawhale
+[4] https://datawhale.cn
 ```
 
-如 `.env.example` 所示(`backend/.env.example`),云端、自建、本地三种配置都以注释形式保留。保持模板不删,有助于在不同部署模式间快速切换。
-以下是调用 SiliconFlow API 作为大模型服务的示例环境配置:
-![14.5 SiliconFlow 环境配置](docs/images/14-figures/14-5.png)
+在执行过程中,系统会实时推送进度信息到前端:
 
-### 14.3.3 启动后端服务
+```json
+{
+  "type": "status",
+  "message": "正在搜索:Datawhale的基本信息"
+}
+```
 
-后端以 FastAPI 形式提供 HTTP API 与 SSE 流接口。启动方式:
+```json
+{
+  "type": "status",
+  "message": "正在总结搜索结果..."
+}
+```
 
-```bash
-cd code/chapter14/helloagents-deepresearch/backend
-uv run uvicorn deep_researcher.api:app --reload
+```json
+{
+  "type": "task",
+  "task": {
+    "id": 1,
+    "title": "Datawhale的基本信息",
+    "status": "completed"
+  }
+}
 ```
 
-成功后可访问 `http://localhost:8000/docs` 查看自动生成的 OpenAPI 文档,验证 `/research` 与 `/research/stream` 接口是否可用。
+**(3)阶段3:报告**
 
-![14.4 FastApi页面](docs/images/14-figures/14-4.png)
+报告阶段的目标是整合所有子任务的总结,生成最终报告。系统接收所有子任务的总结和研究主题作为输入,输出Markdown格式的最终报告。报告包含标题、概述、各个子任务的详细分析、总结和参考文献五个部分。例如,对于"Datawhale是一个什么样的组织?",最终报告可能是:
 
-### 14.3.4 启动前端与联调
+```markdown
+# Datawhale是一个什么样的组织?
 
-前端基于 Vite,默认通过 `.env` 或命令行指定后端地址:
+## 概述
 
-```bash
-cd code/chapter14/helloagents-deepresearch/frontend
-echo "VITE_API_BASE_URL=http://localhost:8000" > .env.local  # 可选
-npm run dev
+本报告系统地研究了Datawhale这个开源组织,涵盖基本信息、主要项目、社区文化和影响力四个方面。
+
+## 1. Datawhale的基本信息
+
+Datawhale是一个专注于数据科学与AI领域的开源组织,成立于2018年...
+
+(此处插入子任务1的总结)
+
+## 2. Datawhale的主要项目
+
+Datawhale发布了多个高质量的开源教程,包括Hello-Agents、Joyful-Pandas等...
+
+(此处插入子任务2的总结)
+......
+## 总结
+
+通过本次研究,我们了解了Datawhale的组织定位、核心项目、社区文化和社会贡献。Datawhale是一个纯粹的学习社区,为AI教育做出了重要贡献。
+
+## 参考文献
+
+[1] https://github.com/datawhalechina
+[2] https://datawhale.club/about
+...
 ```
 
-浏览器访问 `http://localhost:5173`,输入研究主题即可观察到流式时间线、来源列表和逐渐成型的总结。若需要部署或静态打包,可使用 `npm run build` 并将输出托管至任意静态服务器,注意同步配置 `VITE_API_BASE_URL`。
+报告生成Agent会按照子任务的逻辑顺序组织内容,在开头添加简要概述,合并重复的信息,统一Markdown格式,并将所有来源引用整理到参考文献部分。
+
+## 14.3 智能体系统设计
+
+### 14.3.1 Agent职责划分
+
+在深度研究助手中,我们设计了三个专门的Agent,每个Agent负责一个特定的任务。这使得每个Agent都很简单,易于理解和维护。
+
+在第七章中,我们学习了如何使用`SimpleAgent`来构建智能体。`SimpleAgent`的设计理念是简单直接:每次调用`run()`方法时,Agent会分析用户的问题,决定是否需要调用工具,然后返回结果。这种设计在处理简单任务时非常有效,但当面对深度研究这样的复杂任务时,就需要我们继续采用多智能体协作的方案进行。
+
+如表14.1所示,三个Agent分别负责规划、总结和报告生成。
 
-完成以上步骤后,一个全流程的深度研究助手便可以在本地运行。接下来我们将深入拆解关键模块的实现细节,理解为何它能稳定输出结构化的研究结果。
+<div align="center">
+  <p>表 14.1 三个Agent的职责划分</p>
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-table-1.png" alt="" width="85%"/>
+</div>
 
-## 14.4 关键模块拆解
+让我们详细介绍每个Agent的设计。
 
-从代码角度理解核心模块,有助于在后续扩展或排查问题时做到心中有数。本节按流程拆解查询生成、搜索适配、总结与反思逻辑,以及流式通信协议。
+**Agent 1:研究规划专家(TODO Planner)**
 
-### 14.4.1 任务规划:SimpleAgent 的再利用
+**职责**:将研究主题分解为3-5个子任务
 
-`PlanningService` 把第七章的 `SimpleAgent` 模板直接升级为多任务规划器。通过 `ToolAwareSimpleAgent` 的 `run` 方法,我们既继承了原有的 Prompt 能力,也让模型得以调用 `note` 工具记录任务框架。
+**设计理念**:研究规划专家的核心任务是理解用户的研究主题,分析主题的关键方面,然后生成一系列子任务。这个过程类似于人类研究者在开始研究前的"头脑风暴"阶段。
+
+**Prompt设计**:
 
 ```python
-def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
-    prompt = todo_planner_instructions.format(
-        current_date=get_current_date(),
-        research_topic=state.research_topic,
-    )
+todo_planner_instructions = """
+你是一个研究规划专家。你的任务是将用户的研究主题分解为3-5个子任务。
+
+当前日期:{current_date}
+
+研究主题:{research_topic}
+
+请分析这个研究主题,将其分解为3-5个子任务。每个子任务应该:
+1. 涵盖主题的一个重要方面
+2. 有明确的研究目标
+3. 可以通过搜索引擎找到相关资料
+
+请以JSON格式返回子任务列表,每个子任务包含:
+- title:任务标题(简洁明了)
+- intent:任务意图(为什么要研究这个)
+- query:搜索查询(用于搜索引擎的查询字符串,可以使用英文以获得更好的搜索结果)
+
+示例输出:
+[
+  {{
+    "title": "什么是多模态模型",
+    "intent": "了解多模态模型的基础概念,为后续研究打下基础",
+    "query": "multimodal model definition concept 2024"
+  }},
+  ...
+]
+
+请确保:
+1. 子任务数量在3-5个之间
+2. 子任务之间有逻辑关系(如从基础到应用,从现状到趋势)
+3. 搜索查询能够准确找到相关资料
+4. 只返回JSON,不要包含其他文本
+"""
+```
+
+**关键设计点**:提示词包含当前日期以获取最新信息,明确要求JSON格式输出便于解析,通过示例帮助Agent理解期望输出,并强调子任务数量、逻辑关系等约束。
 
-    response = self._agent.run(prompt)
-    self._agent.clear_history()
+**实现代码**:
 
-    tasks_payload = self._extract_tasks(response)
-    todo_items: List[TodoItem] = []
+这里的ToolAwareSimpleAgent是根据SimpleAgent拓展实现,可以在14.3.2了解,这里不用深究。
 
-    for idx, item in enumerate(tasks_payload, start=1):
-        task = TodoItem(
-            id=idx,
-            title=str(item.get("title") or f"任务{idx}").strip(),
-            intent=str(item.get("intent") or "聚焦主题的关键问题").strip(),
-            query=str(item.get("query") or state.research_topic).strip(),
+```python
+class PlanningService:
+    def __init__(self, llm: HelloAgentsLLM):
+        self._agent = ToolAwareSimpleAgent(
+            name="TODO Planner",
+            system_prompt="你是一个研究规划专家",
+            llm=llm,
+            tool_call_listener=self._on_tool_call
+        )
+    
+    def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
+        prompt = todo_planner_instructions.format(
+            current_date=get_current_date(),
+            research_topic=state.research_topic,
         )
-        todo_items.append(task)
+        
+        response = self._agent.run(prompt)
+        tasks_payload = self._extract_tasks(response)
+        
+        todo_items = []
+        for idx, item in enumerate(tasks_payload, start=1):
+            task = TodoItem(
+                id=idx,
+                title=item["title"],
+                intent=item["intent"],
+                query=item["query"],
+            )
+            todo_items.append(task)
+        
+        return todo_items
+    
+    def _extract_tasks(self, response: str) -> List[dict]:
+        """从Agent响应中提取JSON"""
+        # 使用正则表达式提取JSON部分
+        json_match = re.search(r'\[.*\]', response, re.DOTALL)
+        if json_match:
+            json_str = json_match.group(0)
+            return json.loads(json_str)
+        else:
+            raise ValueError("无法从响应中提取JSON")
+```
+
+**Agent 2:任务总结专家(Task Summarizer)**
+
+**职责**:总结搜索结果,提取关键信息
+
+**设计理念**:任务总结专家的核心任务是阅读搜索结果,提取关键信息,并以结构化的方式呈现。这个过程类似于人类研究者在阅读文献后做笔记的过程。
+
+**Prompt设计**:
+
+```python
+task_summarizer_instructions = """
+你是一个任务总结专家。你的任务是总结搜索结果,提取关键信息。
+
+任务标题:{task_title}
+任务意图:{task_intent}
+搜索查询:{task_query}
 
-    state.todo_items = todo_items
-    return todo_items
+搜索结果:
+{search_results}
+
+请仔细阅读以上搜索结果,提取关键信息,并以Markdown格式返回总结。
+
+总结应该包含:
+1. **核心观点**:搜索结果中的核心观点和结论
+2. **关键数据**:重要的数字、日期、名称等
+3. **来源引用**:为每个观点添加来源引用(使用[1]、[2]等标记)
+
+请确保:
+1. 总结简洁明了,避免冗余
+2. 保留重要的细节和数据
+3. 为每个观点添加来源引用
+4. 使用Markdown格式(标题、列表、加粗等)
+
+示例输出:
+## 核心观点
+
+多模态模型是一种能够处理多种类型数据的AI模型[1]。与传统的单模态模型不同,多模态模型可以同时理解文本、图像、音频等[2]。
+
+**关键特点:**
+- 跨模态理解[1]
+- 统一表示[3]
+- 端到端训练[2]
+
+## 来源
+
+[1] https://example.com/source1
+[2] https://example.com/source2
+[3] https://example.com/source3
+"""
 ```
 
-(`backend/src/deep_researcher/app/services/planner_service.py:26`)
+**关键设计点**:提示词包含任务标题、意图、查询等上下文帮助Agent理解任务,明确要求输出包含核心观点、关键数据、来源引用,强调为每个观点添加来源引用,并通过示例帮助Agent理解期望的输出格式。
+
+**实现代码**:
 
-`_extract_tasks` 优先解析 JSON,再兼容 `[TOOL_CALL:note:{...}]` 形式,这与我们在第九章使用 `NoteTool` 的方式完全一致:模型既可以一次输出结构化数组,也可以显式调用工具创建任务笔记。规划阶段只负责生成骨架,笔记的后续更新会在每个任务线程中完成。
+```python
+class SummarizationService:
+    def __init__(self, llm: HelloAgentsLLM):
+        self._agent = ToolAwareSimpleAgent(
+            name="Task Summarizer",
+            system_prompt="你是一个任务总结专家",
+            llm=llm,
+            tool_call_listener=self._on_tool_call
+        )
+    
+    def summarize_task(
+        self,
+        task: TodoItem,
+        search_results: List[dict]
+    ) -> str:
+        # 格式化搜索结果
+        formatted_sources = self._format_sources(search_results)
+        
+        prompt = task_summarizer_instructions.format(
+            task_title=task.title,
+            task_intent=task.intent,
+            task_query=task.query,
+            search_results=formatted_sources,
+        )
+        
+        summary = self._agent.run(prompt)
+        return summary
+    
+    def _format_sources(self, search_results: List[dict]) -> str:
+        """格式化搜索结果"""
+        formatted = []
+        for idx, result in enumerate(search_results, start=1):
+            formatted.append(
+                f"[{idx}] {result['title']}\n"
+                f"URL: {result['url']}\n"
+                f"摘要: {result['snippet']}\n"
+            )
+        return "\n".join(formatted)
+```
 
-### 14.4.2 搜索执行与上下文整合
+**Agent 3:报告撰写专家(Report Writer)**
 
-`search_service.dispatch_search` 把第七章的高级搜索工具纳入统一接口。无论是内置的 DuckDuckGo/Tavily,还是我们自定义的 `advanced` 工具(复用 `code/chapter7/my_advanced_search.py`),最终都会被整理成 `{"results": [...], "notices": [...]}` 的结构。随后 `prepare_research_context` 会调用 `deduplicate_and_format_sources`,去除重复 URL 并裁剪正文长度,确保总结阶段的上下文既全面又精简。
+**职责**:整合所有子任务的总结,生成最终报告
+
+**设计理念**:报告撰写专家的核心任务是将所有子任务的总结整合成一份结构化的报告。这个过程类似于人类研究者在完成所有调研后撰写研究报告的过程。
+
+**Prompt设计**:
+
+```python
+report_writer_instructions = """
+你是一个报告撰写专家。你的任务是整合所有子任务的总结,生成一份结构化的研究报告。
+
+研究主题:{research_topic}
+
+子任务总结:
+{task_summaries}
+
+请整合以上所有子任务的总结,生成一份结构化的研究报告。
+
+报告应该包含:
+1. **标题**:研究主题
+2. **概述**:简要介绍研究主题和报告结构(2-3段)
+3. **各个子任务的详细分析**:按照逻辑顺序组织(使用二级标题)
+4. **总结**:总结研究的主要发现(1-2段)
+5. **参考文献**:所有来源引用(按照子任务分组)
+
+请确保:
+1. 报告结构清晰,逻辑连贯
+2. 消除重复的信息
+3. 保留所有来源引用
+4. 使用Markdown格式
+
+示例输出:
+# 多模态大模型的最新进展
+
+## 概述
+
+本报告系统地研究了多模态大模型的最新进展...
+
+## 1. 什么是多模态模型
+
+(此处插入子任务1的总结)
+
+## 2. 最新的多模态模型有哪些
+
+(此处插入子任务2的总结)
+
+...
+
+## 总结
+
+通过本次研究,我们了解了...
+
+## 参考文献
+
+### 任务1:什么是多模态模型
+[1] https://example.com/source1
+...
+"""
+```
+
+**关键设计点**:提示词明确要求报告包含标题、概述、详细分析、总结、参考文献等结构,强调按逻辑顺序组织内容,要求合并重复信息消除冗余,并保留所有来源引用。
+
+**实现代码**:
 
 ```python
-search_result, notices, answer_text, backend = dispatch_search(
-    task.query,
-    self.config,
-    state.research_loop_count,
+class ReportingService:
+    def __init__(self, llm: HelloAgentsLLM):
+        self._agent = ToolAwareSimpleAgent(
+            name="Report Writer",
+            system_prompt="你是一个报告撰写专家",
+            llm=llm,
+            tool_call_listener=self._on_tool_call
+        )
+    
+    def generate_report(
+        self,
+        research_topic: str,
+        task_summaries: List[Tuple[TodoItem, str]]
+    ) -> str:
+        # 格式化子任务总结
+        formatted_summaries = self._format_summaries(task_summaries)
+        
+        prompt = report_writer_instructions.format(
+            research_topic=research_topic,
+            task_summaries=formatted_summaries,
+        )
+        
+        report = self._agent.run(prompt)
+        return report
+    
+    def _format_summaries(
+        self,
+        task_summaries: List[Tuple[TodoItem, str]]
+    ) -> str:
+        """格式化子任务总结"""
+        formatted = []
+        for idx, (task, summary) in enumerate(task_summaries, start=1):
+            formatted.append(
+                f"## 任务{idx}:{task.title}\n"
+                f"意图:{task.intent}\n\n"
+                f"{summary}\n"
+            )
+        return "\n".join(formatted)
+```
+
+### 14.3.2 ToolAwareSimpleAgent的设计
+
+在第七章中,我们实现了`SimpleAgent`,它是HelloAgents框架的基础Agent。但在深度研究助手中,我们需要一个能够**记录工具调用**的Agent。这就是`ToolAwareSimpleAgent`的由来。
+
+在深度研究助手中,我们需要记录每个Agent的工具调用情况,用于:
+
+1. **调试**:查看Agent调用了哪些工具,传入了什么参数
+2. **日志**:记录研究过程中的所有操作
+3. **分析**:分析Agent的行为模式
+4. **进度展示**:实时显示Agent正在做什么
+
+`SimpleAgent`本身不支持工具调用监听,因此我们需要扩展它。
+
+`ToolAwareSimpleAgent`在`SimpleAgent`的基础上增加了一个`tool_call_listener`参数,这是一个回调函数,每次工具调用时都会被调用。
+
+**使用示例:**
+
+```python
+from hello_agents import ToolAwareSimpleAgent
+
+def tool_listener(call_info):
+    print(f"Agent: {call_info['agent_name']}")
+    print(f"工具: {call_info['tool_name']}")
+    print(f"参数: {call_info['parsed_parameters']}")
+    print(f"结果: {call_info['result']}")
+
+agent = ToolAwareSimpleAgent(
+    name="研究助手",
+    system_prompt="你是一个研究助手",
+    llm=llm,
+    tool_call_listener=tool_listener
 )
+```
+
+`ToolAwareSimpleAgent`继承自`SimpleAgent`,重写了`_execute_tool_call`方法:
+
+```python
+class ToolAwareSimpleAgent(SimpleAgent):
+    def __init__(
+        self,
+        name: str,
+        system_prompt: str,
+        llm: HelloAgentsLLM,
+        tool_registry: Optional[ToolRegistry] = None,
+        tool_call_listener: Optional[Callable] = None,
+    ):
+        super().__init__(
+            name=name,
+            system_prompt=system_prompt,
+            llm=llm,
+            tool_registry=tool_registry,
+        )
+        self._tool_call_listener = tool_call_listener
+    
+    def _execute_tool_call(self, tool_name: str, parameters: str) -> str:
+        """执行工具调用,并通知监听器"""
+        # 解析参数
+        parsed_parameters = self._parse_parameters(parameters)
+        
+        # 调用工具
+        result = super()._execute_tool_call(tool_name, parameters)
+        
+        # 通知监听器
+        if self._tool_call_listener:
+            self._tool_call_listener({
+                "agent_name": self.name,
+                "tool_name": tool_name,
+                "parsed_parameters": parsed_parameters,
+                "result": result,
+            })
+        
+        return result
+```
+
+在深度研究助手中,我们使用`ToolAwareSimpleAgent`来记录所有Agent的工具调用:
+
+```python
+class DeepResearchAgent:
+    def __init__(self, config: Configuration):
+        self.config = config
+        self.llm = HelloAgentsLLM(...)
+        
+        # 创建工具调用监听器
+        def tool_listener(call_info):
+            self._emit_event({
+                "type": "tool_call",
+                "agent": call_info["agent_name"],
+                "tool": call_info["tool_name"],
+                "parameters": call_info["parsed_parameters"],
+            })
+        
+        # 创建三个Agent,都使用相同的监听器
+        self.planner = PlanningService(self.llm, tool_listener)
+        self.summarizer = SummarizationService(self.llm, tool_listener)
+        self.reporter = ReportingService(self.llm, tool_listener)
+```
+
+这样,所有Agent的工具调用都会被记录,并通过SSE推送到前端,实时显示给用户。
+
+### 14.3.3 Agent协作模式
+
+三个Agent之间是**顺序协作**的关系,如图14.6所示。
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-6.png" alt="" width="85%"/>
+  <p>图 14.6 Agent协作流程</p>
+</div>
+
+顺序协作模式的特点是:
+
+1. **线性流程**:Agent按照固定的顺序执行
+2. **明确的输入输出**:每个Agent的输入来自上一个Agent的输出
+3. **无并发**:同一时间只有一个Agent在工作
+
+`DeepResearchAgent`是整个系统的核心协调器,负责调度三个Agent:
+
+```python
+class DeepResearchAgent:
+    def run(self, research_topic: str) -> str:
+        # 1. 规划阶段
+        self._emit_event({"type": "status", "message": "正在规划研究任务..."})
+        todo_list = self.planner.plan_todo_list(research_topic)
+        self._emit_event({"type": "tasks", "tasks": todo_list})
+        
+        # 2. 执行阶段
+        task_summaries = []
+        for task in todo_list:
+            self._emit_event({
+                "type": "status",
+                "message": f"正在研究:{task.title}"
+            })
+            
+            # 搜索
+            search_results = self.search_service.search(task.query)
+            
+            # 总结
+            summary = self.summarizer.summarize_task(task, search_results)
+            task_summaries.append((task, summary))
+            
+            self._emit_event({
+                "type": "task_completed",
+                "task_id": task.id
+            })
+        
+        # 3. 报告阶段
+        self._emit_event({"type": "status", "message": "正在生成报告..."})
+        report = self.reporter.generate_report(research_topic, task_summaries)
+        self._emit_event({"type": "report", "content": report})
+        
+        return report
+```
+
+
+
+## 14.4 工具系统集成
+
+### 14.4.1 SearchTool扩展
+
+在第七章中,我们实现了`SearchTool`的基础版本,集成了Tavily和SerpApi两个搜索引擎,展示了多源搜索的设计思想。在本章的深度研究助手中,我们进一步扩展了`SearchTool`的能力,新增了DuckDuckGo、Perplexity、SearXNG等搜索引擎,并实现了Advanced模式(组合多个搜索引擎)。搜索是深度研究助手最核心的功能,这些扩展使得系统能够适应不同的使用场景和需求。
+
+如表14.2所示,这次增加的搜索引擎有不同的特点和适用场景。
+
+<div align="center">
+  <p>表 14.2 多搜索引擎对比</p>
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-table-2.png" alt="" width="85%"/>
+</div>
+
+我们不再单独讨论如何扩展,可以参考源码以及第七章的拓展案例实现。`SearchTool`提供了统一的搜索接口,无论使用哪个搜索引擎,调用方式都是一样的。
+
+在深度研究助手中,我们通过配置文件选择搜索引擎:
+
+```python
+# config.py
+class SearchAPI(str, Enum):
+    TAVILY = "tavily"
+    DUCKDUCKGO = "duckduckgo"
+    PERPLEXITY = "perplexity"
+    SEARXNG = "searxng"
+    ADVANCED = "advanced"
+
+class Configuration(BaseModel):
+    search_api: SearchAPI = SearchAPI.DUCKDUCKGO
+    # ...
+```
+
+```python
+# .env
+SEARCH_API=tavily
+```
+
+这样,用户可以通过修改`.env`文件来选择搜索引擎,无需修改代码。
+
+`SearchTool`返回的结果是一个字典,包含:
+
+- `results`:搜索结果列表,每个结果包含标题、URL、摘要
+- `backend`:使用的搜索引擎
+- `answer`:AI生成的答案(仅Perplexity)
+- `notices`:通知信息(如API限制、错误等)
+
+以下是一些特殊情况的处理。
+
+搜索结果可能包含重复的URL,我们需要去重:
 
-sources_summary, context = prepare_research_context(
-    search_result,
-    answer_text,
-    self.config,
+```python
+def deduplicate_sources(sources: List[dict]) -> List[dict]:
+    """去除重复的URL"""
+    seen_urls = set()
+    unique_sources = []
+    
+    for source in sources:
+        if source["url"] not in seen_urls:
+            seen_urls.add(source["url"])
+            unique_sources.append(source)
+    
+    return unique_sources
+```
+
+搜索结果可能包含大量文本,我们需要限制每个来源的Token数量:
+
+```python
+def limit_source_tokens(source: dict, max_tokens: int = 2000) -> dict:
+    """限制来源的Token数量"""
+    snippet = source["snippet"]
+    
+    # 简单的Token估算:1个Token约等于4个字符
+    max_chars = max_tokens * 4
+    
+    if len(snippet) > max_chars:
+        snippet = snippet[:max_chars] + "..."
+    
+    return {
+        **source,
+        "snippet": snippet
+    }
+```
+
+### 14.4.2 NoteTool使用
+
+在深度研究助手中,我们使用`NoteTool`来持久化研究进度。`NoteTool`是第九章集成的内置工具,用于创建、读取、更新和删除笔记。
+
+在研究过程中,我们需要记录每个子任务的搜索结果、总结以及最终的研究报告。这些信息需要持久化到磁盘,以便在研究过程中断时能够从上次的进度继续,同时也方便查看研究过程中的所有操作,分析研究的质量和效率。
+
+`NoteTool`将笔记存储在指定的工作空间目录中,每个笔记是一个Markdown文件。笔记的文件名是任务ID,内容包含任务标题、任务意图、搜索查询、搜索结果和总结。
+
+最后生成的文件风格会是下面的树状图风格:
+
+```
+workspace/
+├── notes/
+│   ├── 1.md  # 任务1的笔记
+│   ├── 2.md  # 任务2的笔记
+│   ├── 3.md  # 任务3的笔记
+│   └── ...
+└── reports/
+    └── final_report.md  # 最终报告
+```
+
+在深度研究助手中,我们使用`NoteTool`来记录每个子任务的研究进度:
+
+```python
+class NotesService:
+    def __init__(self, workspace: str):
+        self.note_tool = NoteTool(workspace=workspace)
+    
+    def save_task_summary(
+        self,
+        task: TodoItem,
+        search_results: List[dict],
+        summary: str
+    ):
+        """保存任务总结"""
+        # 格式化笔记内容
+        content = self._format_note_content(
+            task=task,
+            search_results=search_results,
+            summary=summary
+        )
+        
+        # 创建笔记
+        self.note_tool.run({
+            "action": "create",
+            "title": f"任务{task.id}:{task.title}",
+            "content": content,
+            "tags": ["research", "summary"]
+        })
+    
+    def _format_note_content(
+        self,
+        task: TodoItem,
+        search_results: List[dict],
+        summary: str
+    ) -> str:
+        """格式化笔记内容"""
+        content = f"# 任务{task.id}:{task.title}\n\n"
+        content += f"## 任务信息\n\n"
+        content += f"- **意图**:{task.intent}\n"
+        content += f"- **查询**:{task.query}\n\n"
+        
+        content += f"## 搜索结果\n\n"
+        for idx, result in enumerate(search_results, start=1):
+            content += f"[{idx}] {result['title']}\n"
+            content += f"URL: {result['url']}\n"
+            content += f"摘要: {result['snippet']}\n\n"
+        
+        content += f"## 总结\n\n{summary}\n"
+        
+        return content
+```
+
+### 14.4.3 ToolRegistry工具管理
+
+`ToolRegistry`是HelloAgents框架的工具注册表,同样也是在我们的第七章所支持,用于管理所有工具的注册和调用。在深度研究助手中,我们使用`ToolRegistry`来管理`SearchTool`和`NoteTool`。
+
+在创建Agent之前,我们需要先注册工具:
+
+```python
+from hello_agents import ToolAwareSimpleAgent
+from hello_agents.tools import ToolRegistry
+from hello_agents.tools import SearchTool
+from hello_agents.tools import NoteTool
+
+# 创建工具
+search_tool = SearchTool(backend="hybrid")
+note_tool = NoteTool(workspace="./workspace/notes")
+
+# 创建注册表
+registry = ToolRegistry()
+
+# 注册工具
+registry.register_tool(search_tool)
+registry.register_tool(note_tool)
+
+# 创建Agent
+agent = ToolAwareSimpleAgent(
+    name="研究助手",
+    system_prompt="你是一个研究助手",
+    llm=llm,
+    tool_registry=registry
 )
 ```
 
-(`backend/src/deep_researcher/app/agents/deep_research_agent.py:261`)
+当Agent需要调用工具时,它会生成工具调用指令,如图14.7所示。
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-7.png" alt="" width="85%"/>
+  <p>图 14.7 工具调用流程</p>
+</div>
+
+**工具调用流程**:
+
+1. **Agent生成指令**:Agent生成工具调用指令,如`[TOOL_CALL:search_tool:{"input": "Datawhale组织", "backend": "tavily"}]`
+2. **解析指令**:`ToolRegistry`解析指令,提取工具名称和参数
+3. **查找工具**:`ToolRegistry`根据工具名称查找对应的工具
+4. **调用工具**:调用工具的`run`方法,传入参数
+5. **返回结果**:工具返回执行结果
+6. **格式化结果**:将结果格式化为字符串,返回给Agent
+
+## 14.5 服务层实现
+
+本节将详细介绍核心服务的实现,包括PlanningService、SummarizationService、ReportingService和SearchService。这些服务是连接Agent和工具的桥梁,负责具体的业务逻辑。
+
+### 14.5.1 任务规划服务
+
+`PlanningService`负责调用研究规划Agent,将研究主题分解为子任务。这是整个研究流程的第一步,也是最关键的一步。
+
+**(1)方案实现**
+
+它的核心职责是:
+
+1. **构建规划Prompt**:根据研究主题和当前日期构建Prompt
+2. **调用规划Agent**:调用TODO Planner Agent生成子任务列表
+3. **解析JSON响应**:从Agent的响应中提取JSON格式的子任务列表
+4. **验证子任务格式**:确保每个子任务包含必需的字段(title、intent、query)
+
+```python
+import re
+import json
+from typing import List, Callable, Optional
+from datetime import datetime
+
+from hello_agents import HelloAgentsLLM
+from hello_agents import ToolAwareSimpleAgent
+from models import TodoItem, SummaryState
+from prompts import todo_planner_instructions
+
+class PlanningService:
+    """任务规划服务"""
+
+    def __init__(
+        self,
+        llm: HelloAgentsLLM,
+        tool_call_listener: Optional[Callable] = None
+    ):
+        self._llm = llm
+        self._tool_call_listener = tool_call_listener
+
+        # 创建规划Agent
+        self._agent = ToolAwareSimpleAgent(
+            name="TODO Planner",
+            system_prompt="你是一个研究规划专家,擅长将复杂的研究主题分解为清晰的子任务。",
+            llm=llm,
+            tool_call_listener=tool_call_listener
+        )
+
+    def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
+        """规划TODO列表
+
+        Args:
+            state: 研究状态,包含研究主题
+
+        Returns:
+            子任务列表
+        """
+        # 构建Prompt
+        prompt = todo_planner_instructions.format(
+            current_date=self._get_current_date(),
+            research_topic=state.research_topic,
+        )
+
+        # 调用Agent
+        response = self._agent.run(prompt)
+
+        # 解析JSON
+        tasks_payload = self._extract_tasks(response)
+
+        # 验证并创建TodoItem
+        todo_items = []
+        for idx, item in enumerate(tasks_payload, start=1):
+            # 验证必需字段
+            if not all(key in item for key in ["title", "intent", "query"]):
+                raise ValueError(f"任务{idx}缺少必需字段")
+
+            task = TodoItem(
+                id=idx,
+                title=item["title"],
+                intent=item["intent"],
+                query=item["query"],
+            )
+            todo_items.append(task)
+
+        return todo_items
+
+    def _get_current_date(self) -> str:
+        """获取当前日期"""
+        return datetime.now().strftime("%Y年%m月%d日")
+
+    def _extract_tasks(self, response: str) -> List[dict]:
+        """从Agent响应中提取JSON
+
+        Agent的响应可能包含额外的文本,如:
+        "好的,我将为您规划以下任务:\n[{...}, {...}]\n这些任务涵盖了..."
+
+        我们需要提取其中的JSON部分。
+        """
+        # 方法1:使用正则表达式提取JSON数组
+        json_match = re.search(r'\[.*\]', response, re.DOTALL)
+        if json_match:
+            json_str = json_match.group(0)
+            try:
+                return json.loads(json_str)
+            except json.JSONDecodeError as e:
+                raise ValueError(f"JSON解析失败:{e}")
+
+        # 方法2:如果没有找到JSON数组,尝试直接解析整个响应
+        try:
+            return json.loads(response)
+        except json.JSONDecodeError:
+            raise ValueError("无法从响应中提取JSON")
+```
+
+**(2)JSON解析与验证**
+
+Agent返回的JSON可能包含额外的文本或格式错误,我们需要robust的解析逻辑:
+
+**常见问题**:
+
+1. **包含额外文本**:Agent可能在JSON前后添加说明文字
+2. **格式错误**:JSON可能缺少引号、逗号等
+3. **字段缺失**:某些子任务可能缺少必需字段
+
+**解决方案**:
+
+1. **使用正则表达式**:提取JSON部分
+2. **多种解析策略**:先尝试提取JSON数组,再尝试直接解析
+3. **字段验证**:确保每个子任务包含必需字段
+
+**示例**:
+
+```python
+# Agent响应示例1:包含额外文本
+response1 = """
+好的,我将为您规划以下任务:
+
+[
+  {
+    "title": "什么是多模态模型",
+    "intent": "了解基础概念",
+    "query": "multimodal model definition"
+  },
+  {
+    "title": "最新的多模态模型",
+    "intent": "了解技术现状",
+    "query": "latest multimodal models 2024"
+  }
+]
+
+这些任务涵盖了Datawhale组织的基本信息和核心项目。
+"""
+
+# 提取JSON
+tasks1 = service._extract_tasks(response1)
+# 结果:[{"title": "Datawhale的基本信息", ...}, ...]
+
+# Agent响应示例2:纯JSON
+response2 = """
+[
+  {"title": "Datawhale的基本信息", "intent": "了解组织定位", "query": "Datawhale organization introduction"},
+  {"title": "Datawhale的主要项目", "intent": "了解核心内容", "query": "Datawhale projects tutorials 2024"}
+]
+"""
+
+# 提取JSON
+tasks2 = service._extract_tasks(response2)
+# 结果:[{"title": "什么是多模态模型", ...}, ...]
+```
+
+**(3)规划质量评估**
+
+一个好的规划应该满足以下标准:
+
+1. **覆盖全面**:涵盖主题的所有重要方面
+2. **逻辑清晰**:子任务之间有明确的逻辑关系
+3. **查询精准**:搜索查询能够准确找到相关资料
+4. **数量适中**:3-5个子任务
+
+我们可以添加一个评估方法:
+
+```python
+def evaluate_plan(self, todo_items: List[TodoItem]) -> dict:
+    """评估规划质量
+
+    Returns:
+        评估结果,包含分数和建议
+    """
+    score = 100
+    suggestions = []
+
+    # 检查数量
+    if len(todo_items) < 3:
+        score -= 20
+        suggestions.append("子任务数量过少,可能遗漏重要信息")
+    elif len(todo_items) > 5:
+        score -= 10
+        suggestions.append("子任务数量过多,可能存在冗余")
+
+    # 检查查询质量
+    for task in todo_items:
+        if len(task.query.split()) < 2:
+            score -= 10
+            suggestions.append(f"任务「{task.title}」的查询过于简单")
+
+    # 检查逻辑关系
+    # (这里可以添加更复杂的逻辑检查)
+
+    return {
+        "score": score,
+        "suggestions": suggestions
+    }
+```
+
+### 14.5.2 总结服务
+
+`SummarizationService`负责调用任务总结Agent,总结搜索结果。这是研究流程的核心环节,决定了研究的质量。
+
+它的职责是:
+
+1. **格式化搜索结果**:将搜索结果格式化为易读的文本
+2. **构建总结Prompt**:根据任务信息和搜索结果构建Prompt
+3. **调用总结Agent**:调用Task Summarizer Agent生成总结
+4. **提取来源引用**:从总结中提取来源引用
+
+核心代码:
+
+```python
+from typing import List, Callable, Optional, Tuple
+
+from hello_agents import HelloAgentsLLM
+from hello_agents import ToolAwareSimpleAgent
+from models import TodoItem
+from prompts import task_summarizer_instructions
+
+class SummarizationService:
+    """总结服务"""
+
+    def __init__(
+        self,
+        llm: HelloAgentsLLM,
+        tool_call_listener: Optional[Callable] = None
+    ):
+        self._llm = llm
+        self._tool_call_listener = tool_call_listener
+
+        # 创建总结Agent
+        self._agent = ToolAwareSimpleAgent(
+            name="Task Summarizer",
+            system_prompt="你是一个任务总结专家,擅长从搜索结果中提取关键信息。",
+            llm=llm,
+            tool_call_listener=tool_call_listener
+        )
+
+    def summarize_task(
+        self,
+        task: TodoItem,
+        search_results: List[dict]
+    ) -> Tuple[str, List[str]]:
+        """总结任务
+
+        Args:
+            task: 任务信息
+            search_results: 搜索结果列表
+
+        Returns:
+            (总结文本, 来源URL列表)
+        """
+        # 格式化搜索结果
+        formatted_sources = self._format_sources(search_results)
+
+        # 构建Prompt
+        prompt = task_summarizer_instructions.format(
+            task_title=task.title,
+            task_intent=task.intent,
+            task_query=task.query,
+            search_results=formatted_sources,
+        )
+
+        # 调用Agent
+        summary = self._agent.run(prompt)
+
+        # 提取来源URL
+        source_urls = [result["url"] for result in search_results]
+
+        return summary, source_urls
+
+    def _format_sources(self, search_results: List[dict]) -> str:
+        """格式化搜索结果
+
+        将搜索结果格式化为易读的文本,包含:
+        - 序号
+        - 标题
+
+### 报告结构设计
+
+最终报告应该包含以下部分,.......
+
+## 参考文献
+
+### 任务1:什么是多模态模型
+- https://example.com/multimodal-model-definition
+....
+
+### 任务2:最新的多模态模型有哪些
+- https://example.com/gpt4v
+....
+...
+```
+
+### 14.5.3 报告生成服务
+
+`ReportingService`负责调用报告生成Agent,整合所有子任务的总结。这是研究流程的最后一步,生成最终的研究报告。
+
+它的职责是:
+
+1. **格式化子任务总结**:将所有子任务的总结格式化为统一的格式
+2. **构建报告Prompt**:根据研究主题和子任务总结构建Prompt
+3. **调用报告Agent**:调用Report Writer Agent生成最终报告
+4. **整理引用**:将所有来源引用整理到参考文献部分
+
+**核心代码实现**:
+
+```python
+from typing import List, Callable, Optional, Tuple
+
+from hello_agents import HelloAgentsLLM
+from hello_agents import ToolAwareSimpleAgent
+from models import TodoItem
+from prompts import report_writer_instructions
+
+class ReportingService:
+    """报告生成服务"""
+
+    def __init__(
+        self,
+        llm: HelloAgentsLLM,
+        tool_call_listener: Optional[Callable] = None
+    ):
+        self._llm = llm
+        self._tool_call_listener = tool_call_listener
+
+        # 创建报告Agent
+        self._agent = ToolAwareSimpleAgent(
+            name="Report Writer",
+            system_prompt="你是一个报告撰写专家,擅长整合信息并生成结构化的报告。",
+            llm=llm,
+            tool_call_listener=tool_call_listener
+        )
+
+    def generate_report(
+        self,
+        research_topic: str,
+        task_summaries: List[Tuple[TodoItem, str, List[str]]]
+    ) -> str:
+        """生成最终报告
+
+        Args:
+            research_topic: 研究主题
+            task_summaries: 子任务总结列表,每个元素是(任务, 总结, 来源URL列表)
+
+        Returns:
+            最终报告(Markdown格式)
+        """
+        # 格式化子任务总结
+        formatted_summaries = self._format_summaries(task_summaries)
+
+        # 构建Prompt
+        prompt = report_writer_instructions.format(
+            research_topic=research_topic,
+            task_summaries=formatted_summaries,
+        )
+
+        # 调用Agent
+        report = self._agent.run(prompt)
+
+        return report
+
+    def _format_summaries(
+        self,
+        task_summaries: List[Tuple[TodoItem, str, List[str]]]
+    ) -> str:
+        """格式化子任务总结
+
+        将所有子任务的总结格式化为统一的格式,包含:
+        - 任务序号
+        - 任务标题
+        - 任务意图
+        - 总结内容
+        - 来源URL
+        """
+        formatted = []
+        for idx, (task, summary, source_urls) in enumerate(task_summaries, start=1):
+            formatted.append(
+                f"## 任务{idx}:{task.title}\n\n"
+                f"**意图**:{task.intent}\n\n"
+                f"{summary}\n\n"
+                f"**来源**:\n"
+            )
+            for url in source_urls:
+                formatted.append(f"- {url}\n")
+            formatted.append("\n")
+
+        return "".join(formatted)
+```
+
+### 14.5.4 搜索调度服务
+
+`SearchService`负责调度搜索引擎,执行搜索并返回结果。这是连接Agent和SearchTool的桥梁。在这里我们没有采用往常一样的使得simpleAgent直接调用工具的形式,而是将SearchTool的执行结果通过中间层来返回给Agent,这样会使得Agent更加专注处理得到的信息。
+
+它的职责是:
+
+1. **调度搜索引擎**:根据配置选择搜索引擎
+2. **执行搜索**:调用SearchTool执行搜索
+3. **处理结果**:去重、限制Token、格式化
+4. **错误处理**:处理搜索失败的情况
+
+核心代码:
+
+```python
+from typing import List, Optional
+import logging
+
+from hello_agents.tools import SearchTool
+from config import Configuration
+
+logger = logging.getLogger(__name__)
+
+class SearchService:
+    """搜索调度服务"""
+
+    def __init__(self, config: Configuration):
+        self.config = config
+
+        # 创建SearchTool
+        self.search_tool = SearchTool(backend="hybrid")
+
+    def search(
+        self,
+        query: str,
+        max_results: int = 5
+    ) -> List[dict]:
+        """执行搜索
+
+        Args:
+            query: 搜索查询
+            max_results: 最大结果数量
+
+        Returns:
+            搜索结果列表
+        """
+        try:
+            # 调用SearchTool
+            raw_response = self.search_tool.run({
+                "input": query,
+                "backend": self.config.search_api.value,
+                "mode": "structured",
+                "max_results": max_results
+            })
+
+            # 提取结果
+            results = raw_response.get("results", [])
+
+            # 处理结果
+            results = self._deduplicate_sources(results)
+            results = self._limit_source_tokens(results)
+
+            logger.info(f"搜索成功:{query},返回{len(results)}个结果")
+
+            return results
+
+        except Exception as e:
+            logger.error(f"搜索失败:{query},错误:{e}")
+            return []
+
+    def _deduplicate_sources(self, sources: List[dict]) -> List[dict]:
+        """去除重复的URL"""
+        seen_urls = set()
+        unique_sources = []
+
+        for source in sources:
+            url = source.get("url", "")
+            if url and url not in seen_urls:
+                seen_urls.add(url)
+                unique_sources.append(source)
+
+        return unique_sources
+
+    def _limit_source_tokens(
+        self,
+        sources: List[dict],
+        max_tokens_per_source: int = 2000
+    ) -> List[dict]:
+        """限制每个来源的Token数量"""
+        limited_sources = []
+
+        for source in sources:
+            snippet = source.get("snippet", "")
+
+            # 简单的Token估算:1个Token约等于4个字符
+            max_chars = max_tokens_per_source * 4
+
+            if len(snippet) > max_chars:
+                snippet = snippet[:max_chars] + "..."
+
+            limited_sources.append({
+                **source,
+                "snippet": snippet
+            })
+
+        return limited_sources
+```
+
+根据配置选择搜索引擎,如图14.8所示:
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-8.png" alt="" width="85%"/>
+  <p>图 14.8 搜索引擎调度流程</p>
+</div>
 
-正因为我们在第七章就把搜索工具封装成可复用的注册表,现在只需切换 `SEARCH_API=advanced`,`dispatch_search` 就会自动调用自定义工具,并把降级信息通过 `notices` 推送到前端,让用户随时了解当前轮次是否触发了兜底策略。
+**调度逻辑**:
 
-### 14.4.3 并行总结与流式输出
+1. **读取配置**:从`.env`文件读取`SEARCH_API`配置
+2. **选择引擎**:根据配置选择搜索引擎(tavily、duckduckgo、perplexity等)
+3. **执行搜索**:调用SearchTool执行搜索
+4. **处理结果**:去重、限制Token、格式化
+5. **返回结果**:返回处理后的搜索结果
 
-总结阶段同样依赖 `ToolAwareSimpleAgent`,但我们重写了 `stream_run` 以支持在流式输出中执行工具:
+为了提高效率和降低成本,我们可以添加搜索结果缓存
 
 ```python
-for chunk in self.llm.stream_invoke(messages, **kwargs):
-    residual += chunk
-    for segment in process_residual():
-        if not segment:
-            continue
-        segments_this_round.append(segment)
-        final_segments.append(segment)
-        yield segment
+import hashlib
+import json
+from pathlib import Path
+
+class SearchService:
+    def __init__(self, config: Configuration):
+        self.config = config
+        self.search_tool = SearchTool(backend="hybrid")
+
+        # 缓存目录
+        self.cache_dir = Path("./cache/search")
+        self.cache_dir.mkdir(parents=True, exist_ok=True)
+
+    def search(
+        self,
+        query: str,
+        max_results: int = 5,
+        use_cache: bool = True
+    ) -> List[dict]:
+        """执行搜索(带缓存)"""
+        # 生成缓存键
+        cache_key = self._generate_cache_key(query, max_results)
+        cache_file = self.cache_dir / f"{cache_key}.json"
+
+        # 尝试从缓存读取
+        if use_cache and cache_file.exists():
+            logger.info(f"从缓存读取搜索结果:{query}")
+            with open(cache_file, "r", encoding="utf-8") as f:
+                return json.load(f)
+
+        # 执行搜索
+        results = self._execute_search(query, max_results)
+
+        # 保存到缓存
+        if use_cache and results:
+            with open(cache_file, "w", encoding="utf-8") as f:
+                json.dump(results, f, ensure_ascii=False, indent=2)
+
+        return results
+
+    def _generate_cache_key(self, query: str, max_results: int) -> str:
+        """生成缓存键"""
+        # 使用查询和最大结果数生成MD5哈希
+        content = f"{query}_{max_results}_{self.config.search_api.value}"
+        return hashlib.md5(content.encode()).hexdigest()
+```
 
-for call_text in tool_call_texts:
-    tool_calls.extend(self._parse_tool_calls(call_text))
+通过四个核心服务(PlanningService、SummarizationService、ReportingService、SearchService),我们构建了一个完整的研究流程。这些服务各司其职,通过清晰的接口协作,实现了从研究主题到最终报告的自动化流程。
+
+## 14.6 前端交互设计
+
+在前面的章节中,我们实现了完整的后端系统。本节将详细介绍前端交互设计,包括全屏模态对话框UI、实时进度展示和研究结果可视化。
+
+### 14.6.1 全屏模态对话框UI设计
+
+深度研究助手采用全屏模态对话框的UI设计,这种设计有以下优势:
+
+1. **沉浸式体验**:全屏显示,避免干扰,专注于研究
+2. **清晰的层次**:主页面和研究页面分离,层次清晰
+3. **易于关闭**:点击关闭按钮或按ESC键即可返回主页面
+4. **响应式设计**:适配不同屏幕尺寸
+
+如图14.9所示,全屏模态对话框包含以下部分:
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-9.png" alt="" width="85%"/>
+  <p>图 14.9 全屏模态对话框UI</p>
+</div>
+
+**UI组件**:
+
+1. **顶部栏**:包含研究主题和关闭按钮
+2. **进度区域**:显示当前研究进度(规划、执行、报告)
+3. **内容区域**:显示研究结果(Markdown格式)
+4. **底部栏**:显示状态信息(如"研究中..."、"已完成")
+
+对应的Vue实现如下所示(ResearchModal.vue):
+
+```vue
+<template>
+  <div v-if="isOpen" class="modal-overlay" @click.self="close">
+    <div class="modal-container">
+      <!-- 顶部栏 -->
+      <div class="modal-header">
+        <h2>{{ researchTopic }}</h2>
+        <button @click="close" class="close-button">
+          <svg><!-- 关闭图标 --></svg>
+        </button>
+      </div>
+      
+      <!-- 进度区域 -->
+      <div class="progress-section">
+        <div class="progress-bar">
+          <div 
+            class="progress-fill" 
+            :style="{ width: progressPercentage + '%' }"
+          ></div>
+        </div>
+        <div class="progress-text">{{ progressText }}</div>
+      </div>
+      
+      <!-- 内容区域 -->
+      <div class="content-section">
+        <div v-if="isLoading" class="loading-spinner">
+          <div class="spinner"></div>
+          <p>研究中,请稍候...</p>
+        </div>
+        
+        <div v-else class="markdown-content" v-html="renderedMarkdown"></div>
+      </div>
+      
+      <!-- 底部栏 -->
+      <div class="modal-footer">
+        <span class="status-text">{{ statusText }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { marked } from 'marked'
+
+interface Props {
+  isOpen: boolean
+  researchTopic: string
+}
 
-if tool_calls:
-    messages.append({"role": "assistant", "content": clean_response})
-    tool_results_text = "\n\n".join(self._execute_tool_call(...))
-    messages.append({"role": "user", "content": "工具执行结果:\n..."})
+const props = defineProps<Props>()
+const emit = defineEmits<{
+  close: []
+}>()
+
+// 状态
+const isLoading = ref(true)
+const progressPercentage = ref(0)
+const progressText = ref('准备中...')
+const statusText = ref('研究中...')
+const markdownContent = ref('')
+
+// 渲染Markdown
+const renderedMarkdown = computed(() => {
+  return marked(markdownContent.value)
+})
+
+// 关闭模态框
+const close = () => {
+  emit('close')
+}
+
+// 监听ESC键
+const handleKeydown = (e: KeyboardEvent) => {
+  if (e.key === 'Escape') {
+    close()
+  }
+}
+
+// 挂载时添加键盘监听
+watch(() => props.isOpen, (isOpen) => {
+  if (isOpen) {
+    document.addEventListener('keydown', handleKeydown)
+  } else {
+    document.removeEventListener('keydown', handleKeydown)
+  }
+})
+</script>
+
+<style scoped>
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+}
+......
+</style>
+```
+
+为了适配不同屏幕尺寸,我们添加媒体查询:
+
+```css
+/* 平板设备 */
+@media (max-width: 768px) {
+  .modal-container {
+    width: 95vw;
+    height: 95vh;
+  }
+  
+  .modal-header,
+  .progress-section,
+  .content-section,
+  .modal-footer {
+    padding: 15px 20px;
+  }
+}
+
+/* 手机设备 */
+@media (max-width: 480px) {
+  .modal-container {
+    width: 100vw;
+    height: 100vh;
+    border-radius: 0;
+  }
+  
+  .modal-header h2 {
+    font-size: 18px;
+  }
+}
 ```
 
-(`backend/src/deep_researcher/app/agents/tool_aware_agent.py:243`)
+### 14.6.2 实时进度展示
+
+深度研究助手使用SSE实现实时进度展示。SSE是一种服务器推送技术,允许服务器主动向客户端发送数据,在协议章节也有所讲解。
+
+如图14.10所示,SSE流程包括以下步骤:
+
+<div align="center">
+  <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/14-figures/14-10.png" alt="" width="85%"/>
+  <p>图 14.10 SSE流程</p>
+</div>
+
+**流程说明**:
 
-`process_residual` 会截断尚未完整输出的 `[TOOL_CALL:...]` 片段,确保前端永远不会看到残缺的工具指令;一旦工具执行完成,新的上下文会立即写回消息栈,继续下一轮对话。配合 `SummarizationService.stream_task_summary`,我们可以在前端实时看到任务总结的增量内容,并且在总结结束后自动拿到最终文本。
+1. **客户端发起请求**:发送POST请求到`/api/research`,包含研究主题
+2. **服务器建立SSE连接**:返回`text/event-stream`响应
+3. **服务器推送进度**:定期推送研究进度(规划、执行、报告)
+4. **客户端接收进度**:监听SSE事件,更新UI
+5. **研究完成**:服务器推送最终报告,关闭连接
 
-### 14.4.4 笔记同步与报告沉淀
+如果想把SSE用于前后端的项目中还需要做如下配置。
 
-`ToolCallTracker` 记录所有笔记相关事件,并在任务线程之间同步 `note_id`。当所有任务完成后,`_persist_final_report` 会把最终 Markdown 写入结论笔记:若已有同名笔记则更新,否则创建新笔记并返回 `report_note` 事件。
+**后端FastAPI SSE端点**:
 
 ```python
-payload = {
-    "type": "report_note",
-    "note_id": note_id,
-    "title": note_title,
-    "content": content,
-    "note_path": str(note_path),
+from fastapi import FastAPI
+from fastapi.responses import StreamingResponse
+from typing import AsyncGenerator
+import asyncio
+import json
+
+app = FastAPI()
+
+async def research_stream(topic: str) -> AsyncGenerator[str, None]:
+    """研究流式生成器
+    
+    生成SSE格式的数据:
+    data: {"type": "progress", "data": {...}}
+    
+    """
+    try:
+        # 1. 规划阶段
+        yield f"data: {json.dumps({'type': 'progress', 'stage': 'planning', 'percentage': 10, 'text': '正在规划研究任务...'})}\n\n"
+        
+        # 调用PlanningService
+        todo_items = await planning_service.plan_todo_list(topic)
+        
+        yield f"data: {json.dumps({'type': 'plan', 'data': [item.dict() for item in todo_items]})}\n\n"
+        
+        # 2. 执行阶段
+        task_summaries = []
+        for idx, task in enumerate(todo_items, start=1):
+            # 更新进度
+            percentage = 10 + (idx / len(todo_items)) * 70
+            yield f"data: {json.dumps({'type': 'progress', 'stage': 'executing', 'percentage': percentage, 'text': f'正在研究任务{idx}/{len(todo_items)}:{task.title}'})}\n\n"
+            
+            # 搜索
+            search_results = await search_service.search(task.query)
+            
+            # 总结
+            summary, source_urls = await summarization_service.summarize_task(task, search_results)
+            
+            task_summaries.append((task, summary, source_urls))
+            
+            # 推送任务总结
+            yield f"data: {json.dumps({'type': 'task_summary', 'task_id': task.id, 'summary': summary})}\n\n"
+        
+        # 3. 报告阶段
+        yield f"data: {json.dumps({'type': 'progress', 'stage': 'reporting', 'percentage': 90, 'text': '正在生成最终报告...'})}\n\n"
+        
+        # 生成报告
+        report = await reporting_service.generate_report(topic, task_summaries)
+        
+        # 推送最终报告
+        yield f"data: {json.dumps({'type': 'report', 'data': report})}\n\n"
+        
+        # 完成
+        yield f"data: {json.dumps({'type': 'progress', 'stage': 'completed', 'percentage': 100, 'text': '研究完成!'})}\n\n"
+        
+    except Exception as e:
+        # 错误处理
+        yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
+
+@app.post("/api/research")
+async def research(request: ResearchRequest):
+    """研究端点(SSE)"""
+    return StreamingResponse(
+        research_stream(request.topic),
+        media_type="text/event-stream",
+        headers={
+            "Cache-Control": "no-cache",
+            "Connection": "keep-alive",
+        }
+    )
+```
+
+**前端使用EventSource接收SSE**:
+
+```typescript
+// composables/useResearch.ts
+import { ref } from 'vue'
+
+export function useResearch() {
+  const isLoading = ref(false)
+  const progressPercentage = ref(0)
+  const progressText = ref('')
+  const markdownContent = ref('')
+  const error = ref<string | null>(null)
+  
+  const startResearch = (topic: string) => {
+    isLoading.value = true
+    error.value = null
+    
+    // 创建EventSource
+    const eventSource = new EventSource(`/api/research?topic=${encodeURIComponent(topic)}`)
+    
+    // 监听消息
+    eventSource.onmessage = (event) => {
+      const data = JSON.parse(event.data)
+      
+      switch (data.type) {
+        case 'progress':
+          progressPercentage.value = data.percentage
+          progressText.value = data.text
+          break
+          
+        case 'plan':
+          // 显示规划结果
+          console.log('规划结果:', data.data)
+          break
+          
+        case 'task_summary':
+          // 追加任务总结到Markdown
+          markdownContent.value += `\n\n## 任务${data.task_id}\n\n${data.summary}`
+          break
+          
+        case 'report':
+          // 显示最终报告
+          markdownContent.value = data.data
+          break
+          
+        case 'error':
+          error.value = data.message
+          eventSource.close()
+          isLoading.value = false
+          break
+          
+        case 'completed':
+          eventSource.close()
+          isLoading.value = false
+          break
+      }
+    }
+    
+    // 错误处理
+    eventSource.onerror = (err) => {
+      console.error('SSE错误:', err)
+      error.value = '连接失败,请重试'
+      eventSource.close()
+      isLoading.value = false
+    }
+  }
+  
+  return {
+    isLoading,
+    progressPercentage,
+    progressText,
+    markdownContent,
+    error,
+    startResearch,
+  }
 }
+```
 
-yield {
-    "type": "final_report",
-    "report": report,
-    "note_id": state.report_note_id,
-    "note_path": state.report_note_path,
+**在组件中使用**:
+
+```vue
+<script setup lang="ts">
+import { useResearch } from '@/composables/useResearch'
+
+const { 
+  isLoading, 
+  progressPercentage, 
+  progressText, 
+  markdownContent, 
+  error,
+  startResearch 
+} = useResearch()
+
+const handleStartResearch = (topic: string) => {
+  startResearch(topic)
 }
+</script>
+```
+
+### 14.6.3 研究结果可视化
+
+研究结果以Markdown格式展示,包含标题、段落、列表、引用等元素。我们使用`marked`库将Markdown转换为HTML,并添加自定义样式。
+
+**渲染Markdown**:
+
+```typescript
+import { marked } from 'marked'
+
+// 配置marked
+marked.setOptions({
+  breaks: true,  // 支持换行
+  gfm: true,     // 支持GitHub Flavored Markdown
+})
+
+// 渲染
+const renderedHtml = marked(markdownContent.value)
 ```
 
-(`backend/src/deep_researcher/app/agents/deep_research_agent.py:248`)
+研究报告中包含大量来源引用,我们需要特殊处理:
+
+```markdown
+## 参考文献
+
+### 任务1:Datawhale的基本信息
+- [Datawhale GitHub](https://github.com/datawhalechina)
+- [Datawhale 官网](https://datawhale.club)
+
+### 任务2:Datawhale的主要项目
+- [Hello-Agents 教程](https://github.com/datawhalechina/Hello-Agents)
+......
+```
+
+通过全屏模态对话框UI、SSE实时进度展示和Markdown结果可视化,我们构建了一个用户友好的前端界面。用户可以清晰地看到研究进度,并以美观的格式查看研究结果。
+
+## 14.7 本章小结
+
+在本章中,我们从零开始构建了一个完整的自动化深度研究智能体系统。让我们回顾一下核心要点:
+
+**(1)TODO驱动的研究范式**
+
+我们提出了一种新的研究范式——TODO驱动的研究。这种范式将复杂的研究主题分解为可执行的子任务,通过三个阶段完成研究:
+
+- **规划阶段**:将研究主题分解为3-5个子任务,每个子任务包含标题、意图和搜索查询
+- **执行阶段**:对每个子任务执行搜索和总结,生成结构化的知识
+- **报告阶段**:整合所有子任务的总结,生成最终的研究报告
+
+这种范式的优势在于:
+
+1. **可控性强**:每个子任务都有明确的目标和范围
+2. **质量可靠**:通过专门的Agent保证每个环节的质量
+3. **易于调试**:可以单独调试每个子任务
+4. **可扩展性好**:可以轻松添加新的子任务或修改现有子任务
+
+**(2)三Agent协作系统**
+
+我们设计了三个专门的Agent,各司其职:
+
+- **TODO Planner(研究规划专家)**:负责将研究主题分解为子任务
+- **Task Summarizer(任务总结专家)**:负责总结每个子任务的搜索结果
+- **Report Writer(报告撰写专家)**:负责整合所有子任务的总结,生成最终报告
+
+这种设计的优势在于:
+
+1. **职责清晰**:每个Agent专注于一个特定的任务
+2. **Prompt优化**:可以为每个Agent定制专门的Prompt
+3. **易于维护**:修改一个Agent不会影响其他Agent
+4. **质量保证**:每个Agent都是该领域的"专家"
+
+**(3)ToolAwareSimpleAgent的设计**
+
+我们扩展了HelloAgents框架的`SimpleAgent`,实现了`ToolAwareSimpleAgent`。这个Agent具有工具调用监听能力,可以:
+
+- **监听工具调用**:通过回调函数监听每次工具调用
+- **实时反馈**:将工具调用信息实时推送给前端
+- **调试支持**:记录所有工具调用,便于调试
+
+这个Agent已经集成到HelloAgents框架中,可以在其他项目中复用。
+
+**(4)工具系统集成**
+
+我们充分利用了HelloAgents框架的工具系统:
+
+- **SearchTool**:扩展支持更多种搜索引擎(Tavily、DuckDuckGo、Perplexity等)
+- **NoteTool**:持久化研究进度,支持恢复和审计
+- **ToolRegistry**:统一管理所有工具,支持自定义扩展
+
+通过配置化的设计,用户可以轻松切换搜索引擎,无需修改代码。
+
+**(5)核心服务实现**
+
+我们实现了四个核心服务,连接Agent和工具:
+
+- **PlanningService**:调用规划Agent,解析JSON,验证格式
+- **SummarizationService**:调用总结Agent,处理搜索结果,提取来源
+- **ReportingService**:调用报告Agent,整合总结,生成报告
+- **SearchService**:调度搜索引擎,处理结果,错误降级,结果缓存
+
+这些服务各司其职,通过清晰的接口协作,实现了从研究主题到最终报告的自动化流程。
+
+**(6)前端交互设计**
+
+我们设计了用户友好的前端界面:
+
+- **全屏模态对话框**:沉浸式体验,清晰的层次
+- **SSE实时进度**:实时展示研究进度,用户体验良好
+- **Markdown可视化**:美观的格式,清晰的结构
+
+通过Vue 3 + TypeScript + SSE的技术栈,我们实现了一个现代化的Web应用。
+
 
-这意味着前端可以在报告区域直接显示笔记路径,甚至提供“一键打开 Markdown” 的快捷入口;而后端的数据结构中也会记录 `report_note_id`,便于二次加工或测试校验。
 
-### 14.4.5 SSE 事件协议升级
+这些知识不仅适用于深度研究助手,也可以应用到其他AI应用中。希望读者能够在本章的基础上,探索更多的可能性,构建出更强大的AI系统。
 
-随着并行化与笔记沉淀的引入,SSE 事件类型也进行了扩展:
+在下一章中,我们将构建一个与游戏引擎结合的多Agent系统——赛博小镇,探索Agent之间的复杂交互和协作模式。敬请期待!
 
-- `todo_list`:包含任务清单以及每个任务的 `stream_token`。
-- `task_status` / `task_summary_chunk` / `sources`:逐任务输出状态、总结增量与来源摘要。
-- `tool_call`:复用第九章的事件格式,记录模型执行 `note` 等工具的详情。
-- `report_note`:最终报告落盘后的通知,附带 `note_id`、`note_path` 与正文内容。
-- `final_report` / `done`:宣告流程结束,并把最终 Markdown 与笔记信息一起返回。
 
-前端 `runResearchStream` 只需在事件分发器中新增对 `report_note` 的处理,即可在 UI 中同步展示最新笔记。由于每条事件都包含 `task_id` 与 `stream_token`,即使多个任务并行输出,也能稳定位于正确的面板。
 
-掌握以上模块,对于定制 Prompt、扩展企业内部检索源或构建新的可视化界面都有直接帮助。下一节我们将讨论可能的扩展方向与实践建议。

BIN
docs/images/14-figures/14-1.png


BIN
docs/images/14-figures/14-10.png


BIN
docs/images/14-figures/14-2.png


BIN
docs/images/14-figures/14-3.png


BIN
docs/images/14-figures/14-4.png


BIN
docs/images/14-figures/14-5.png


BIN
docs/images/14-figures/14-6.png


BIN
docs/images/14-figures/14-7.png


BIN
docs/images/14-figures/14-8.png


BIN
docs/images/14-figures/14-9.png


BIN
docs/images/14-figures/14-table-1.png


BIN
docs/images/14-figures/14-table-2.png


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio