go+eino框架实现agent智能体

借助字节的eino框架实现了一个本地资料库+websearch+mcp(数据库、excel、计算。。。)的智能体,项目地址 https://gitee.com/newly-released_0/enioagent

ScreenShot_2026-07-02_093851_309

# enioagent

基于 [CloudWeGo Eino](https://github.com/cloudwego/eino) 的 RAG 智能体。
支持把 PDF / Word(.docx) / HTML / Markdown / TXT 文档入向量库,通过 Gin 接口查询;
本地命中就返回,命中不到自动联网搜索(DuckDuckGo)。Agent 模式下还能让模型自己写 SQL 查 MySQL。

向量库支持两种后端(`config.yaml` 的 `store.backend`):**local**(纯文件、零依赖、默认)
与 **qdrant**(外部 [Qdrant](https://qdrant.tech) 服务,可扩展、支持集群)。两者实现同一套
eino 接口,切换只改一行配置、上层代码零改动。详见下面"向量库后端"小节。

两种回答策略:

- **固定流程(默认)**:先本地检索,相关度不够再联网回退。流程写死,可控、省 token。
- **Agent 模式(`{"mode":"agent"}`)**:基于 eino ReAct 编排,模型自己决定调哪个工具
  (查知识库 / 联网 / 算计算器 / 查 MySQL),可多步、可组合。能力上限更高。
- **多 Agent 模式(`{"mode":"multi"}`)**:基于 eino host 多 agent 编排,一个"调度"大模型先把问题
  分诊给最合适的**专家**(知识专家 / 数据专家),每个专家是工具集更小、提示更专注的 ReAct agent,
  路由更准、更易扩展。

## 目录

- [快速开始](#快速开始)
- [架构](#架构)
- [Eino 组件抽象详解](#eino-组件抽象详解)
- [向量库后端](#向量库后端)
- [配置](#配置)
- [接口](#接口)
- [测试](#测试)
- [MCP 扩展](#mcp-扩展)
- [术语表(专业名词解释)](#术语表专业名词解释)
- [路线图](#路线图)

## 快速开始

前置:Go 1.26+,一个 OpenAI 兼容的大模型网关(`config.yaml` 里填 `base_url` / `api_key`)。
数据库工具可选,需要本地有 MySQL。

```bash
# 1. 拉依赖
go mod download

# 2. 改 config.yaml:填 llm / embedder 的 api_key(或用环境变量,见“配置”)

# 3. 启动
go run ./cmd/server -config config.yaml

# 4. 入库一篇文档,然后提问
curl -X POST localhost:9999/api/ingest/path \
  -H "Content-Type: application/json" -d '{"path":"data/docs"}'
curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" -d '{"question":"你的问题"}'

架构

cmd/server          程序入口,装配各组件
cmd/migrate         把本地文件版向量库迁移进 Qdrant (直接灌向量, 不重新 embedding)
internal/config     配置加载 (yaml + 环境变量覆盖)
internal/llm        ChatModel / Embedder 工厂 (OpenAI 兼容)
internal/ingest     文档解析(pdf/docx/html/txt/md) + 分片 + 入库
internal/store      向量库: 纯文件本地版(store.go, 含 BM25) + Qdrant 版(qdrant.go); 都实现 store.VectorStore 接口
internal/rerank     检索重排的 /v1/rerank HTTP 客户端 (provider=api 时用)
internal/search     DuckDuckGo 联网搜索封装
internal/tools      ReAct agent 可调用的工具: 知识库检索 / 联网 / 计算器 / MySQL 查询; 外部 MCP server 接入(mcp.go)
internal/agent      三种 agent: 固定流程(agent.go) + ReAct 工具编排(react.go) + host 多 agent(multi.go); 含 LLM 摘要/重排/查询改写
internal/memory     多轮对话记忆 (每会话一个 JSON 文件, 持久化)
internal/server     Gin HTTP 接口
examples/graph      独立示例: 手写 compose.Graph 编排 (离线, 无需 key)

Eino 组件抽象详解

Eino 把"大模型应用"拆成一组强类型、可组合的组件接口。每个组件只定义"输入→输出"的契约,
具体实现(OpenAI、本地文件、Milvus、自研工具……)可随意替换而不影响上层编排。本项目用到了
其中 5 个核心组件,下面结合本仓库的代码逐一说明。

1. ChatModel —— 对话模型

接口契约:输入一组消息 []*schema.Message,输出一条助手消息(含可能的 ToolCalls)。
既能一次性 Generate,也能流式 Stream。本项目用的是 model.ToolCallingChatModel
(支持函数调用的子接口),ReAct agent 靠它让模型"决定调哪个工具"。

接口 github.com/cloudwego/eino/components/model
本项目实现 eino-ext 的 OpenAI 适配器,任何 OpenAI 兼容网关都能用
代码位置 internal/llm/llm.goNewChatModel
cm, _ := modelopenai.NewChatModel(ctx, &modelopenai.ChatModelConfig{
    BaseURL: c.BaseURL, APIKey: c.APIKey, Model: c.Model,
})
// 返回 model.ToolCallingChatModel —— ReAct agent 直接吃这个接口

要点:temperature 仅在配置 > 0 时才下发(部分 beta 模型如 gpt-5.x 拒绝自定义温度)。

2. Embedder —— 向量化模型

接口契约EmbedStrings(ctx, []string) ([][]float64, error),把文本批量编码成稠密向量。
入库(索引文档)和查询(编码问题)用的必须是同一个模型,否则向量不在同一空间、相似度无意义。

接口 github.com/cloudwego/eino/components/embedding
本项目实现 eino-ext 的 OpenAI Embedding 适配器
代码位置 internal/llm/llm.goNewEmbedder;被 internal/store 持有
emb, _ := embopenai.NewEmbedder(ctx, &embopenai.EmbeddingConfig{
    BaseURL: c.BaseURL, APIKey: c.APIKey, Model: c.Model,
})
// store 在 Index/Retrieve 两条路径上都调它
vectors, _ := s.embedder.EmbedStrings(ctx, texts)

3. Indexer —— 写入路径

接口契约Index(ctx, []*schema.Document) ([]string, error),把文档(连同其向量、元数据)
写进存储,返回写入的 ID。这是 RAG 的"建库"环节。

接口 github.com/cloudwego/eino/components/indexer
本项目实现 自研纯文件向量库 internal/store.Store(也实现了 Retriever,见下);另有 Qdrant 版 store.QdrantStore
代码位置 internal/store/store.goIndex;上游调用在 internal/ingest

Store.Index 做三件事:① 用 Embedder 批量编码 → ② 向量归一化后 upsert(同 ID 覆盖,
重复入库幂等)→ ③ 原子落盘(temp 文件 + rename,防止写一半崩溃损坏索引)。

4. Retriever —— 读取路径

接口契约Retrieve(ctx, query string, opts...) ([]*schema.Document, error)
对查询做相似度检索,返回 TopK 个最相关文档,每个文档带 Score()(余弦相似度)。

接口 github.com/cloudwego/eino/components/retriever
本项目实现 同一个 internal/store.Store(向量预归一化,cosine 退化成点积,brute-force 扫全表)
代码位置 internal/store/store.goRetrieve
// store.go 顶部用一行编译期断言锁死接口契约:
var _ retriever.Retriever = (*Store)(nil)

设计亮点:同一个 Store 结构体同时实现 Indexer 和 Retriever,所以它可以原样
丢进 eino 的 Chain/Graph。上层的 ingest / agent / tools / server 只依赖 store.VectorStore
接口(Index / Retrieve / Size),所以换成 Qdrant(store.QdrantStore)只改 config.yaml
一行、代码零改动——想再接 Milvus / Redis 也只是照样另写一个实现。retriever.WithTopK(k) /
WithScoreThreshold 这类 Option 也是 eino 统一约定的,自定义实现照样能解析。

混合检索 + 重排 —— 多路召回再精排,提升相关性

纯向量检索是召回导向(快、近似),但 embedding 会把专有名词/精确词(型号、字段名、标识符)
糊成语义,单路向量未必排得准。完整检索链路在它之上加了两层:

              ┌─ 向量召回(语义,cosine)        ─┐
query ────────┤                                   ├─ RRF 融合 → fetch_k 候选 → 重排精排 → top_k → ChatModel
              └─ BM25 召回(关键词,字面匹配)   ─┘

① 混合检索(Hybrid):向量召回擅长语义近似,BM25 擅长字面精确匹配,二者互补。
RRF(Reciprocal Rank Fusion) 融合两路排名——每个文档得分 Σ 1/(rrf_k + 排名)
两路都靠前的自然冒头,无需调权重。BM25 是纯 Go 实现(internal/store/bm25.go
中文按字符 bigram 切分、英文按词,零分词器依赖),索引随入库自动重建。

② 重排(Rerank):把融合后的候选交给更强的打分器,按"与查询的真实相关性"精排,只留最相关几条。

两层都只落在 store.Retrieveinternal/store/store.go)入口,所以固定流程和 agent 的知识库工具
都自动享受,eino 接口不变。任意一层都可单独开关、且失败自动降级(重排挂了退回融合排序,
不开混合就是纯向量)。重排两种后端(config.yamlrerank.provider):

provider 怎么打分 适用
llm(默认) 复用对话大模型,把查询+候选 listwise 排序,返回 [{index,score}] 任何 OpenAI 兼容中转站都能用,无需专用 rerank 模型
api 调用专用 /v1/rerank 交叉编码器(Jina/Cohere 规范) 更快更省、质量更高,但中转站必须有 bge-reranker 之类的模型

本仓库的中转站没有专用 rerank 模型,所以默认走 llm。相关代码:混合检索 internal/store/bm25.go
LLM 重排 internal/agent/reranker.go/v1/rerank 客户端 internal/rerank/rerank.go
代价:混合检索几乎零成本(本地算);llm 重排每次查询多一次大模型调用(延迟约 +0.5~1.5s、少量 token)。

对话式查询改写 —— 让追问也能检索准

多轮对话里,用户的追问常常是省略的:先问"什么是 Eino?",再问"有哪些核心组件?"。
直接拿"它有哪些核心组件?"去检索,向量/BM25 都不知道"它"指谁,召回会跑偏。

改写这一步(internal/agent/rewrite.go)在检索之前插入一次轻量大模型调用,结合最近对话历史
把追问补全成独立、可单独检索的查询:

历史:用户"什么是 Eino?" / 助手"Eino 是字节开源的 Go 大模型框架…"
追问:"它有哪些核心组件?"  ──改写──▶  "Eino 有哪些核心组件?"  ──▶  拿去检索

关键点:只有送去检索/联网的 query 被改写,真正拿去生成答案、写进记忆的仍是用户原话,
所以改写不会泄漏进对话。且只作用于固定流程——agent 模式的 ReAct 模型本就带着完整历史、
能自己组织检索用的查询,无需这一步。任何失败(无历史、模型出错、结果异常)都自动退回原问题,
检索照常进行。开关:config.yamlrag.rewrite_query

5. Tool —— 可被模型调用的工具

接口契约:每个工具暴露一份 *schema.ToolInfo(名字 + 描述 + 参数 JSON Schema)
和一个 InvokableRun(ctx, argsJSON) (resultJSON, error)。模型读描述决定要不要调、怎么传参,
框架负责把模型生成的 JSON 参数喂进来、把返回 JSON 喂回去。

接口 github.com/cloudwego/eino/components/toolBaseTool / InvokableTool
构造助手 tool/utils.InferTool —— 从一个普通 Go 函数 + 带 jsonschema tag 的入参结构体自动生成工具
本项目实现 internal/tools/knowledge_base_searchweb_searchcalculatordb_schema / db_query
// internal/tools/tools.go —— 把"知识库检索"包成一个工具
func NewKBSearch(st store.VectorStore, topK int) (tool.InvokableTool, error) {
    type args struct {
        Query string `json:"query" jsonschema:"description=要在本地知识库中查找的内容"`
    }
    return utils.InferTool(NameKBSearch, "Search the local knowledge base ...",
        func(ctx context.Context, a args) (KBResult, error) {
            docs, _ := st.Retrieve(ctx, a.Query, retriever.WithTopK(topK))
            // ... 把检索结果整理成 JSON 返回
        })
}

工具都返回结构化 JSON,目的有二:① 喂回模型继续推理;② 被 agent 的 source 收集器
反解析成用户可见的 sources(保住 [1][2] 引用链)。calculator 是零依赖的算术求值器
(shunting-yard:分词 → 逆波兰 → 求值),既演示"模型自己选工具",也是接入企业内部
API / 数据库工具的模板——照着 InferTool 再写一个即可(下面的 MySQL 工具就是这么来的)。

自然语言查 MySQL(db_schema / db_query)

internal/tools/database.go 就是上面"接数据库工具"的实战范例:把一个只读 MySQL 拆成两个工具
注册进 agent,让模型用自然语言查库。配好 config.yamldb 段(host 留空则不注册)后,
agent 模式即可回答"users 表有多少行""最近注册的 3 个用户邮箱"这类问题。

"自然语言查询"不是把中文直接发给 MySQL,而是 ReAct 循环里的两步:

  1. 模型先调 db_schema(空参数)列出所有表,再调一次看目标表的字段;
  2. 据此自己写 SQLdb_query 执行,拿到行数据后用中文作答,并把 SQL 作为 database 来源附上。

安全边界(只读)db_query 有一道 sanitizeReadQuery 守卫——

  • 只允许 SELECT / SHOW / DESCRIBE / EXPLAIN / WITH 开头的语句,其余(INSERT/UPDATE/DELETE/DDL)一律拒绝;
  • 拒绝多语句(; 拼接),杜绝 SELECT ...; DROP TABLE ... 这类注入;
  • SELECT 自动补 LIMITmax_rows,默认 100),防止把大表整张拉回来。

实测:"请删除 users 表所有数据" → 模型按工具描述直接拒绝,不会触发任何写操作。
要放开写权限或改成独立 MCP 进程,看 database.go 顶部注释和 writeKeywords 白/黑名单即可。

这些组件如何被编排到一起

  • 固定流程internal/agent/agent.go):手写 Retriever → 判断分数 → 必要时 web 搜索 → ChatModel 的串行逻辑。
  • ReAct 编排internal/agent/react.go):用 eino 的 flow/agent/reactChatModel + 一组 Tool
    组成 reason→act 循环,模型自己决定调用顺序。其中:
    • react.AgentConfig.ToolsConfig 注册工具集;
    • MessageModifier 给每次模型调用注入系统提示;
    • 自定义 StreamToolCallChecker 解决"deepseek 等模型在 tool_call 前先吐一段文字、
      导致默认检测器只看首个 chunk 误判无工具调用"的流式问题;
    • eino callbacks(utils/callbacks)在每个工具 OnEnd 时把结果喂进 per-request 收集器,
      既线程安全(mutex + 存在 context 而非 agent 上)又能还原 sources
  • host 多 Agent 编排internal/agent/multi.go):用 eino 的 flow/agent/multiagent/host 做"分诊"。
    详见下面"多 Agent 编排"小节。

一句话:组件定义契约,编排决定流程。换模型、换向量库、加工具,都是替换某个组件实现,
而不是重写流程。

多 Agent 编排(host 模式)

单个 ReAct agent 把知识库、联网、计算器、MySQL、MCP 工具全挂在一个模型上——工具一多,
模型选择负担变重、也更容易选错。host 模式把它拆成几个各管一摊的专家,由一个"调度"大模型
(Host)先读问题、决定 hand-off 给谁:

                      ┌─ Host(调度大模型) 读问题,决定交给哪个专家
用户问题 ────────────▶│
                      ├─▶ knowledge_expert(ReAct: 知识库 + 联网 + 计算器 + MCP)
                      └─▶ data_expert     (ReAct: db_schema + db_query,只读取数)
  • Host:一个 ToolCallingChatModel,eino 把每个专家包装成 host 的一个"工具",Host 通过
    工具调用完成 hand-off(同样用自定义 StreamToolCallChecker 兼容 deepseek 的流式)。
  • Specialist(专家):每个专家就是一个 react.AgentbuildSpecialistAgent),有自己更小的
    工具集和更专注的系统提示。host.Specialist 直接吃 react.AgentGenerate/Stream
    (即 Invokable/Streamable),所以现有 ReAct 代码零改动被复用。
  • 专家划分cmd/server/main.gobuildMultiAgent):知识专家拿 KB + web(+ 共享的
    计算器 / MCP);数据专家拿 DB 工具,且只在配了数据库时才注册
  • sources 收集器与 ReAct 模式完全一样——专家内部工具的 OnEnd 回调喂进 per-request
    收集器,origin 照常是 local/web/database/mcp[1][2] 引用链不变。

好处是路由更准、每个专家提示更短、加新专家只是往 buildMultiAgent 里多塞一个 SpecialistSpec
代价是多一次 Host 调用(延迟 + token)。用 {"mode":"multi"} 启用,与固定流程 / agent 模式并存。

底层编排:手写 compose.Graph(示例)

host 多 agent 和 ReAct 都是构建在 eino 更底层的 compose.Graph 之上的。想看"裸图"长什么样,
examples/graph/ 是一个独立、离线、无需 API key 的小程序:把固定流程那套
"检索 → 按分数分支 → 本地作答 / 联网回退"用原始的节点 + 边 + 分支手工搭出来。

          ┌──────────────────────────────────────────────┐
 START ─▶ retrieve ─▶ (branch on score) ─▶ localGen ─▶ END
                              │                 ▲
                              └─▶ webSearch ─▶ webGen ─┘
  • compose.NewGraph[string, string]():定义"问题进、答案出"的图;
  • AddLambdaNode + compose.InvokableLambda:把普通 Go 函数包成节点(这里用假的检索/联网/生成,纯本地);
  • AddEdge 连边、compose.NewGraphBranch 按分数做条件路由(强命中→本地生成,弱命中→联网);
  • Compile 校验拓扑(无悬空边、类型匹配、能到达 END)后返回和其他 eino 组件一样的 Runnable。
go run ./examples/graph   # 打印两条问题分别走"本地作答"和"联网回退"两条分支

学会这一层,react.Agent / host.MultiAgent 的内部就不再是黑盒。

向量库后端

向量库通过 config.yamlstore.backend 选择,两种后端实现同一套 store.VectorStore
接口(Index / Retrieve / Size),上层代码无差别使用:

local(默认) qdrant
存储 单个 JSON 文件(data/index/store.json 外部 Qdrant 服务
检索 内存暴力扫全表(cosine) Qdrant HNSW 索引
依赖 零外部依赖 需跑一个 Qdrant(Docker / brew / 集群)
规模 数千~数万 chunk 数万~亿级,支持分片 + 副本集群
连接方式 —— REST API(internal/store/qdrant.go,不引 gRPC)
BM25 混合检索 ✅ 支持(内存 BM25 + RRF) ✅ 支持(Qdrant named sparse 向量 idf + 客户端 RRF 融合)
重排 Rerank
对话式改写

设计上 main.gonewVectorStore 按配置构建后端;SetHybrid / SetReranker 改成对
store.Hybridable / store.Rerankable 接口做类型断言——后端支持就接线、不支持就跳过并日志说明。
两种后端都实现了这两个接口,所以 rag.hybrid / rerank 在 local 与 qdrant 上行为一致。

Qdrant 版的混合检索:collection 建成 dense + 一个名为 bm25 的 sparse 向量(modifier: idf
IDF 由 Qdrant 服务端计算),入库时用与本地版相同的 tokenize() 生成 sparse 向量一并写入;
检索时分别跑 dense 与 sparse 查询,再在客户端用 RRF 融合两个排序。刻意不走服务端 fusion——
那会把 Score() 覆盖成 RRF 分,破坏上层 hasStrongHit / 低分过滤依赖的 cosine 0~1 语义。

用 Qdrant 后端

① 起一个 Qdrant(官方不发 macOS 二进制,用 Docker 或 brew):

docker run -p 6333:6333 -p 6334:6334 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
# 或: brew install qdrant && qdrant
# 起来后 http://localhost:6333/dashboard 有可视化界面

② 改配置config.yaml):

store:
  backend: "qdrant"              # 从 local 切到 qdrant
  qdrant:
    url: "http://localhost:6333"
    collection: "enioagent"      # 不存在则按 embedder 维度自动创建 (Cosine 距离)
    api_key: ""                  # 本地无需; Qdrant Cloud / 开鉴权时填
    timeout_seconds: 30

也可用环境变量覆盖:STORE_BACKEND=qdrantQDRANT_URLQDRANT_COLLECTIONQDRANT_API_KEY

③ 启动——/healthchunks 反映 Qdrant collection 里的 point 数:

go run ./cmd/server -config config.yaml   # 日志: vector store loaded: backend=qdrant, N chunks

chunk 的原始字符串 ID(如 sample.txt#0)经确定性 UUIDv5 映射为 Qdrant point ID,
因此同源文档重复入库是覆盖式幂等,与本地版行为一致。

从本地迁移到 Qdrant(不重新 embedding)

切到 Qdrant 后 collection 是空的,需要把既有数据灌进去。cmd/migrate 直接读本地
store.json 里已算好的向量灌入 Qdrant,跳过 embedding(省时省 token):

go run ./cmd/migrate -config config.yaml
# loaded 16 entries from data/index/store.json
# migrated 16 chunks into qdrant collection "enioagent" (total now 16)

幂等,可反复运行不产生重复。两个后端的数据互相独立、互不影响——切回 backend: local
本地文件原封不动还在。

配置

编辑 config.yaml(密钥也可用环境变量覆盖,避免写进文件):

  • server.addr:监听地址,默认 :9999
  • llm.model:对话模型。中转站当前可用 deepseek-v3.2claude-*gemini-* 等。
    Agent 模式要求模型支持 function calling
  • embedder.model:向量化模型,必须是 embedding 类型(如 text-embedding-3-small)。
  • store.backend:向量库后端。local(默认,纯文件)或 qdrant(外部 Qdrant,store.qdrant.*
    配连接)。详见上面"向量库后端"小节。
  • rag.score_threshold:本地最高分低于此值就联网搜索(余弦相似度 0~1)。
  • rag.hybrid / rag.rrf_k:开混合检索(向量 + BM25,RRF 融合)及融合常数。详见上面"混合检索 + 重排"小节。
  • rag.rewrite_query:开对话式查询改写。多轮追问时先用大模型把"它/这个"之类的指代补全成独立查询再检索
    (仅固定流程;agent 模式模型自带历史无需此步)。详见下面"对话式查询改写"小节。
  • rerank.*:检索重排。base_url 留空关闭;provider: llm(默认,复用对话模型)或 api(专用 /v1/rerank);
    fetch_k 是重排前候选数(应明显大于 rag.top_k)。
  • memory.max_turns:注入提示词的最近对话轮数(一问一答算一轮)。
  • agent.max_step:ReAct 循环最大步数(防死循环烧 token)。默认 25——数据库多表探查、
    Excel 多步操作(读→建表→写→画图)都很费步,模型每轮思考加每次工具调用各算一步,设太小会报 exceeds max steps
    多 Agent 模式下,每个专家的 ReAct 循环也用这个上限。
  • agent.enable_calculator:是否注册计算器工具(知识库检索 / 联网搜索始终注册)。
  • db.*:MySQL 只读查询工具的连接信息。db.host 留空则不注册数据库工具。db.max_rows
    限制单次返回行数(裸 SELECT 自动补 LIMIT)。
  • mcp.servers:接入外部 MCP server,把它们的工具注册进 agent。每个 server 单独 enabled
    开关,transportstdio(本地子进程)/ sse / http(远程 URL)。详见下面"MCP 扩展"小节。
# config.yaml 里的 db 段(host 留空即关闭数据库工具)
db:
  dialect: "mysql"
  host: "localhost"
  port: 3306
  user: "root"
  password: "123456"
  name: "seeai"
  max_rows: 100

环境变量覆盖:LLM_API_KEY LLM_BASE_URL LLM_MODEL EMBEDDER_API_KEY EMBEDDER_BASE_URL EMBEDDER_MODEL STORE_BACKEND QDRANT_URL QDRANT_COLLECTION QDRANT_API_KEY RERANK_BASE_URL RERANK_API_KEY RERANK_MODEL SEARCH_PROXY SERVER_ADDR DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME DB_DIALECT

安全提醒:config.yaml 里的 api_key 和数据库密码是明文。生产环境建议改用上面的环境变量,
别把含密钥的 config.yaml 提交进 git。

运行

go run ./cmd/server -config config.yaml

接口

健康检查(返回当前向量库分片数):

curl localhost:9999/health

上传文件入库(multipart,字段名 files,可多个):

curl -X POST localhost:9999/api/ingest \
  -F "files=@/path/to/a.pdf" -F "files=@/path/to/b.docx"

入库服务器本地路径(文件或整个目录):

curl -X POST localhost:9999/api/ingest/path \
  -H "Content-Type: application/json" -d '{"path":"data/docs"}'

查询(默认固定流程):

curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" -d '{"question":"你的问题"}'

Agent 模式(模型自己选工具:查库 / 联网 / 计算器):

curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" \
  -d '{"question":"用计算器算 88 的平方","mode":"agent"}'

多 Agent 模式(Host 分诊给知识专家 / 数据专家):

# 知识问题 → 路由到 knowledge_expert(sources 为 local/web)
curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" \
  -d '{"question":"eino 是什么框架?","mode":"multi"}'

# 数据问题 → 路由到 data_expert(sources 为 database,SQL 是 COUNT(*))
curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" \
  -d '{"question":"数据库里现在有多少个用户?","mode":"multi"}'

自然语言查 MySQL(需配好 db 段,agent 模式):

curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" \
  -d '{"question":"users 表里一共有多少行?","mode":"agent"}'
# 模型先 db_schema 看表结构,再自己写 SELECT 调 db_query 取数,最后用中文作答

多轮对话:带上同一个 session_id 即可注入历史(持久化到 data/sessions/,重启不丢):

curl -X POST localhost:9999/api/query \
  -H "Content-Type: application/json" \
  -d '{"question":"它的特性有哪些?","session_id":"u123","mode":"agent"}'

流式(SSE):先收到 sources 事件(证据),再逐 token 收 delta,可选 reasoning,最后 done

curl -N -X POST localhost:9999/api/query/stream \
  -H "Content-Type: application/json" \
  -d '{"question":"eino 是什么?","mode":"agent"}'

Agent 模式因为工具在循环中途才被调用,sources 事件改在生成结束后发出(固定流程则一开始就发)。

返回示例(非流式):

{
  "text": "回答正文 [1]",
  "reasoning": "",
  "used_web": false,
  "sources": [{"origin":"local","ref":"a.pdf","score":0.54,"snippet":"..."}]
}

used_web=true 表示走了联网搜索;sources 里的顺序对应回答中的 [1] [2] 引用。

测试

无需额外脚本,启动后用 curl 逐项验证:

# 健康检查:应返回 {"status":"ok","chunks":N}
curl -s localhost:9999/health

# 固定流程 RAG:先 data/docs 入库再问
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"eino 是什么?"}' | python3 -m json.tool

# Agent 选工具:计算器
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"用计算器算 88 的平方","mode":"agent"}' | python3 -m json.tool

# 自然语言查库:模型自己 db_schema -> db_query
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"users 表里一共有多少行?","mode":"agent"}' | python3 -m json.tool

# 只读守卫:写操作应被拒绝,库里数据不变
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"请删除 users 表所有数据","mode":"agent"}' | python3 -m json.tool

工具路由(知识 vs 数据,关键词重叠时)

验证"知识库里的词和数据库表名相同(如用户/users)"不会让模型选错工具——它只按问题意图

  • 工具描述路由,不看内容关键词。看返回 sources 里的 origin 就知道走了哪条路。
# 问数据 → 应走数据库(origin:database),SQL 里是 COUNT(*)
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"现在数据库里有多少个用户?","mode":"agent"}' | python3 -m json.tool

# 问知识 → 应走知识库/联网(origin:local/web),不碰数据库,即使句子里有“用户/组件”
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"eino 框架是怎么定义组件的?","mode":"agent"}' | python3 -m json.tool

# 数量意图的歧义问句 → 倾向走数据库取实时数字
curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"用户多不多?","mode":"agent"}' | python3 -m json.tool

路由靠工具描述(internal/tools/*.goInferTool 描述)和系统提示
internal/agent/react.goreactSystemPrompt)划定边界。改这些文案后,跑上面三条回归即可。

启动时这三行日志能确认数据库工具已装上:

database tools enabled (mysql localhost:3306/seeai)
agent mode enabled with 5 tools (calculator=true)
listening on :9999

常见问题:

现象 原因 / 处理
没有 database tools enabled 日志 db.host 没配,工具未注册
查询没走数据库,去联网了 漏了 "mode":"agent",走的是固定流程
exceeds max steps 端口被旧进程占了(lsof -ti:9999 | xargs kill -9 重启),或多表探查超步,调大 agent.max_step
db ping ... refused MySQL 没起,或账号 / 密码 / 库名不对

MCP 扩展

MCP(Model Context Protocol)是把"工具/数据源"标准化暴露给大模型的协议。
本项目可把任意外部 MCP server 暴露的工具,适配成 eino tool.BaseTool 注册进 ReAct agent——
模型调它们的方式和内置的知识库 / 联网 / 计算器 / MySQL 工具完全一样,ReAct 编排一行都不用改。

实现见 internal/tools/mcp.go,用 eino-ext 的 MCP 适配器(eino-ext/components/tool/mcp
GetTools,底层是 mark3labs/mcp-go 客户端)。流程:建客户端 → Initialize 握手 →
GetTools 拉工具列表 → append 进 cmd/server/main.gotoolList

三种 transport(config.yamlmcp.servers[].transport):

transport 怎么连 配置字段
stdio(默认) 启动一个子进程,走 stdin/stdout 通信 command + args(+ 可选 env
sse 连接远程 SSE 端点 base_url
http 连接远程 streamable-HTTP 端点 base_url
# config.yaml 的 mcp 段:每个 server 单独开关,enabled=false 或 servers 为空则不接入
mcp:
  servers:
    - name: "everything"
      enabled: false                # 改 true 即接入(联调用)
      transport: "stdio"
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-everything"]
      # tools: ["add", "echo"]      # 留空=导入该 server 全部工具;填则只导入指定工具
    # SSE/HTTP 远程 server 示例:
    # - name: "remote"
    #   enabled: false
    #   transport: "sse"
    #   base_url: "http://localhost:12345/sse"

联调可用官方示例 server @modelcontextprotocol/server-everything(需本机有 npx),它提供
add / echo / longRunningOperation 等演示工具。把 enabledtrue 启动,日志会打印
mcp server "everything" connected (N tools, transport=stdio),随后 agent 模式即可调用:

curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"用 echo 工具回显 hello","mode":"agent"}' | python3 -m json.tool

实战:自然语言操作 Excel(读写 / 汇总 / 画图)

接一个真实的第三方 MCP server——excel-mcp-server(纯 Python,不需要装 Microsoft Excel),
就能用中文让 agent 读写 xlsx、做数据汇总、建工作表、画图表。它是 Python 包,用
uv 跑最省事(uv 自带隔离环境和 Python 版本管理,绕开系统 Python 的依赖坑):

brew install uv          # 或 curl -LsSf https://astral.sh/uv/install.sh | sh
mkdir -p data/excel      # 放 xlsx 的目录

config.yamlmcp.servers 加一条:

    - name: "excel"
      enabled: true
      transport: "stdio"
      command: "uvx"
      args: ["excel-mcp-server", "stdio"]
      env:
        # ⚠️ stdio 模式必须用绝对路径(EXCEL_FILES_PATH 只在 sse/http 模式才作基准目录)
        - "EXCEL_FILES_PATH=/abs/path/to/enioagent/data/excel"

首次启动 uvx 会自动拉包并建隔离环境(几秒),日志出现
mcp server "excel" connected (25 tools, transport=stdio) 即接入成功。然后直接说人话:

curl -s -X POST localhost:9999/api/query \
  -H 'Content-Type: application/json' \
  -d '{"question":"读取 /abs/path/to/data/excel/sales.xlsx 的 sales 工作表,新增一个 sheet 并画出各区销售额的饼图","mode":"agent"}' \
  | python3 -m json.tool

agent 会自己拆成多步工具调用:read_data_from_excel → create_worksheet → write_data_to_excel → create_chart
结果作为 origin: mcp 的 source 附回。两个易踩的坑:

  • 文件名要用绝对路径:stdio 模式下 excel-mcp-server 要求客户端传绝对路径,提问时写全路径(或改用 sse 模式跑 uvx excel-mcp-server sse 才认相对文件名)。
  • 多步任务要够步数:读→建 sheet→写→画图 是 4 次工具调用,加上模型每轮思考,容易撞上 agent.max_step。本项目已把默认调到 25;Excel/DB 这类多步组合别设太小,否则会报 exceeds max steps

openpyxl 只能读、不能创建原生透视表,所以这类 server 的"透视表"多是 Python 算好的静态汇总表。要真正的可交互原生透视表,得走 Windows COM 版的 Excel MCP。

MCP 工具的名字是动态的(由 server 决定),所以 main.go 把它们的名字收集起来传给
reactAgent.SetMCPToolNames(...),agent 的 source 收集器据此把这些工具的返回结果以
origin: mcp 形式附进 sources,保住 [1][2] 引用链。

想自研一个本地工具而非接外部 MCP,看 internal/tools/database.go——它用 InferTool
把只读 MySQL 包成两个工具,是"手写工具接入 agent"的完整范例。

带记忆问答原理

server.go:handleQuery
  └─ s.react.Query(ctx, "u123", "它的特性?")          react.go:158
       │
       ├─① loadHistory("u123")                         react.go → mem.History()
       │     └─ memory.go:81 History()
       │          ├─ read("u123") 读 data/sessions/u123.json     memory.go:131
       │          ├─ 截断到最近 maxTurns*2 条                      memory.go:93
       │          └─ 转成 []*schema.Message (user/assistant 交替)  memory.go:97
       │
       ├─② 拼接:input = [历史…] + UserMessage("它的特性?")        react.go:161
       │
       ├─③ agent.Generate(ctx, input, callbacks)        react.go:163
       │     └─ 模型看到整段对话 → 可能调工具 → 出答案
       │
       ├─④ snapshot() 收集本轮 sources                  react.go:172
       │
       └─⑤ saveHistory("u123", 问, 答)                  react.go → mem.Append()
             └─ memory.go:59 Append()
                  ├─ read 旧文件
                  ├─ append 两条(user+assistant)         memory.go:71
                  └─ 原子写回 u123.json (temp+rename)     memory.go:148

可观测性(Prometheus 指标)

开启后暴露 /metrics,记录 HTTP 请求AI 管线内部调用(LLM / 工具 / 检索)的耗时、成败、token 用量。默认关闭,零开销。

# config.yaml
observability:
  metrics_enabled: true

为什么代码里不配置 Prometheus 的地址:Prometheus 是"拉"模型——不是应用去连 Prometheus,而是 Prometheus 每隔一段时间主动来抓应用的 /metrics。所以应用只负责把指标暴露在自己端口上(跟随 server.addr,本项目即 :9999),完全不需要知道 Prometheus 在哪。抓取目标写在 Prometheus 那边的 deploy/prometheus.yml

scrape_configs:
  - job_name: enioagent
    static_configs:
      - targets: ["192.168.10.15:9999"]   # 应用地址(宿主机局域网 IP),由 Prometheus 来抓

埋点原理:不用在每个组件里手写埋点。eino 对每个 model / tool / retriever 节点都发 OnStart/OnEnd/OnError 回调,main.gocallbacks.AppendGlobalHandlers(...) 注册一个全局 metrics handler,就能全链路量到每次 LLM 调用、工具调用、检索的耗时与错误——internal/observability/ 三个文件(指标定义 / gin 中间件 / eino callback)即全部实现。

用 Docker 启动 Prometheus + Grafana(配置已放在 deploy/):

  1. 先确保应用已启动,且 config.yaml 已开启 observability.metrics_enabled: true
go run ./cmd/server
curl http://127.0.0.1:9999/metrics
  1. 启动 Prometheus(读取 deploy/prometheus.yml,抓取本机 :9999/metrics):
docker run -d --name prometheus -p 9090:9090 \
  -v "$PWD/deploy/prometheus.yml:/etc/prometheus/prometheus.yml" \
  prom/prometheus:latest

打开 http://localhost:9090/targets,应看到 enioagentUP。也可以用 API 验证:

curl -s "http://localhost:9090/api/v1/targets"
curl -s "http://localhost:9090/api/v1/query?query=enioagent_http_requests_total"
  1. 启动 Grafana(数据源和 dashboard 会自动加载):
docker run -d --name grafana -p 3000:3000 \
  -e GF_SECURITY_ADMIN_USER=admin \
  -e GF_SECURITY_ADMIN_PASSWORD=admin \
  -v "$PWD/deploy/grafana/provisioning:/etc/grafana/provisioning" \
  -v "$PWD/deploy/grafana/dashboards:/var/lib/grafana/dashboards" \
  grafana/grafana:latest

打开 http://localhost:3000,账号/密码是 admin/admin,进入 Dashboards → enioagent → enioagent 可观测性。Grafana 通过 provisioning 自动连上 Prometheus(数据源地址 http://192.168.10.15:9090,见 deploy/grafana/provisioning/datasources/prometheus.yml;换机器改这个 IP 即可),并自动加载 deploy/grafana/dashboards/enioagent.json——含 HTTP / LLM / 工具 / 检索的速率、P95 延迟与 token 用量共 8 个面板。

常用维护命令:

# 查看运行状态
docker ps --filter "name=prometheus" --filter "name=grafana"

# 查看日志
docker logs prometheus
docker logs grafana

# 停止/启动
docker stop prometheus grafana
docker start prometheus grafana

# 删除后重建(会删除容器,不会删除 deploy/ 下的配置)
docker rm -f prometheus grafana

查询示例(PromQL):

rate(enioagent_llm_tokens_total[5m])
histogram_quantile(0.95, sum by (le, tool) (rate(enioagent_tool_call_duration_seconds_bucket[5m])))
sum by (tool,status) (enioagent_tool_calls_total)

主要指标:enioagent_http_requests_total / _http_request_duration_seconds_llm_calls_total / _llm_call_duration_seconds / _llm_tokens_total{type=prompt|completion|reasoning}_tool_calls_total / _tool_call_duration_seconds_retrieval_calls_total / _retrieval_duration_seconds,外加 Go runtime / 进程基础指标。

分布式追踪(trace,OTLP → Jaeger/Tempo)与结构化日志(slog 注入 trace_id)属于下一阶段 P1,可在同一 eino callback 接缝上叠加,无需再改组件代码。

路线图

术语表(专业名词解释)

本节把项目里出现的专业名词集中解释一遍,每个词先给一句话本质,再说在本项目哪里用到
按主题分组,方便对照上文查阅。

一、RAG 与向量检索

名词 一句话本质 在本项目
RAG(Retrieval-Augmented Generation,检索增强生成) 回答前先去知识库"查资料",把查到的内容连同问题一起喂给大模型,让它据实作答而非凭记忆瞎编。 整个项目的核心范式:先 RetrieveGenerate,检索不到才联网。
Embedding / 向量化 把一段文本用模型编码成一串浮点数(向量),语义相近的文本向量也相近。 internal/llm 的 Embedder;入库和查询都用它,且必须同一个模型
稠密向量(dense vector) Embedding 产出的那种"每一维都有值"的向量,表达语义。 向量召回走的就是它。
稀疏向量(sparse vector) 绝大多数维是 0、只在出现的词上有值的向量,表达"字面命中了哪些词"。 Qdrant 后端的 BM25 用命名 sparse 向量(modifier: idf)实现。
余弦相似度(cosine similarity) 用两个向量的夹角衡量相似度,0~1,越大越像。 检索打分用它;向量预先归一化后 cosine 退化成点积,算得更快。
归一化(normalization) 把向量缩放成单位长度,使点积恰好等于余弦相似度。 Store.Index 入库时对向量归一化。
分片(Chunk) 把长文档切成若干小段再分别向量化,检索时命中的是"段"而非整篇。 internal/ingestchunk_size / chunk_overlap 切分;相邻片有重叠,防止把一句话拦腰截断。
TopK 检索时只取相似度最高的前 K 条。 rag.top_kretriever.WithTopK(k)
向量库 / 向量数据库 专门存向量并支持"最近邻检索"的存储。 本项目两种后端:本地纯文件版 + Qdrant。
暴力检索(brute-force) 拿查询向量和库里每一条逐个算相似度,简单但线性慢。 本地文件后端就是它,数千~数万条足够快。
HNSW(分层可导航小世界图) 一种近似最近邻索引,用图结构把检索从"扫全表"降到近似对数级,海量向量也快。 Qdrant 后端用它,支撑数万~亿级规模。
BM25 经典的关键词打分算法,看查询词在文档里的词频/稀有度,擅长精确字面匹配。 internal/store/bm25.go,纯 Go 实现,中文按字符 bigram 切、英文按词。
IDF(逆文档频率) "越少文档出现的词越重要"的权重,BM25 的核心因子。 本地 BM25 自己算;Qdrant 版由服务端算(modifier: idf)。
bigram(二元分词) 把中文按"相邻两字"切成词单元(如"向量库"→"向量""量库"),零分词器依赖。 BM25 的中文切分方式。
混合检索(Hybrid) 同时跑向量召回和 BM25 召回,再融合两路结果,语义+字面互补。 rag.hybrid;解决 embedding 把专有名词"糊成语义"排不准的问题。
RRF(Reciprocal Rank Fusion,倒数排名融合) 融合多路排名的简单算法:每文档得分 Σ 1/(k+排名),两路都靠前的自然冒头,无需调权重 rag.rrf_k(默认 60),融合向量与 BM25 两路。
召回 vs 精排 召回=快而粗地捞回一批候选;精排=对候选再仔细排一次。 fetch_k 召回候选,再 Rerank 精排到 top_k
Rerank(重排) 用更强的打分器对候选按"与查询的真实相关性"重新排序,只留最相关几条。 rerank.*;两种后端 llm / api,失败自动降级回融合排序。
交叉编码器(cross-encoder) 把"查询+候选"拼在一起同时编码打分,比分别编码更准但更慢,专用 rerank 模型属于这类。 provider: api/v1/rerank(如 bge-reranker)走的就是它。
listwise 排序 一次把整个候选列表交给模型排序(相对"两两比较")。 provider: llm 复用对话模型做 listwise 打分,返回 [{index,score}]
score_threshold(分数阈值) 本地最高分低于它就认为"没查好",触发联网回退。 rag.score_threshold(余弦 0~1)。
查询改写(query rewrite) 多轮追问里把"它/这个"之类指代补全成能独立检索的完整问题,再拿去检索。 internal/agent/rewrite.go;只改送检索的 query,不改写进记忆的原话,仅固定流程用。

二、大模型与生成

名词 一句话本质 在本项目
LLM(大语言模型) 能理解和生成自然语言的大模型。 对话生成、查询改写、LLM 重排都靠它。
ChatModel / Embedder Eino 的两类模型组件:前者做对话,后者做向量化。 internal/llm/llm.go 用 OpenAI 适配器构造。
token 大模型处理文本的最小单位(约等于半个中文字 / 一个词根),计费和长度都按它算。 指标 _llm_tokens_totalprompt/completion/reasoning 分类统计。
prompt / completion token 输入(提示)消耗的 token 叫 prompt,模型输出消耗的叫 completion。 分开计量,方便算成本。
temperature(温度) 采样随机性旋钮:越高越发散有创意,越低越确定保守,0 最稳定。 llm.temperature;仅 >0 时才下发(部分 beta 模型必须为 0)。
Function calling / Tool calling(函数调用) 模型不直接答,而是输出"要调用哪个函数、传什么参数"的结构化请求,由程序执行后再把结果喂回。 Agent 模式的基石;要求模型支持此能力。
reasoning(推理)模型 会先输出一段"思考过程"再给答案的模型(如带 reasoning 的模型)。 前端把 reasoning 事件渲染成可折叠的"思考过程";deepseek-v3.2 不发这类内容。
流式(Stream)/ SSE(Server-Sent Events) 边生成边逐块推给前端,而非等全部生成完再一次性返回;SSE 是浏览器原生的服务器单向推送协议。 /api/query/stream;前端打字机效果、工具进度 status 都靠它。

三、Agent(智能体)与编排

名词 一句话本质 在本项目
Agent(智能体) 能自己决策、调用工具、多步完成任务的大模型程序,而不只是一问一答。 三种:固定流程 / ReAct / host 多 Agent。
ReAct(Reason + Act) 让模型循环地"思考(Reason)→行动(Act,调工具)→看结果→再思考",直到得出答案。 internal/agent/react.go,用 eino 的 flow/agent/react
Tool(工具) 暴露给模型调用的一个函数,带名字、描述、参数 schema。 知识库检索 / 联网 / 计算器 / MySQL / MCP 工具。
工具编排 / 固定流程 编排=模型自己决定调用顺序;固定流程=代码写死步骤。 Agent 模式是前者,默认模式是后者(可控省 token)。
hand-off(交接/分诊) 调度者把任务转交给最合适的下游处理者。 host 模式的 Host 把问题分诊给某个专家。
Host / Specialist(宿主 / 专家) 多 Agent 里的角色:Host 只负责选人,Specialist 是各管一摊的子 Agent。 internal/agent/multi.go:知识专家 + 数据专家。
max_step(最大步数) ReAct 循环的步数上限,防止模型死循环烧 token。 agent.max_step(默认 25);多表探查/Excel 多步很吃步数。
系统提示(system prompt) 每次模型调用前注入的"角色与规则"说明,划定它怎么行事。 reactSystemPrompt;通过 MessageModifier 注入。
callbacks(回调) 框架在每个节点 OnStart/OnEnd/OnError 时触发的钩子。 收集 sources、埋 metrics、发工具进度 status 都挂在这。
sources / 引用链 [1][2] 把工具返回结果整理成"来源"列表,答案里用编号引用,可溯源。 per-request 收集器(线程安全,存 context 非 agent 上)还原。

四、框架与基础设施

名词 一句话本质 在本项目
Eino 字节 CloudWeGo 开源的 Go 大模型应用框架,把应用拆成强类型可组合的组件。 本项目的底座;换实现不改流程。
组件抽象 / 接口契约 只定义"输入→输出"的接口,具体实现可随意替换。 ChatModel / Embedder / Indexer / Retriever / Tool 五大件。
Indexer / Retriever Eino 的写入路径与读取路径两个组件接口。 同一个 Store 同时实现二者,可直接丢进 Chain/Graph。
compose.Graph / Chain Eino 的底层编排原语:用节点+边+分支把组件连成数据流图(Chain 是线性特例)。 ReAct/host 都构建在其上;examples/graph/ 是裸图 demo。
Runnable 编排编译(Compile)后产出的可执行体,输入进、输出出。 Graph 校验拓扑后返回它。
编译期断言(var _ I = (*T)(nil) 一行代码让编译器强制检查 T 确实实现了接口 I。 store.go 用它锁死 retriever.Retriever 契约。
幂等(idempotent) 同样操作做多次和做一次效果相同。 同源文档重复入库覆盖式写入,不产生重复。
UUIDv5 由"命名空间+名字"确定性生成的 UUID(同输入必得同输出)。 把 chunk 字符串 ID 映射成 Qdrant point ID,保证幂等。
upsert "有则更新、无则插入"的写入。 Store.Index 按 ID upsert。
原子写(temp + rename) 先写临时文件再重命名替换,避免写一半崩溃损坏数据。 向量库落盘、会话文件持久化都用它。
MCP(Model Context Protocol) 把"工具/数据源"标准化暴露给大模型的开放协议。 internal/tools/mcp.go 适配外部 MCP server 成 eino 工具。
transport(stdio / sse / http) MCP 客户端连 server 的三种方式:本地子进程管道 / 远程 SSE / 远程 HTTP。 mcp.servers[].transport
shunting-yard(调度场算法) 把中缀算式转成逆波兰式再求值的经典算法。 calculator 工具零依赖求值。
只读守卫 / SQL 注入 只放行读语句、拒绝多语句拼接,防 SELECT ...; DROP TABLE 这类攻击。 db_querysanitizeReadQuery
可观测性(Observability) 通过指标/日志/追踪了解系统内部运行状态。 Prometheus metrics(P0);trace 是下一阶段。
Prometheus / Grafana 前者按"拉"模型定时抓 /metrics 存时序数据,后者做可视化面板。 deploy/ 下有现成配置。
P95 延迟 / histogram 95% 的请求快于该耗时;直方图桶用于估算分位数。 面板用 histogram_quantile(0.95, ...)
PromQL Prometheus 的查询语言。 见"可观测性"小节的查询示例。
trace / OTLP / trace_id(下一阶段) 分布式追踪:把一次请求经过的各环节串成一条链,OTLP 是上报协议,trace_id 是贯穿的标识。 路线图 P1,可在同一 callback 接缝叠加。
posted on 2026-07-02 09:42  朝阳1  阅读(23)  评论(0)    收藏  举报