前言

最近 AI Agent 这个概念火得不行,各种框架(LangChain、CrewAI、AutoGen)层出不穷。但我发现很多新手一上来就啃框架源码,结果被各种抽象层绕晕了。

其实 Agent 的核心原理非常简单——就是一个 while 循环。你完全可以从零手写一个,不超过 200 行代码。

这篇文章带你从零实现一个本地知识库 AI Agent,它能自动搜索、阅读、整理你的 Markdown 笔记。全程 Python,调用 DeepSeek API,没有任何框架依赖。

🔗 完整代码已开源:GitHub - Hao-max1/knowledge-agent · GitHub


一、什么是 Agent?

1.1 普通 LLM vs Agent

普通 ChatGPT 是一问一答:

你: "1+1等于几?"
AI: "2"
→ 结束

Agent 是多轮思考:

你: "帮我找关于 Python 的笔记"
AI: [思考] 用户想找笔记 → 我先搜一下
    → 调用 search_notes("Python")         ← 第一次 API 调用
    → 结果: python-basics.md
​
AI: [再思考] 搜到了,但用户想要内容 → 读一下
    → 调用 read_note("python-basics.md")  ← 第二次 API 调用
    → 结果: 文件内容是...
​
AI: [最终] 拿到了,可以回答了
    → "你的笔记里讲了 Python 基础,包括..."  ← 最终回复

Agent = LLM + 工具 + 循环。就这么简单。

1.2 为什么需要循环?

因为 LLM 不知道工具的执行结果。它只能说"我想用工具 X",然后停下来等你执行。你把结果喂回去,它才能继续思考。


二、项目结构

knowledge-agent/
├── main.py              # CLI 入口
├── agent.py             # ★ 核心:Agent 循环
├── tools.py             # 工具定义 + 执行
├── config.py            # 配置加载
├── requirements.txt     # 仅 2 个依赖
├── .env                 # API Key
└── knowledge_base/      # 你的笔记目录

技术选型

项目 选择 理由
语言 Python 3.9+ 生态最好
LLM API DeepSeek(deepseek-chat) 便宜、支持 tool calling、OpenAI 兼容
SDK openai 标准 SDK,DeepSeek 完全兼容
存储 本地 Markdown 文件 零依赖,即开即用

依赖

openai>=1.0.0
python-dotenv>=1.0.0

仅两个依赖,极简。


三、核心代码:Agent 循环

这是整个项目的心脏,agent.py 里的核心逻辑:

"""核心 Agent 循环 — 思考 → 行动 → 观察 → 再思考"""
​
import json
from openai import OpenAI
from config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, MODEL, MAX_TOOL_ROUNDS
from tools import TOOL_SCHEMAS, execute_tool
​
client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)
​
SYSTEM_PROMPT = """你是一个知识库助手 Agent,帮助用户管理和检索笔记。
​
## 你的能力
- search_notes:搜索包含关键词的笔记
- list_notes:列出知识库中所有笔记
- read_note:读取一篇笔记的完整内容
- write_note:创建或更新一篇 Markdown 笔记
​
## 工作方式
1. 理解用户需求
2. 先搜索,再阅读 —— 不要猜测文件内容
3. 基于实际内容回答,不要编造
4. 用中文回复"""
​
​
def run_agent(user_query: str, on_tool_call=None) -> str:
    """执行一次 Agent 对话。"""
    # messages 就是 Agent 的"记忆"
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_query},
    ]
​
    # ★ Agent 循环
    for round_num in range(1, MAX_TOOL_ROUNDS + 1):
        # ① 调用 LLM
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOL_SCHEMAS,
            temperature=0.1,
        )
​
        choice = response.choices[0]
        msg = choice.message
​
        # ② 判断:LLM 说完了还是想调工具?
        if choice.finish_reason == "stop":
            return msg.content or "(未返回回复)"
​
        elif choice.finish_reason == "tool_calls":
            # ③ 把 LLM 的回复加入记忆
            messages.append(msg.to_dict())
​
            # ④ 执行每个工具调用
            for tc in msg.tool_calls:
                tool_name = tc.function.name
                tool_input = json.loads(tc.function.arguments)
​
                if on_tool_call:
                    on_tool_call(tool_name, tool_input)
​
                result = execute_tool(tool_name, tool_input)
​
                # ⑤ 把工具结果加入记忆
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": result,
                })
​
            # ⑥ 循环继续!LLM 看到结果后会再次思考
​
    return "[WARNING] Agent 达到最大思考轮次。"

流程图

用户输入
  │
  ▼
┌─────────────────────┐
│  LLM 思考            │ ← chat.completions.create()
│  (带上全部历史消息)    │
└──────┬──────────────┘
       │
       ├─ finish_reason="stop" ──→ 返回回复给用户
       │
       └─ finish_reason="tool_calls"
                │
                ▼
        ┌──────────────┐
        │  执行工具      │ ← execute_tool()
        └──────┬───────┘
               │
               ▼
        ┌──────────────┐
        │  把结果加入    │ ← messages.append()
        │  messages     │
        └──────┬───────┘
               │
               └──→ 回到开头,继续循环

四、工具定义

工具是 Agent 的"手"。用 JSON Schema 定义,LLM 通过 description 字段理解每个工具该什么时候用。

