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

# 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.go → NewChatModel |
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.go → NewEmbedder;被 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.go → Index;上游调用在 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.go → Retrieve |
// 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.Retrieve(internal/store/store.go)入口,所以固定流程和 agent 的知识库工具
都自动享受,eino 接口不变。任意一层都可单独开关、且失败自动降级(重排挂了退回融合排序,
不开混合就是纯向量)。重排两种后端(config.yaml 的 rerank.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.yaml 的 rag.rewrite_query。
5. Tool —— 可被模型调用的工具
接口契约:每个工具暴露一份 *schema.ToolInfo(名字 + 描述 + 参数 JSON Schema)
和一个 InvokableRun(ctx, argsJSON) (resultJSON, error)。模型读描述决定要不要调、怎么传参,
框架负责把模型生成的 JSON 参数喂进来、把返回 JSON 喂回去。
| 接口 | github.com/cloudwego/eino/components/tool(BaseTool / InvokableTool) |
| 构造助手 | tool/utils.InferTool —— 从一个普通 Go 函数 + 带 jsonschema tag 的入参结构体自动生成工具 |
| 本项目实现 | internal/tools/:knowledge_base_search、web_search、calculator、db_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.yaml 的 db 段(host 留空则不注册)后,
agent 模式即可回答"users 表有多少行""最近注册的 3 个用户邮箱"这类问题。
"自然语言查询"不是把中文直接发给 MySQL,而是 ReAct 循环里的两步:
- 模型先调
db_schema(空参数)列出所有表,再调一次看目标表的字段; - 据此自己写 SQL 调
db_query执行,拿到行数据后用中文作答,并把 SQL 作为database来源附上。
安全边界(只读):db_query 有一道 sanitizeReadQuery 守卫——
- 只允许
SELECT / SHOW / DESCRIBE / EXPLAIN / WITH开头的语句,其余(INSERT/UPDATE/DELETE/DDL)一律拒绝; - 拒绝多语句(
;拼接),杜绝SELECT ...; DROP TABLE ...这类注入; - 裸
SELECT自动补LIMIT(max_rows,默认 100),防止把大表整张拉回来。
实测:"请删除 users 表所有数据" → 模型按工具描述直接拒绝,不会触发任何写操作。
要放开写权限或改成独立 MCP 进程,看 database.go 顶部注释和 writeKeywords 白/黑名单即可。
这些组件如何被编排到一起
- 固定流程(
internal/agent/agent.go):手写Retriever → 判断分数 → 必要时 web 搜索 → ChatModel的串行逻辑。 - ReAct 编排(
internal/agent/react.go):用 eino 的flow/agent/react把ChatModel+ 一组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.Agent(buildSpecialistAgent),有自己更小的
工具集和更专注的系统提示。host.Specialist直接吃react.Agent的Generate/Stream
(即Invokable/Streamable),所以现有 ReAct 代码零改动被复用。 - 专家划分(
cmd/server/main.go的buildMultiAgent):知识专家拿 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.yaml 的 store.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.go 的 newVectorStore 按配置构建后端;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=qdrant、QDRANT_URL、QDRANT_COLLECTION、QDRANT_API_KEY。
③ 启动——/health 的 chunks 反映 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.2、claude-*、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
开关,transport选stdio(本地子进程)/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/*.go的InferTool描述)和系统提示
(internal/agent/react.go的reactSystemPrompt)划定边界。改这些文案后,跑上面三条回归即可。
启动时这三行日志能确认数据库工具已装上:
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.go 的 toolList。
三种 transport(config.yaml 的 mcp.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 等演示工具。把 enabled 改 true 启动,日志会打印
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.yaml 的 mcp.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.go 用 callbacks.AppendGlobalHandlers(...) 注册一个全局 metrics handler,就能全链路量到每次 LLM 调用、工具调用、检索的耗时与错误——internal/observability/ 三个文件(指标定义 / gin 中间件 / eino callback)即全部实现。
用 Docker 启动 Prometheus + Grafana(配置已放在 deploy/):
- 先确保应用已启动,且
config.yaml已开启observability.metrics_enabled: true:
go run ./cmd/server
curl http://127.0.0.1:9999/metrics
- 启动 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,应看到 enioagent 为 UP。也可以用 API 验证:
curl -s "http://localhost:9090/api/v1/targets"
curl -s "http://localhost:9090/api/v1/query?query=enioagent_http_requests_total"
- 启动 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,检索增强生成) | 回答前先去知识库"查资料",把查到的内容连同问题一起喂给大模型,让它据实作答而非凭记忆瞎编。 | 整个项目的核心范式:先 Retrieve 再 Generate,检索不到才联网。 |
| 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/ingest 按 chunk_size / chunk_overlap 切分;相邻片有重叠,防止把一句话拦腰截断。 |
| TopK | 检索时只取相似度最高的前 K 条。 | rag.top_k;retriever.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_total 按 prompt/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_query 的 sanitizeReadQuery。 |
| 可观测性(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 接缝叠加。 |
浙公网安备 33010602011771号