【Java/Go后端手撸原生Agent(第十一篇):RAG检索——给Agent加上“查资料“能力】

系列目录:Phase 1-2 基础框架Phase 3 状态机+文件读取Phase 4-6 原生FC+Judge校验Phase 5 多工具并行+BashToolPhase 6 流式输出Phase 7 Token预算与滑动窗口Phase 8 摘要压缩Phase 9 规划模式Phase 10 Web UIPhase 11 RAG检索(本文)

上一篇链接:【Java/Go后端手撸原生Agent(第十篇):HTTP+SSE+Web UI------让Agent长出真正的用户界面】

项目最终代码仓库

前言

前10篇做完,我们的Agent已经具备:原生Function Calling、流式输出、多工具并行、BashTool、LLM-as-Judge证据链校验、Token预算管理、滑动窗口+摘要压缩、Plan-and-Execute规划、事件生成器+SSE+Web UI。在浏览器里它能正确调工具、流式展示思考、长对话不爆token、回答不全会被Judge打回、复杂任务先做计划。

但有一个核心问题一直没解决:Agent只能基于LLM自己的参数记忆回答问题

问它"XXX公司的报销流程是什么"、"这个项目的部署架构是什么"------LLM训练语料里没这些信息,它要么硬编(hallucination),要么干巴巴地说"不知道"。

这就是RAG(Retrieval-Augmented Generation,检索增强生成)要解决的问题:给Agent外挂一个知识库,遇到知识盲区时主动查资料,而不是靠LLM"编"

但本文要做的不是市面上教程常见的"老式RAG"(用户问什么→检索→拼prompt→LLM答),而是 Agentic RAG :把检索封装成一个工具 knowledge_search,让Agent自己判断要不要查、查几次、查到的内容够不够。这是从"流水线RAG"到"智能体RAG"的关键升级。

本文涉及6个新文件,并改动 main.py 3处。Agent核心 run_agent_stream() 一行不改------这正是Phase 10事件生成器解耦的收益。

一、问题本质:老式RAG的三大缺陷

市面上的RAG教程基本都是这个流水线:

复制代码
用户问题 → embedding检索 top-k → 拼到 prompt → LLM 生成答案

看起来简单直接,但生产环境有三个致命问题:

缺陷1:简单问题也强制检索。用户问"你好"、"谢谢",流水线照样去检索,浪费一次embedding调用 + 浪费top-k个token塞进prompt。

缺陷2:无法多轮检索。用户问"TRAE有哪些模型?分别什么价位?"------一次检索很难同时召回"模型列表"和"价位表"两份文档。流水线RAG只能拼一次检索结果,回答必然残缺。

缺陷3:检索结果不可控。LLM拿到一堆chunk,没有"够不够"的判断机制。chunk不相关也得硬用,chunk够了也不知道停。

第一性原理 :检索应该是Agent的**"动作",而不是"前置流程"**。Agent有自己的判断力,它知道什么时候需要查资料、什么时候自己就能答、查完一份不够还能再查一份。把检索做成工具,让Agent像人一样"遇到盲区就翻文档,翻够了就回答"------这才是RAG的正确姿势。

这就是 Agentic RAG 的核心思想:RAG = 检索工具 + Agent的自主决策

二、整体架构:从文件读取到语义检索

回顾Phase 3,我们做过一个 read_file 工具------Agent能按路径读文件。但这有三个局限:必须知道文件路径、只能整篇读、无法跨文档语义检索。

本文的升级路径:

维度 Phase 3 read_file Phase 11 knowledge_search
检索方式 按路径定位 按语义相似度
粒度 整篇文件 切分后的chunk
跨文档 不支持 支持
决策方 用户告诉Agent路径 Agent自己判断要不要查

整体架构:

复制代码
文档(raw)
  → Chunker 切分
  → Embedder 向量化
  → VectorStore 入库(持久化)

用户问题
  → Agent 决策(调用 knowledge_search?)
    → Embedder 向量化 query
    → VectorStore 粗排召回 top-k*5
    → Reranker 精排取 top-k
    → 格式化返回给 LLM

新增目录结构:

复制代码
native-agent-demo/
├── rag/                          # 新增:RAG 模块
│   ├── __init__.py
│   ├── chunker.py                # 文档切分
│   ├── embeddings.py             # 向量化
│   ├── vector_store.py           # 手撸向量库
│   ├── reranker.py               # 重排
│   └── ingest.py                 # 入库脚本
├── tools/
│   └── knowledge_search.py       # 新增工具(继承 BaseTool)
├── docs/                         # 新增:原始文档
├── data/                         # 新增:向量库持久化
└── main.py                       # 改 3 处

