从零搭建个人科研Agent:混合模型策略与LangGraph核心闭环实战
项目摘要: ScholarCraft 是一个基于本地与云端混合模型的科研调研Agent,旨在通过自主规划、工具调用和论文阅读生成结构化报告。项目采用 LangGraph + Ollama(Qwen3-4B) + 小米MiMoAPI 技术栈,已完成核心闭环(任务规划→工具调用→联网检索→报告生成),并集成 MCP协议 和 迭代式检索机制。 核心能力: 任务规划:通过MiMoAPI拆解模糊需求为结构化
项目名称:ScholarCraft
定位:一个本地与远端混合模型驱动的科研调研Agent,能自主规划、调用工具、阅读论文并生成结构化报告。
技术栈:LangGraph + Ollama (Qwen3-4B) + 小米 MiMo API
进度:环境搭建、混合模型选型、核心闭环(任务规划→工具调用→联网检索→报告生成)、MCP协议集成、迭代式检索机制
前言
继之前的企业级RAG系统和大模型微调项目之后,这次我把目光投向了2026年的新风口——AI Agent。
网上的Agent教程虽然越来越多,但大部分还是教你如何调用一个API,或者用现成框架跑个Demo。真正从零开始,把任务规划、工具调用、状态管理、记忆机制这些核心模块都自己写一遍,并且把每一步的决策逻辑讲清楚的项目,其实并不多。
我决定自己动手。这个项目我命名为ScholarCraft,目标是构建一个能辅助科研调研的Agent。本文是系列的第一篇,主要记录基础环境搭建、模型选型策略、核心闭环的实现过程,以及后续迭代中引入的联网检索、MCP协议集成和迭代式检索机制。我的开发环境是一台Windows笔记本,配置为RTX 3060 Laptop(6GB显存)加上WSL2。
一、项目总览
1.1 项目定位与能力矩阵
ScholarCraft的核心目标是展示一个Agent工程师的五大核心能力:
| 能力维度 | 当前状态 | 实现方式 |
|---|---|---|
| 任务规划 | ✅ 已完成 | MiMo API 将模糊需求拆解为结构化步骤 |
| 工具调用 | ✅ 已完成 | 本地检索 + arXiv联网检索 + 论文详情读取 + 报告保存 |
| 联网检索 | ✅ 已完成 | arXiv API,按日期降序,支持自动重试 |
| MCP协议集成 | ✅ 已完成 | 四个工具全部封装为标准MCP Server |
| 迭代式检索 | ✅ 已完成 | 搜索结果不足时自动换词重搜 |
| 记忆管理 | ⬜ 规划中 | 短期记忆(滑动窗口+摘要压缩)+ 长期记忆(SQLite+ChromaDB) |
| 上下文工程 | ⬜ 规划中 | Token预检 + 工具返回筛选 + 摘要兜底 |
| 安全与观测 | ⬜ 规划中 | 熔断机制 + 权限模型 + 全链路Trace日志 |
| 评测体系 | ⬜ 规划中 | 15个分级测试用例 + 自动化评测脚本 |
1.2 项目文件架构
text
scholar-craft/
├── app.py # CLI入口,接收用户输入并调用graph
├── requirements.txt # 项目依赖(langgraph, ollama, httpx, chromadb等)
├── .env # API Key(MiMo),已加入.gitignore
├── .gitignore
│
├── data/
│ └── papers.json # 本地论文数据库(结构化JSON,含标题/摘要/方法/指标等)
│
├── reports/ # 生成的调研报告(Markdown)
│ └── report_*.md
│
├── docs/
│ └── design.md # 设计决策记录(模型选型、架构权衡等)
│
├── evaluation/
│ ├── test_cases.json # 15个分级测试用例(简单/多步/模糊/无结果/极长指令)
│ └── run_eval.py # 自动化评测脚本
│
└── src/
├── __init__.py
│
├── graph/ # LangGraph 编排引擎
│ ├── __init__.py
│ ├── state.py # AgentState 定义(8个核心字段)
│ ├── nodes.py # 6个业务节点 + 2个路由函数
│ └── builder.py # StateGraph 组装(节点注册、条件边、循环逻辑)
│
├── tools/ # 工具层(已全部封装为MCP Server)
│ ├── __init__.py
│ ├── search_local.py # 本地论文关键词检索
│ ├── search_arxiv.py # arXiv 联网检索(httpx + XML解析)
│ ├── read_paper.py # 论文详情读取
│ ├── save_report.py # 报告保存为Markdown
│ ├── permission.py # 工具权限装饰器(read/write/network)
│ ├── circuit_breaker.py # 熔断器(连续失败3次暂停执行)
│ ├── mcp_server.py # MCP Server(将4个工具注册为标准接口)
│ └── test_mcp.py # MCP Server 工具发现与调用测试
│
├── models/ # 模型层
│ ├── __init__.py
│ ├── ollama_client.py # 本地 Qwen3-4B 调用封装
│ └── xiaomi_client.py # MiMo API 调用封装(OpenAI兼容)
│
├── memory/ # 记忆层(规划中)
│ ├── __init__.py
│ ├── short_term.py # 短期记忆(SqliteSaver + 滑动窗口)
│ ├── long_term.py # 长期记忆(SQLite + ChromaDB 语义检索)
│ └── compressor.py # 摘要压缩
│
├── context/ # 上下文工程(规划中)
│ ├── __init__.py
│ └── manager.py # ContextManager(Token计数、结果筛选、摘要兜底)
│
├── planner/ # 规划层
│ ├── __init__.py
│ └── planner.py # MiMo API Prompt模板
│
├── observability/ # 观测层(规划中)
│ ├── __init__.py
│ └── tracer.py # 全链路Trace日志
│
└── api/ # FastAPI接口(规划中)
├── __init__.py
├── main.py
└── routes/
1.3 执行流程全景图
text
用户输入: "调研分子生成领域扩散模型的最新进展"
│
▼
┌──────────────────────────────────────────────────────┐
│ LangGraph 编排引擎 │
│ │
│ [plan_node] │
│ MiMo API 拆解任务 → 生成4-5个步骤(JSON) │
│ "搜索本地 → 搜索arXiv → 读取论文 → 生成报告" │
│ │ │
│ ▼ │
│ ┌───[ 执行循环 ]────────────────────────────────┐ │
│ │ │ │
│ │ [tool_select_node] │ │
│ │ 本地Qwen3-4B: 根据当前步骤选择工具并生成参数 │ │
│ │ 输出: {"query": "...", "max_results": 5} │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [tool_call_node] │ │
│ │ 通过 MCP 协议调用工具 │ │
│ │ → search_local_papers / search_arxiv / ... │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [evaluate_node] │ │
│ │ 检查结果数量和质量 │ │
│ │ ├── 有效论文 ≥ 3 → 前进到下一步 │ │
│ │ └── 不足 + 重试次数 < 2 → 回到tool_select │ │
│ │ (生成新搜索词,再搜一轮) │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ [report_node] │
│ 汇总所有tool_results → 本地LLM整合为Markdown报告 │
│ │ │
│ ▼ │
│ [save_node] → 写入 reports/ 目录 │
│ │
└──────────────────────────────────────────────────────┘
二、技术选型与架构设计
2.1 为什么选 LangGraph 而不是 LangChain?
这是面试中的高频问题。LangChain是"工具包",提供了模型调用、文档加载、Prompt模板等组件,擅长构建A→B→C的线性链。但Agent的执行流程充满了分支、循环和动态决策——比如"搜完发现结果不够,需要换个关键词再搜"——这种非线性流程需要的是状态机和图框架,而不是传送带。
LangGraph的三个核心概念——节点、边、状态——完美匹配Agent的需求。节点是执行单元,边定义流程路径(包括条件分支),State是集中式的数据中枢。所有状态更新都是显式且可追溯的,出问题时能立刻定位到具体步骤。这个选择背后的逻辑,在面试中往往比技术本身更被看重。
2.2 混合模型策略
| 模型 | 部署方式 | 用途 | 选择理由 |
|---|---|---|---|
| Qwen3-4B-Instruct (4-bit) | Ollama 本地 | 工具选择、步骤评估、报告整合 | 低延迟、离线可控、隐私保护 |
| MiMo-V2.5-Pro | 小米API | 任务规划(初始拆解) | 推理能力强,稳定输出JSON |
| nomic-embed-text | Ollama 本地 | 长期记忆语义检索(后续使用) | 本地运行,保持离线闭环 |
权衡逻辑:工具选择和评估在ReAct循环里被反复调用,需要毫秒级响应,所以放在本地;而任务拆解每次任务只调用一次,但对推理能力要求高,用远端API更划算。这种"本地优先、远端补强"的策略,同时平衡了成本、延迟和隐私。
2.3 MCP协议集成
MCP(模型上下文协议)正在成为AI Agent工具调用的行业标准。它解决的问题是:不同Agent框架、不同工具之间的接口不统一,每次新增工具都要改框架代码。
在ScholarCraft中,我把四个论文工具封装为MCP Server:
text
tool_call_node → MCP Client → MCP Server(注册了所有工具) → 工具函数
核心价值:工具层和编排层彻底解耦。新增工具只需在MCP Server注册,nodes.py不用动。面试时能直接展示对行业标准的关注和实践。
三、基础环境准备
我的硬件环境是Windows 11系统,搭载NVIDIA GeForce RTX 3060 Laptop显卡(6GB显存),通过WSL2来运行Linux环境,Python版本选择的是3.11。
踩坑提示:强烈建议使用Python 3.10或3.11。AI生态圈对最新Python版本的兼容往往有延迟,追求过新的版本(比如3.12或3.13)容易在编译依赖时遇到令人头疼的报错。
创建Conda环境:
bash
conda create -n scholarcraft python=3.11 -y
conda activate scholarcraft
Ollama安装与模型拉取:
bash
ollama pull qwen3:4b-instruct
ollama pull nomic-embed-text
Qwen3-4B用了4-bit量化版本,大小约2.5GB,显存占用大约4GB,我的6GB卡跑起来绰绰有余。nomic-embed-text只有274MB,主要用于后续长期记忆的语义检索。
MiMo API完全兼容OpenAI接口格式,验证完毕后把API Key移到.env文件里保护起来。
四、核心闭环:从模糊需求到结构化报告
4.1 工具系统
最初我实现了三个基础工具:search_local_papers基于关键词检索本地论文库,read_paper_detail根据论文ID返回完整信息,save_report将最终报告保存为Markdown文件。
种子数据库papers.json包含3篇结构化论文,覆盖扩散模型、等变扩散和LoRA方向。每篇论文包含标题、作者、年份、关键词、摘要、方法、数据集、关键指标和对比基线。
4.2 状态机与节点设计
AgentState包含八个关键字段:user_query存放用户原始需求,plan存放任务规划生成的步骤列表,current_step_index标记当前执行进度,tool_results记录每个步骤的工具返回结果,messages用于消息历史,final_report存放最终报告,terminated标记任务是否结束,errors收集执行错误。
六个核心节点:plan_node调用MiMo API拆解任务,tool_select_node让本地LLM选择工具并生成参数,tool_call_node执行工具函数,evaluate_node评估结果并决定下一步,report_node汇总数据生成报告,save_node写入文件。
在tool_select_node中我做了一个重要优化:将LLM生成的参数存入current_params字段,避免下游节点重复解析JSON可能出错。
4.3 遇到的坑与解决方案
坑1:ollama.chat()返回值无法解析。现象是tool_select_node中报错"the JSON object must be str",原因是ollama.chat()返回的是ChatResponse对象而非纯字符串。解决方法是封装ollama_client.py,在内部提取message.content并返回字符串。
坑2:tool_call_node无法获取read_paper的paper_id参数。原因是参数依赖messages列表传递,不同节点间格式未对齐。解决方法是新增current_params字段,由tool_select_node直接写入、tool_call_node直接读取。
4.4 联网检索与迭代式检索
新增search_arxiv工具,调用arXiv API获取最新预印本,按日期降序排列确保时效性。更重要的是,引入了迭代式检索机制——让Agent在搜索结果不足时自动调整策略。
核心改动在evaluate_node:搜索步骤完成后检查有效论文数量。如果少于3篇且重试次数未满2次,回到tool_select_node让LLM根据上轮结果生成新的搜索词,再搜一轮。直到结果足够多或重试次数耗尽。
升级后的日志清晰展示了这一过程:
text
[EVAL] 只找到 1 篇有效论文,触发补充搜索(第1次重试)
[SELECT] 补充搜索模式,生成新查询词...
[CALL] 使用 arXiv 联网检索: 'diffusion model for molecule generation drug design 2025'
[EVAL] 找到 4 篇有效论文,搜索完成,继续下一步
4.5 端到端测试
运行python app.py "帮我调研分子生成领域扩散模型的最新进展",Agent完整执行:本地搜索→arXiv联网检索→读取论文详情→生成报告。
最终报告整合了本地论文库和arXiv的最新工作,所有数据可溯源到实际检索结果。对arXiv预印本诚实标注"指标未提供",不编造不存在的性能数据。方法对比表展示了本地综述论文与arXiv最新工作的整合分析,所有arXiv论文标题旁都标注了[arXiv],区分了数据源的权威性等级。
五、总结与下一步
本文记录了ScholarCraft从零开始的核心旅程:P0环境基建和模型验证、P1工具系统和状态机设计、P1.5联网检索和迭代式检索机制、P2 MCP协议集成。至此,Agent已具备任务规划、多步工具调用(本地+联网)、搜索质量反馈与自动重试、MCP标准化工具接口、基于真实数据的诚实报告生成等能力。
LangGraph的状态机模式让调试变得高效——出问题时能立刻定位到具体节点和状态。迭代式检索让Agent在搜索结果不够好时学会换词再搜。MCP集成让工具层和编排层彻底解耦,为后续扩展奠定了架构基础。
在下一篇文章中,我将继续迭代优化:引入LLM驱动的智能评估,让Agent判断搜索结果的真实相关性;加强流程控制,实现显式条件边和熔断机制;优化上下文管理,在多轮搜索中计算Token消耗并动态压缩历史信息。
完整的项目代码会同步更新到GitHub,欢迎关注和交流。
本文是ScholarCraft系列的第一篇,后续文章将陆续更新。如果有任何工程落地上的问题或建议,欢迎在评论区讨论。
更多推荐



所有评论(0)