实习20-DeepResearch项目 deep_researcher.py分块讲解 + 逐行语法注释版这份文档是给“基础还不太扎实,但想认真读懂源码”的读者准备的。你现在希望这份文档同时满足两件事:按块讲清楚“这一段代码整体在做什么”在每个代码块里,再尽量逐行解释语法所以这份文档采用统一结构:先给出一个代码块先讲这段代码的整体作用再讲这段代码里每一行或每几行对应的语法说明一下:这份源码接近 1000 行如果真的把 946 行每一行都写成“单独一条注释”,文档会非常长,也不太好读所以这里采用“按逻辑块拆开,但块内尽量逐行解释语法”的方式对应源码文件:deep_researcher.py1. 先建立全局感觉先别急着扎进细节。这份文件本质上不是一个普通的“从上到下执行完就结束”的 Python 脚本,而是一张 LangGraph 状态图。它的大流程是:用户提问 - clarify_with_user - write_research_brief - research_supervisor - final_report_generation - END其中:clarify_with_user:先判断要不要追问用户write_research_brief:把问题改写成研究任务research_supervisor:总控拆任务、调度 researcherfinal_report_generation:根据研究结果生成最终报告你后面看到的所有函数,基本都是在为这个主流程服务。2. 文件头、导入、全局模型代码"""Main LangGraph implementation for the Deep Research agent."""importasyncioimportjsonimportosfromdatetimeimportdatetimefromtypingimportLiteralfromlangchain.chat_modelsimportinit_chat_modelfromlangchain_core.messagesimport(AIMessage,HumanMessage,SystemMessage,ToolMessage,filter_messages,get_buffer_string,)fromlangchain_core.runnablesimportRunnableConfigfromlanggraph.graphimportEND,START,StateGraphfromlanggraph.typesimportCommandfromopen_deep_research.configurationimport(Configuration,)fromopen_deep_research.promptsimport(clarify_with_user_instructions,compress_research_simple_human_message,compress_research_system_prompt,final_report_generation_prompt,lead_researcher_prompt,research_system_prompt,transform_messages_into_research_topic_prompt,)fromopen_deep_research.stateimport(AgentInputState,AgentState,ClarifyWithUser,ConductResearch,ResearchComplete,ResearcherOutputState,ResearcherState,ResearchQuestion,SupervisorState,)fromopen_deep_research.utilsimport(anthropic_websearch_called,get_all_tools,get_api_key_for_model,get_base_url_for_model,get_model_token_limit,get_notes_from_tool_calls,get_today_str,is_token_limit_exceeded,invoke_json_model,invoke_text_model,openai_websearch_called,remove_up_to_last_ai_message,think_tool,)configurable_model=init_chat_model(configurable_fields=("model","max_tokens","api_key","base_url","streaming"),)这块代码整体在做什么这一块是在“搭地基”。它主要做两件事:导入后面要用到的标准库、LangChain / LangGraph 组件、项目内部模块创建一个全局可配置模型configurable_model这个configurable_model很重要。它不是固定某一个模型,而是一个“模型壳子”。后面的每个节点都会基于它再注入:modelmax_tokensapi_keybase_urlstreaming逐行语法解释"""Main LangGraph implementation for the Deep Research agent."""三引号字符串放在文件最前面时,一般会被当成模块说明文字,也就是模块 docstringimportasyncioimportjsonimportosimport xxx是最基础的导入语法这里导入的是 Python 标准库asyncio后面用于异步并发json后面用于保存 JSON 文件os后面用于路径和目录操作fromdatetimeimportdatetimefromtypingimportLiteralfrom ... import ...表示从模块里只拿出指定名字datetime后面用于生成时间戳Literal用于类型提示,表示某个值只能是固定几个字符串之一fromlangchain.chat_modelsimportinit_chat_model导入函数init_chat_model后面会用它初始化一个可配置的大模型对象fromlangchain_core.messagesimport(AIMessage,HumanMessage,SystemMessage,ToolMessage,filter_messages,get_buffer_string,)这是多行导入写法外层圆括号让导入列表可以分多行写,更清楚AIMessage / HumanMessage / SystemMessage / ToolMessage都是消息对象类型filter_messages是消息筛选工具get_buffer_string是把消息列表拼成字符串的工具fromlangchain_core.runnablesimportRunnableConfigfromlanggraph.graphimportEND,START,StateGraphfromlanggraph.typesimportCommandRunnableConfig是运行配置类型START/END是图的起点和终点常量StateGraph是状态图类Command是 LangGraph 里非常重要的对象,表示“下一步去哪 + 更新什么状态”fromopen_deep_research.configurationimport(Configuration,)导入项目自己的配置类Configuration后面很多函数都会从config里解析出这个对象fromopen_deep_research.promptsimport(clarify_with_user_instructions,compress_research_simple_human_message,compress_research_system_prompt,final_report_generation_prompt,lead_researcher_prompt,research_system_prompt,transform_messages_into_research_topic_prompt,)导入项目里的 Prompt 模板它们大多数本质上是字符串模板,后面会用.format(...)往里面填内容fromopen_deep_research.stateimport(AgentInputState,AgentState,ClarifyWithUser,ConductResearch,ResearchComplete,ResearcherOutputState,ResearcherState,ResearchQuestion,SupervisorState,)导入项目自己的状态结构和 schemaAgentState/SupervisorState/ResearcherState主要是运行时状态ClarifyWithUser/ConductResearch/ResearchComplete/ResearchQuestion主要和结构化输出、工具调用有关fromopen_deep_research.utilsimport(anthropic_websearch_called,get_all_tools,get_api_key_for_model,get_base_url_for_model,get_model_token_limit,get_notes_from_tool_calls,get_today_str,is_token_limit_exceeded,invoke_json_model,invoke_text_model,openai_websearch_called,remove_up_to_last_ai_message,think_tool,)导入各种辅助函数这些函数后面会在多个节点里复用configurable_model=init_chat_model(configurable_fields=("model","max_tokens","api_key","base_url","streaming"),)configurable_model是变量名=是赋值init_chat_model(...)是函数调用configurable_fields=(...)传入的是一个元组 tuple这个元组定义了后面可以动态配置的字段3.serialize_message代码defserialize_message(message)-dict:"""Convert a LangChain message-like object into JSON-safe data."""ifhasattr(message,"model_dump"):try:returnmessage.model_dump()exceptException:passreturn{"type":getattr(message,"type",type(message).__name__),"content":str(getattr(message,"content",message)),"name":getattr(message,"name",None),"tool_calls":getattr(message,"tool_calls",None),"additional_kwargs":getattr(message,"additional_kwargs",None),"response_metadata":getattr(message,"response_metadata",None),}这块代码整体在做什么这个函数负责把 LangChain 的消息对象转换成普通字典,方便后面写入 JSON。如果对象本身支持model_dump(),就优先调用它;如果不行,再手动拼字典。逐行语法解释defserialize_message(message)-dict:def是定义函数的关键字serialize_message是函数名message是参数名- dict是返回值类型提示,表示这个函数应该返回字典"""Convert a LangChain message-like object into JSON-safe data."""这是函数 docstring,用来说明这个函数的用途ifhasattr(message,"model_dump"):if是条件判断hasattr(obj, "attr")用来判断对象有没有某个属性或方法try:returnmessage.model_dump()exceptException:passtry表示尝试执行可能报错的代码return message.model_dump()表示调用方法并直接返回结果except Exception表示如果报异常就进入这里pass表示这里不做任何处理,继续往下走return{"type":getattr(message,"type",type(message).__name__),"content":str(getattr(message,"content",message)),"name":getattr(message,"name",None),"tool_calls":getattr(message,"tool_calls",None),"additional_kwargs":getattr(message,"additional_kwargs",None),"response_metadata":getattr(message,"response_metadata",None),}return { ... }表示返回一个字典 literalgetattr(obj, "属性名", 默认值)是安全取属性的写法type(message).__name__表示取对象的类名字符串str(...)表示强制转字符串4.summarize_text代码defsummarize_text(text:str,max_len:int=500)-str:"""Create a compact summary string for trace payloads."""iftextisNone:return""text=str(text)iflen(text)=max_len:returntextreturntext[:max_len]+"...truncated"这块代码整体在做什么这个函数负责把长文本裁短,主要给 trace 日志用,避免日志内容太大。逐行语法解释defsummarize_text(text:str,max_len:int=500)-str:text: str表示参数text期望是字符串max_len: int = 500表示参数max_len是整数,并且默认值是 500- str表示返回值是字符串iftextisNone:return""is None是判断空值的标准写法如果输入是空,就返回空字符串text=str(text)把输入强制转为字符串=是重新赋值iflen(text)=max_len:returntextlen(text)计算字符串长度=表示“小于等于”returntext[:max_len]+"...truncated"text[:max_len]是切片语法,表示取前max_len个字符+是字符串拼接5.make_trace_event代码defmake_trace_event(event_type:str,**kwargs)-dict:"""Create a timestamped trace event."""return{"timestamp":datetime.now().isoformat(),"event_type":event_type,**kwargs,}这块代码整体在做什么这个函数负责统一生成 trace 事件,也就是运行轨迹日志。后面每次写入run_trace时,基本都会通过它来构造一条事件。逐行语法解释defmake_trace_event(event_type:str,**kwargs)-dict:event_type: str表示第一个参数应该是字符串**kwargs表示这个函数还能接收任意数量的额外命名参数- dict表示返回字典return{"timestamp":datetime.now().isoformat(),"event_type":event_type,**kwargs,}datetime.now()取当前时间.isoformat()把时间转成标准字符串**kwargs在字典里表示“把这些额外键值对展开进去”例如:make_trace_event("researcher_started",research_topic="paper")会得到近似这样的结果:{"timestamp":"...","event_type":"researcher_started","research_topic":"paper",}6.detect_output_language代码defdetect_output_language(messages)-str:"""Infer the user's preferred output language from message content."""combined="\n".join(str(getattr(message,"content",""))formessageinmessages)ifany("\u4e00"=ch="\u9fff"forchincombined):return"Chinese"ifany("\u3040"=ch="\u30ff"forchincombined):return"Japanese"ifany("\uac00"=ch="\ud7af"forchincombined):return"Korean"return"English"这块代码整体在做什么这个函数根据用户消息内容,推断最终输出应该用什么语言。你前面遇到“搜论文时为什么输出成韩语”的问题,后面修复逻辑就依赖这个函数。逐行语法解释defdetect_output_language(messages)-str:定义函数messages没有写明确类型提示,但从上下文可以知道它通常是消息列表- str表示返回字符串combined="\n".join(str(getattr(message,"content",""))formessageinmessages)combined是变量名"\n".join(...)表示把多个字符串用换行符拼起来for message in messages是生成式写法getattr(message, "content", "")表示安全读取消息内容str(...)把内容转成字符串ifany("\u4e00"=ch="\u9fff"forchincombined):return"Chinese"any(...)表示只要里面有一个条件为真,就返回Truefor ch in combined表示逐字符遍历字符串"\u4e00" = ch = "\u9fff"是判断字符是否在中文 Unicode 范围内后面两段日文、韩文判断语法完全一样,只是 Unicode 范围不同。return"English"如果前面都没命中,就默认返回英文7.save_run_artifact代码defsave_run_artifact(state:AgentState,final_report:str,config:RunnableConfig)-None:"""Persist a completed QA/research run to an out directory as JSON."""out_dir=os.path.join(os.getcwd(),"out")os.makedirs(out_dir,exist_ok=True)messages=state.get("messages",[])supervisor_messages=state.get("supervisor_messages",[])user_messages=[str(message.content)formessageinmessagesifgetattr(message,"type","")=="human"]configurable=config.get("configurable",{})payload={"saved_at":datetime.now().isoformat(),"thread_id":configurable.get("thread_id"),"config":{"search_api":configurable.get("search_api"),"research_model":configurable.get("research_model"),"summarization_model":configurable