下面按数据流逐个拆解。

三、文档切分:Chunker

3.1 为什么要切分

第一性原理:为什么不把整篇文档直接向量化?

  • LLM上下文有限:一篇10万字文档塞不进prompt
  • 中间遗忘问题:即使塞得进,长上下文有"lost in the middle"现象,LLM对中间内容注意力下降
  • 检索粒度:整篇文档向量化会"稀释"语义------一篇讲5个主题的文档,向量是5个主题的混合,任何单主题query都匹配不准

所以必须切分。但切分又引入新问题:切多粗?切多细?

  • 太粗:一个chunk包含多个主题,召回时噪声大
  • 太细:一个chunk只有半句话,语义碎片化,LLM看不懂
3.2 切分策略权衡

三种主流策略:

  1. 固定字符窗口:简单粗暴,但会切断句子
  2. 按句子/段落:语义完整,但长度不可控
  3. Parent-Child:检索小块、返回所属大块(生产推荐)

本文用**"按段落 + 字符窗口 + overlap"**的混合策略,平衡简单与效果:

python 复制代码
# rag/chunker.py
from dataclasses import dataclass
from typing import List


@dataclass
class Chunk:
    text: str
    metadata: dict  # source, chunk_id, char_len


class Chunker:
    def __init__(self, chunk_size: int = 500, overlap: int = 50, separator: str = "\n\n"):
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.separator = separator

    def split(self, text: str, source: str = "unknown") -> List[Chunk]:
        # 1. 先按段落粗切
        paragraphs = [p.strip() for p in text.split(self.separator) if p.strip()]

        # 2. 段落内按 chunk_size 滑窗切,保留 overlap
        chunks: List[Chunk] = []
        chunk_id = 0
        for para in paragraphs:
            if len(para) <= self.chunk_size:
                chunks.append(Chunk(
                    text=para,
                    metadata={"source": source, "chunk_id": chunk_id, "char_len": len(para)},
                ))
                chunk_id += 1
                continue

            start = 0
            while start < len(para):
                end = start + self.chunk_size
                piece = para[start:end]
                chunks.append(Chunk(
                    text=piece,
                    metadata={"source": source, "chunk_id": chunk_id, "char_len": len(piece)},
                ))
                chunk_id += 1
                start = end - self.overlap  # overlap 保证跨块语义连续

        return chunks

overlap 的作用:相邻chunk有50字符的重叠区,避免"关键信息正好被切断"的悲剧。

⚠️ 注意:overlap 不是越大越好。500字符chunk配50 overlap是10%,标准做法10%~20%。一开始图保险设了overlap=200,结果相邻chunk高度重复,检索top-k全是同一段的几个变体,浪费召回名额。

四、向量化:Embedder

python 复制代码
# rag/embeddings.py
import numpy as np
from openai import OpenAI


class Embedder:
    """
    为什么不手撸 embedding?
    - embedding 模型是训练出来的(对比学习),自己训练成本极高且无意义
    - 这是"用模型"不是"做模型",但向量检索本身可以手撸(见 vector_store.py)
    """
    def __init__(self, model: str = "text-embedding-3-small", dim: int = 1536):
        self.client = OpenAI()
        self.model = model
        self.dim = dim

    def embed(self, texts: list[str]) -> np.ndarray:
        """批量向量化,返回 [N, dim] 的 numpy 数组"""
        all_vecs = []
        batch_size = 512  # OpenAI 单次最多 2048 条,512 比较稳
        for i in range(0, len(texts), batch_size):
            batch = texts[i:i + batch_size]
            resp = self.client.embeddings.create(input=batch, model=self.model)
            all_vecs.extend([d.embedding for d in resp.data])

        vecs = np.array(all_vecs, dtype=np.float32)
        # 关键:L2 归一化,后续余弦相似度退化为点积,省一次开方
        norms = np.linalg.norm(vecs, axis=1, keepdims=True)
        norms[norms == 0] = 1.0
        return vecs / norms

    def embed_query(self, query: str) -> np.ndarray:
        """单条查询向量化,返回 [dim] 向量"""
        return self.embed([query])[0]

