feat(dispatcher): Eino 组件化补完 —— 检索 Retriever + 提示词 ChatTemplate

补完终态架构剩余两层(A 收尾):
- 检索:新增 ragRetriever(eino_components.go)实现 components/retriever.Retriever,
  包 mcp-go kb_search(NATS)、命中转 *schema.Document;retrieve() 改经组件,
  retriever 节点与报告分章检索统一走它。
- 提示词:buildMessages 改用 prompt.FromMessages + MessagesPlaceholder
  (FString 仅解析模板串、值原样注入 → JSON 花括号安全),产出消息序列与旧手拼等价。
- 工具:mcpTool 即 InvokableTool(Phase B),用于模型自主调用;静态 tool 节点
  确定性裸调用 by design。

测试:buildMessages 形状(含花括号)+ ragRetriever 文档解析单测;make test-go 全绿。

性能:compose 每任务编译实测 ~13µs(基准),相对 LLM 秒级可忽略 → 编译图缓存
判为 premature optimization 暂不做;并行效率已由 Phase C DAG 调度拿到。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-23 11:20:52 +08:00
parent 2e927eca0a
commit 45ad852806
5 changed files with 142 additions and 25 deletions
@@ -0,0 +1,62 @@
package eino
import (
"context"
"encoding/json"
"strings"
"github.com/cloudwego/eino/components/retriever"
"github.com/cloudwego/eino/schema"
"github.com/sundynix/sundynix-shared/contract"
)
const defaultRetrieveTopK = 4
// ragRetriever 把 mcp-go 的混合检索(NATS kb_search)包成 Eino components/retriever.Retriever。
// kb 在构造时绑定(owner 作用域库名);查询经 NATS 调 mcp-go,命中转 *schema.Document。
type ragRetriever struct {
caller ToolCaller
kb string
topK int
}
// newRetriever 构造一个绑定到指定库的 Eino Retriever 组件。
func (o *Orchestrator) newRetriever(kb string) retriever.Retriever {
return &ragRetriever{caller: o.tools, kb: kb, topK: defaultRetrieveTopK}
}
// Retrieve 实现 retriever.Retriever:经 NATS kb_search 检索,返回命中文档(不可用时降级空)。
func (r *ragRetriever) Retrieve(ctx context.Context, query string, _ ...retriever.Option) ([]*schema.Document, error) {
if r.caller == nil || r.kb == "" {
return nil, nil
}
topK := r.topK
if topK <= 0 {
topK = defaultRetrieveTopK
}
cctx, cancel := context.WithTimeout(ctx, toolCallTimeout)
defer cancel()
res, err := r.caller.CallTool(cctx, contract.ToolSubjectGo("kb_search"), &contract.ToolCall{
Tool: "kb_search", Args: map[string]any{"kb": r.kb, "q": query, "topK": topK},
})
if err != nil || res == nil || !res.OK || res.Content == "" || res.Content == "[]" {
return nil, nil
}
var hits []struct {
Text string `json:"text"`
Score float64 `json:"score"`
}
if json.Unmarshal([]byte(res.Content), &hits) != nil {
// 非结构化命中:整体作为单篇文档返回(与旧行为对齐)。
return []*schema.Document{{Content: res.Content}}, nil
}
docs := make([]*schema.Document, 0, len(hits))
for _, h := range hits {
docs = append(docs, &schema.Document{
Content: strings.TrimSpace(h.Text),
MetaData: map[string]any{"score": h.Score},
})
}
return docs, nil
}