# tools.py — 以 search_notes 为例
​
TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "search_notes",
            "description": (
                "在知识库中搜索包含指定关键词的笔记文件。"
                "当用户问「有没有关于XX的笔记」「帮我找XX」时使用此工具。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词",
                    },
                },
                "required": ["query"],
            },
        },
    },
    # ... list_notes, read_note, write_note
]

关键点:description 非常非常重要! 它决定了 LLM 能不能在正确的时机调用工具。写得太笼统(如"用来搜索"),LLM 可能不知道该什么时候用。

工具执行是纯 Python 文件操作:

def _search_notes(query: str) -> str:
    """遍历知识库,搜索包含关键词的文件。"""
    query_lower = query.lower()
    results = []
​
    for root, dirs, files in os.walk(KNOWLEDGE_BASE):
        for fname in files:
            if not fname.endswith((".md", ".txt")):
                continue
            content = Path(root, fname).read_text(encoding="utf-8")
            if query_lower in content.lower():
                # 提取匹配行作为摘要
                matching_lines = [
                    line.strip() for line in content.split("\n")
                    if query_lower in line.lower()
                ]
                results.append(f"[文件] {fname}\n  {matching_lines[:5]}")
​
    return f"找到 {len(results)} 篇:\n" + "\n\n".join(results) if results \
        else f"未找到「{query}」相关笔记。"

五、安全设计

5.1 路径逃逸防护

LLM 是不可控的,万一它尝试 ../../etc/passwd 呢?

def _safe_path(relative_path: str) -> Path:
    """确保 LLM 无法访问知识库外的路径。"""
    full = (KNOWLEDGE_BASE / relative_path).resolve()
    if not str(full).startswith(str(KNOWLEDGE_BASE.resolve())):
        raise ValueError(f"不允许访问知识库外的路径: {relative_path}")
    return full

5.2 防死循环

MAX_TOOL_ROUNDS = 10,Agent 最多调用 10 轮工具后强制中断。


六、Anthropic vs DeepSeek 对比

我最初用的是 Claude API,后来换成 DeepSeek。Agent 循环逻辑完全没变,只是 API 格式不同:

项目 Anthropic (Claude) DeepSeek
SDK import anthropic from openai import OpenAI
调用方法 client.messages.create() client.chat.completions.create()
工具格式 name + description + input_schema type:"function" + function:{...}
结束标志 stop_reason == "end_turn" finish_reason == "stop"
工具调用标志 stop_reason == "tool_use" finish_reason == "tool_calls"
工具参数 block.input(直接是 dict) json.loads(tc.function.arguments)
结果回传 role:"user" + type:"tool_result" role:"tool"
System Prompt 独立参数 system=... messages[0], role:"system"

核心收获

Agent 的灵魂是你写的循环逻辑,不是某个具体的 API。 换一家 LLM 只需改 API 调用格式,循环不动。


七、效果演示

$ python main.py

╔══════════════════════════════════════════╗
║        知识库 AI Agent                  ║
╠══════════════════════════════════════════╣
║  模型: deepseek-chat                   ║
║  知识库: .../knowledge_base             ║
║  工具: search_notes, list_notes, ...    ║
╚══════════════════════════════════════════╝

> 帮我找关于 Python 的笔记
  Thinking...
  >> Tool: search_notes(query=Python)     ← Agent 自动搜索
  >> Tool: read_note(file_path=py.md)     ← Agent 自动读取

你的知识库中有一篇关于 Python 的笔记,内容摘要如下:
- Python 是一门解释型语言
- 语法简洁,适合初学者
- 广泛应用于数据科学、Web 开发、自动化脚本等领域

> 帮我写一篇 Git 常用命令的笔记
  Thinking...
  >> Tool: write_note(file_path=git.md, content=...)  ← Agent 自动创建

[OK] 笔记已保存: git-cheatsheet.md(856 字符)

八、如何扩展?

学完基础后,你可以:

  1. 加工具:网页搜索、天气查询、发邮件——在 tools.py 里加定义 + 实现即可

  2. 加记忆:把对话历史存到 SQLite,下次启动恢复,实现跨会话记忆

  3. 加流式输出stream=True,让 LLM 回复逐字显示

  4. 换 MCP 协议:把工具标准化为 MCP Server,其他 Agent 也能用

  5. 做 Web UI:用 Streamlit 或 Gradio 包一层网页界面

  6. 加上 RAG:引入向量数据库,实现语义搜索


九、总结

AI Agent 开发不需要从啃框架开始。核心就三样东西:

Agent = LLM + 工具 + while 循环

理解了这三样,什么 LangChain/CrewAI/AutoGen,不过是在这个基础上加了更多抽象。

建议的学习路径:

  1. 先用起来:用 Claude Code、Cursor 等体验 Agent 能做什么

  2. 手写一个:像本文一样,50-200 行代码写一个最小 Agent

  3. 加复杂度:多工具、记忆、流式输出

  4. 回头看框架:这时候你才能真正理解框架的价值

纸上得来终觉浅,绝知此事要躬行。直接动手吧。


完整代码

项目地址:https://github.com/Hao-max1/knowledge-agent

直接把代码拷到本地,改 .env 里的 API Key 就能跑:

git clone https://github.com/Hao-max1/knowledge-agent.git
cd knowledge-agent
cp .env.example .env
# 编辑 .env 填入你的 DeepSeek API Key
pip install -r requirements.txt
python main.py

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。有问题可以在评论区留言交流。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