⚠️ 注意:归一化一定要在入库前做,且query也要归一化。如果写忘了归一化query,分数永远是0.x附近,排序还勉强对,但分数不可解释。归一化后余弦相似度=点积,分数在-1, 1之间,可解释性强。

选型建议:

  • 中文为主:BAAI/bge-large-zh-v1.5(开源,可本地部署)
  • 英文为主:text-embedding-3-small(OpenAI,便宜效果好)
  • 隐私敏感:本地部署 BGE / m3e

五、手撸向量库:VectorStore

这是本文的硬核部分------不依赖任何向量数据库,用numpy手撸一个

5.1 第一性原理:向量检索在做什么

给定query向量q,找数据库中与q最相似的k个向量。"相似" = 余弦相似度 = 归一化后点积。

暴力法:算q和所有N个向量的点积,排序取top-k。复杂度O(N×D)。

python 复制代码
# rag/vector_store.py
import os
import pickle
import numpy as np
from rag.chunker import Chunk


class VectorStore:
    """
    手撸向量库:numpy + 暴力余弦相似度。

    为什么生产环境要换 ANN(近似最近邻)?
    - 暴力法 O(N×D),10万条 1536 维要 1.5 亿次乘法,几十毫秒
    - 100万条就到秒级,不可接受
    - HNSW / IVF 把复杂度降到 O(log N)

    但学习阶段强烈建议先用暴力法------你能 100% 看懂检索过程,没有黑盒。
    """
    def __init__(self, dim: int):
        self.dim = dim
        self.vectors = np.zeros((0, dim), dtype=np.float32)  # [N, dim]
        self.chunks: list[Chunk] = []

    def add(self, chunks: list[Chunk], vectors: np.ndarray):
        """入库:chunk 和它的向量必须一一对应"""
        assert len(chunks) == vectors.shape[0]
        assert vectors.shape[1] == self.dim
        self.vectors = np.vstack([self.vectors, vectors])
        self.chunks.extend(chunks)

    def search(self, query_vec: np.ndarray, top_k: int = 5) -> list[tuple[Chunk, float]]:
        """检索:返回 [(chunk, score)] 列表,score 越高越相似"""
        if len(self.chunks) == 0:
            return []

        # query_vec 也归一化,余弦相似度 = 点积
        q = query_vec / (np.linalg.norm(query_vec) + 1e-8)

        # 暴力点积:[N] 个分数
        scores = self.vectors @ q  # [N]

        # top-k 索引
        k = min(top_k, len(self.chunks))
        # argpartition O(N) 比 argsort O(N log N) 快,先粗排取前 k
        top_idx = np.argpartition(-scores, k - 1)[:k]
        # 再在这 k 个里精排
        top_idx = top_idx[np.argsort(-scores[top_idx])]

        return [(self.chunks[i], float(scores[i])) for i in top_idx]

    def save(self, path: str):
        """持久化:vectors 用 npy,chunks 用 pickle。path 不带后缀。"""
        os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
        np.save(path + ".npy", self.vectors)
        with open(path + ".pkl", "wb") as f:
            pickle.dump(self.chunks, f)

    def load(self, path: str):
        self.vectors = np.load(path + ".npy")
        with open(path + ".pkl", "rb") as f:
            self.chunks = pickle.load(f)
        self.dim = self.vectors.shape[1]
5.2 两个关键优化点

优化1:argpartition 替代 argsortargsort 是O(N log N)全排序,但我们只需要top-k,不需要知道第k+1名是谁。argpartition是O(N),先粗排找到前k个(这k个内部无序),再对这k个做argsort。N=10万、k=5时,这个优化能快几倍。

优化2:归一化前置 。入库时一次性归一化,后续每次检索只需做点积,不用重复算分母。余弦相似度公式 a ⋅ b ∣ a ∣ ∣ b ∣ \frac{a \cdot b}{|a||b|} ∣a∣∣b∣a⋅b 在归一化后简化为 a ⋅ b a \cdot b a⋅b,省掉一次开方。

想深入理解HNSW等ANN算法的,可以看我的前置文章 从跳表到HNSW:深度拆解向量ANN检索的分层设计与近似本质。理解暴力法是理解ANN的前提。

六、重排:Reranker

6.1 为什么embedding检索后还要 rerank

embedding是双塔模型:query和doc独立编码,相似度靠余弦。

  • 优点:doc可以预计算,检索快
  • 缺点:query和doc没有交互,细节匹配差。比如query问"价格",doc里写"售价500元",双塔模型可能匹配不上

Cross-encoder(reranker)[query, doc] 拼起来过一次transformer,让两者在注意力层充分交互。精度高但慢,所以用在召回后的小集合上------从100个粗排候选里精挑5个

这就是两阶段检索:粗排(embedding)+ 精排(reranker)。和搜索引擎的"召回+排序"是一回事。

6.2 两种实现
python 复制代码
# rag/reranker.py
from typing import List, Tuple
from rag.chunker import Chunk


class Reranker:
    """
    本实现提供两种:
    1. LLM Reranker:用 LLM 打分(学习用,能跑就行)
    2. 真实 Reranker:bge-reranker-v2-m3(生产推荐)
    """
    def __init__(self, mode: str = "llm"):
        self.mode = mode
        if mode == "bge":
            # pip install FlagEmbedding
            from FlagEmbedding import FlagReranker
            self.model = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)

    def rerank(self, query: str, candidates: List[Tuple[Chunk, float]],
               top_k: int = 3) -> List[Tuple[Chunk, float]]:
        if not candidates:
            return []

        if self.mode == "bge":
            pairs = [[query, c.text] for c, _ in candidates]
            scores = self.model.compute_score(pairs, normalize=True)
            if isinstance(scores, float):
                scores = [scores]
            ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
            return [(c, float(s)) for (c, _), s in ranked[:top_k]]

        # LLM reranker:让 LLM 给 0-10 分
        from openai import OpenAI
        client = OpenAI()
        scored = []
        for chunk, _vec_score in candidates:
            prompt = f"""请给以下文档片段对用户问题的相关性打分(0-10 整数)。

用户问题:{query}

文档片段:{chunk.text[:500]}

只输出一个数字,不要任何其他内容。"""
            resp = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": prompt}],
                temperature=0,
            )
            try:
                score = int(resp.choices[0].message.content.strip())
            except ValueError:
                score = 0
            scored.append((chunk, float(score)))

        scored.sort(key=lambda x: -x[1])
        return scored[:top_k]

⚠️ 注意:LLM reranker慢且贵,每个候选要一次LLM调用。top-k=3、粗排召回15个候选,就是15次LLM调用,一轮检索几秒钟。生产一定要换bge-reranker,本地CPU都能跑,几十毫秒一个候选。学习阶段用LLM reranker是为了零依赖跑通,理解"两阶段检索"的本质。

七、入库脚本:ingest.py

python 复制代码
# rag/ingest.py
import sys
from pathlib import Path
from rag.chunker import Chunker
from rag.embeddings import Embedder
from rag.vector_store import VectorStore


def ingest(docs_dir: str, store_path: str):
    """扫描 docs_dir 下所有 .txt/.md 文件,入库到 store_path(不带后缀)"""
    embedder = Embedder()
    chunker = Chunker(chunk_size=500, overlap=50)
    store = VectorStore(dim=embedder.dim)

    docs_path = Path(docs_dir)
    files = list(docs_path.rglob("*.txt")) + list(docs_path.rglob("*.md"))
    if not files:
        print(f"未找到文档: {docs_dir}")
        return

    print(f"发现 {len(files)} 个文档,开始切块...")
    all_chunks = []
    for f in files:
        text = f.read_text(encoding="utf-8")
        chunks = chunker.split(text, source=str(f.relative_to(docs_path)))
        all_chunks.extend(chunks)
        print(f"  {f.name}: {len(chunks)} 个 chunk")

    print(f"共 {len(all_chunks)} 个 chunk,开始向量化...")
    texts = [c.text for c in all_chunks]
    vectors = embedder.embed(texts)

    print("入库...")
    store.add(all_chunks, vectors)
    store.save(store_path)
    print(f"完成。向量库: {store_path} ({len(all_chunks)} 条)")


if __name__ == "__main__":
    # 用法: python -m rag.ingest ./docs ./data/store
    ingest(sys.argv[1] if len(sys.argv) > 1 else "./docs",
           sys.argv[2] if len(sys.argv) > 2 else "./data/store")

入库是一次性操作,文档变了再跑一次。生产环境可以做成增量入库(只处理新增/修改的文件),但学习阶段全量重建最简单。

八、工具封装:KnowledgeSearchTool

这是本文最关键的一步------把RAG封装成工具 。复用Phase 3的 BaseTool 抽象,和 calculator / bash / read_file 同等地位:

python 复制代码
# tools/knowledge_search.py
from pathlib import Path
from pydantic import BaseModel, Field

from rag.embeddings import Embedder
from rag.reranker import Reranker
from rag.vector_store import VectorStore
from tools.base_tool import BaseTool


PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_STORE_PATH = str(PROJECT_ROOT / "data" / "store")


class KnowledgeSearchArgs(BaseModel):
    query: str = Field(description="检索查询词,应该是完整的问题或关键词组合,不要只填一个词")
    top_k: int = Field(default=3, ge=1, le=10, description="返回的文档片段数量,默认3")


class KnowledgeSearchTool(BaseTool):
    """
    Agentic RAG 的核心:把检索做成工具,让 LLM 自己决定何时调用。
    """
    def __init__(self, store_path: str = DEFAULT_STORE_PATH,
                 embedder: Embedder | None = None,
                 reranker: Reranker | None = None):
        self.embedder = embedder or Embedder()
        self.reranker = reranker
        self.store = VectorStore(dim=self.embedder.dim)
        try:
            self.store.load(store_path)
        except FileNotFoundError:
            pass  # 库未初始化,store 为空,execute 时会返回提示

    @property
    def name(self) -> str:
        return "knowledge_search"

    @property
    def desc(self) -> str:
        return (
            "在知识库中检索相关文档片段。当你需要查询项目文档、产品资料、历史案例、"
            "内部规范等知识时使用。返回相关片段及其来源和相关度分数。"
        )

    @property
    def args_schema(self) -> type[BaseModel]:
        return KnowledgeSearchArgs

    def run(self, args: KnowledgeSearchArgs) -> str:
        if len(self.store.chunks) == 0:
            return "知识库为空,请先运行 python -m rag.ingest ./docs ./data/store 入库文档。"

        # 1. 向量召回(粗排,多召回一些给 rerank 用)
        recall_k = args.top_k * 5  # 召回 5 倍
        q_vec = self.embedder.embed_query(args.query)
        candidates = self.store.search(q_vec, top_k=recall_k)

        if not candidates:
            return "未检索到相关内容。"

        # 2. 重排(精排,从粗排结果里挑 top_k)
        if self.reranker:
            results = self.reranker.rerank(args.query, candidates, top_k=args.top_k)
        else:
            results = candidates[:args.top_k]

        # 3. 格式化输出给 LLM 看
        lines = []
        for i, (chunk, score) in enumerate(results, 1):
            lines.append(
                f"【片段 {i}】(相关度 {score:.3f} | "
                f"来源: {chunk.metadata.get('source', 'unknown')})"
            )
            lines.append(chunk.text)
            lines.append("")

        return "\n".join(lines) if lines else "未检索到相关内容。"

关键设计点desc 字段是Agentic RAG成败的关键。LLM根据这段描述决定何时调用------必须明确告诉它"什么时候该用、什么时候不该用"。我的描述里写了"项目文档、产品资料、历史案例、内部规范",LLM问"你好"时就不会调用,问"项目架构"时就会调用。

九、接入主循环:main.py 改 3 处

改动1:导入新模块

python 复制代码
from tools.knowledge_search import KnowledgeSearchTool
from rag.embeddings import Embedder
from rag.reranker import Reranker

改动2:tool_list 加上新工具

python 复制代码
# RAG 工具全局只初始化一次(embedding 模型加载 + 向量库读盘)
_embedder = Embedder()
_reranker = Reranker(mode="llm")  # 学习用 LLM reranker,生产换 mode="bge"
_rag_tool = KnowledgeSearchTool(embedder=_embedder, reranker=_reranker)

tool_list: list[BaseTool] = [CalcTool(), FileReadTool(), BashTool(), _rag_tool]
tool_map = {t.name: t for t in tool_list}

改动3:PLANNER_SYSTEM_PROMPT 加上 knowledge_search 能力描述

text 复制代码
你拥有以下工具能力:
- calculator:计算数学表达式
- read_file:读取工作区内的文件内容(支持offset/limit分段读取)
- bash:执行shell命令(ls/grep/cat/wc/find等),用于文件系统操作、代码搜索、运行命令等
- knowledge_search:在知识库中检索项目文档、产品资料等内部知识(参数:query检索词, top_k返回数量)

run_agent_stream() 一行不改! AgentEvent / app.py / web/index.html 一行都不改!

这就是Phase 10事件生成器解耦的收益------加一个工具,对Agent核心循环是透明的。前端会自动渲染新的工具卡片(折叠展开),打字机效果自动复用,草稿归档逻辑自动复用。

十、验证点

跑通后用这三组问题验证Agentic RAG的行为:

验证1:简单问题不检索 。问"你好"、"1+1等于几"------Agent应该直接回答,不调用 knowledge_search。如果调了,说明 desc 写得不够清楚。

验证2:知识问题主动检索 。问"XXX公司的历史?"------Agent应该主动调用 knowledge_search,拿到chunk后基于内容回答,而不是凭LLM参数记忆编造。

验证3:多轮检索 。问"XXX公司有哪些模型?分别什么价位?"------Agent应该能调2次 knowledge_search(一次查模型,一次查价位),这是老式流水线RAG做不到的。Agent甚至可以第一次检索后自己判断"信息不够,需要再查一次"。

验证4:reranker效果对比 。暂时把 _reranker = None 跑一组问题,再开reranker跑同一组,对比top-1命中率。能明显看到reranker把粗排第3、4位的相关chunk提到第1位。

十一、Agentic RAG 的本质

回顾本文,我们做的事情可以浓缩成一句话:把检索从"流水线前置流程"降级为"Agent的主动动作"

这个降级带来的三个收益:

  1. 按需检索:简单问题不浪费检索调用
  2. 多轮检索:一次不够就再查,复杂问题可分解
  3. 可组合:检索结果和其他工具(bash、calculator)的结果可以混合推理

这其实就是 从"RAG系统"到"会查资料的Agent"的范式转变。RAG不是一个独立的系统,而是Agent的一个能力------和"会算数"、"会执行命令"一样,是Agent技能树上的一个节点。

后续 长期记忆 会把 memory_save / memory_recall 也做成工具,让Agent自主管理跨会话记忆------和本文的 knowledge_search 是同一设计哲学:把"能力"工具化,让Agent自己决定何时用

完整代码

本文完整代码已同步到仓库 day_10 分支:

🔗 Gitee 仓库

跑通步骤:

bash 复制代码
# 1. 装依赖
pip install openai numpy

# 2. 准备文档
mkdir docs
echo "自行输入XXX公司的相关历史" > docs/trae_intro.md

# 3. 入库
python -m rag.ingest ./docs ./data/store

# 4. 启动
python -m uvicorn app:app --host 0.0.0.0 --port 8000

# 5. 浏览器打开 http://localhost:8000 或者直接在main中调试

进阶

  • 混合检索 :在 knowledge_search.py 里加一路 BM25 检索(用 rank_bm25 库),和向量结果用 RRF 融合
  • Parent-Child chunking:检索小块、返回所属大块,解决"chunk 太短缺上下文"问题
  • Self-RAG:Agent 检索完自己判断"这些片段够不够回答",不够再查一次(已经有 Judge,扩展一下)

专栏:后端转Agent开发杂记 | 作者:小马小牛 | 持续更新中

相关推荐
数据库小学妹4 小时前
向量数据库选型:独立vs融合对比与场景推荐(附SQL实战)
经验分享·数据库架构·向量数据库·rag·数据库选型·ai数据库
sg_knight5 小时前
ZCode 3.0 安装全攻略:下载、配置、跑通第一个任务
llm·agent·ai编程·coding·ai工具·智谱·zcode
扯蛋4387 小时前
1.x 时代的记忆系统 (一)
langchain·llm·aigc
佛祖让我来巡山7 小时前
共鸣的灵魂:一个故事讲透AI是如何“理解”人类的
自然语言处理·llm·ai原理·自回归机制
梦想三三10 小时前
LangChain RAG PDF 智能问答实战:用 Streamlit 构建本地知识库(完整代码)
人工智能·python·langchain·大模型·rag
GuWenyue10 小时前
不用第三方SDK!Vue3原生Fetch实现DeepSeek流式输出,90%前端都会踩的分片解析坑一次性解决
前端·人工智能·llm
昵称好难啊11 小时前
ClaudeCode: 怎么规划任务
llm·agent
凉凉的知识库11 小时前
MCP 协议迎来重大升级:2026-07-28 新版核心变化速览
llm·claude·mcp
人生百态,人生如梦12 小时前
每日论文解读 (8.3) 1——DeepResearch Agent System:稀疏激活架构驱动的自主深度研究Agent
架构·llm·agent·deepresearch