目前大部分AI应用基本都是python开发,因为python的语言优势和学习成本,使其成为计算科学、深度学习的主流编程语言。

因为公司项目的需求,所以最近调研了下golang有什么比较好的AI框架,现在github有几个比较火的框架,比如 langchaingoeino还有最近的 adk-go

此文主要介绍下langchaingo和eino,因为adk-go才推出不久,我还没有用过

Langchaingo @

核心能力 @

对于现代的AI框架来说,模型、编排、记忆、数据成为核心功能,langchain就是最典型的ai框架之一。

langchaingo的核心能力主要包括以下部分:

  • 模型集成,langchaingo提供几个厂商的llm client的封装,包括openai​、anthropic​、googleai​、ollama​、huggingface

  • 链式编排,链是 LangChainGo 的核心编排能力,用于将多个组件串联成复杂的工作流。

    • 基础链​:如 LLMChain​(提示词 + LLM)、ConversationChain(对话管理)。
    • 组合链​:如 SequentialChain​(顺序执行多个链)、RouterChain(根据输入动态选择执行的链)。
    • 自定义链:支持开发者通过实现 Chain 接口创建业务专属的链逻辑。
  • 数据连接,负责与外部数据源交互,让 LLM 能够访问和处理私有数据。

    • 文档加载器:支持加载多种格式的文档,如文本文件、Markdown、PDF、HTML 等。
    • 文本分割器:提供多种策略(如按字符、按句子、递归字符分割)将长文档切分为适合模型处理的小块。
    • 向量存储集成:支持主流向量数据库,如 Pinecone、Chroma、Weaviate、本地文件存储等,用于文档的嵌入存储与检索。
    • 检索增强生成(RAG) :通过 RetrievalQA 链快速构建 RAG 应用,实现基于私有数据的问答。
  • 智能体,赋予 LLM 自主决策和调用工具的能力,是构建复杂自主应用的核心。

    • 工具调用​:内置多种工具(如 SerpAPI​ 搜索引擎、Calculator 计算器),也支持自定义工具。
    • 智能体类型​:实现了 ReAct​、ZeroShotAgent 等经典智能体模式,支持 LLM 根据任务规划步骤、选择工具、执行操作、验证结果。
    • 记忆机制:智能体可结合记忆组件,保留对话或任务上下文。
  • 记忆,用于在对话或多轮任务中保留上下文信息。

    • 多种记忆类型​:如 ConversationBufferMemory​(简单对话缓存)、ConversationSummaryMemory​(对话摘要记忆)、VectorStoreRetrieverMemory(基于向量的长期记忆)。
    • 状态管理:将记忆无缝集成到链或智能体中,实现多轮对话的上下文连贯。

入门级用法 @

如果需要调用deepseek可以使用openai的协议,因为deepseek也兼容openai的调用方式,初始化llm代码如下

llm, err := openai.New(
	openai.WithModel("deepseek-chat"),
	openai.WithBaseURL("https://api.deepseek.com"),
	openai.WithToken("xxxxxxxxxooooooooo"),
)

langchaingo提示词的类型主要是以下几类

const (
	// ChatMessageTypeAI is a message sent by an AI.
	ChatMessageTypeAI ChatMessageType = "ai"
	// ChatMessageTypeHuman is a message sent by a human.
	ChatMessageTypeHuman ChatMessageType = "human"
	// ChatMessageTypeSystem is a message sent by the system.
	ChatMessageTypeSystem ChatMessageType = "system"
	// ChatMessageTypeGeneric is a message sent by a generic user.
	ChatMessageTypeGeneric ChatMessageType = "generic"
	// ChatMessageTypeFunction is a message sent by a function.
	ChatMessageTypeFunction ChatMessageType = "function"
	// ChatMessageTypeTool is a message sent by a tool.
	ChatMessageTypeTool ChatMessageType = "tool"
)

实际常用的就是human和system了

以下demo是我使用AI将原有的python写的poc插件转为nuclei规则的提示词

Nuclei 是一款现代化的高性能漏洞扫描器,它利用基于 YAML 的简单模板。能够设计模拟真实世界条件的自定义漏洞检测场景,从而实现零误报。

content := []llms.MessageContent{
	llms.TextParts(llms.ChatMessageTypeSystem,
		`你是一个资深的安全行业从业者,擅长编写基于nuclei的yaml规则。
			要求如下:
			1. 输入:python代码
			2. 输出:nuclei的yaml规则
			4. 只能使用request协议或network协议
			5. 不要做其他任何说明,请直接输出纯文本,不要包含任何格式标记
			6. 请进行自检查,确认多个matcher之间的条件关系或者matcher内多个匹配中间的条件关系是否正确,可以使用condition: and 或者 matchers-condition: and 进行修正
			7. 如果存在多个请求之间的逻辑关系,可以使用flow语法,但仅限于使用&&或者||等运算符,例如:flow: requests(1) && requests(2) && requests(3) || requests(4) && requests(5) && requests(6)
	`),
	llms.TextParts(llms.ChatMessageTypeHuman, pycode),
}

如果只是调用llm来生成回答,那接下来可以直接使用llm.GenerateContent方法

下例是我将内容直接输入到文件的例子,这样可以直接帮我生成nuclei的yaml规则

completion, err := llm.GenerateContent(
	ctx,
	content,
	llms.WithMaxTokens(2000),
	llms.WithTemperature(0.7),
	llms.WithStreamingReasoningFunc(func(ctx context.Context, reasoningChunk []byte, chunk []byte) error {
		file, err := os.OpenFile("test1.yaml", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			return err
		}
		defer file.Close()

		writer := bufio.NewWriter(file)
		defer writer.Flush()
		if _, err := writer.Write(chunk); err != nil {
			return err
		}
		if writer.Buffered() > 4096 {
			return writer.Flush()
		}
		if len(chunk) > 0 {
			fmt.Printf("Streaming Content: %s\n", string(chunk))
		}
		return nil
	}),
)

llms.WithStreamingReasoningFunc是流式输出的方法,可以接受一个callback函数将流的内容自定义处理,比如上例我通过一个callback的回调将其内容直接输出到文本中,如果只是简单输出,那下例即可

ctx := context.Background()
userprompt := "你能帮我实现愿望吗?"
content := []llms.MessageContent{
	llms.TextParts(llms.ChatMessageTypeSystem,`你是一个小仙女,能帮别人实现所有愿望`),
	llms.TextParts(llms.ChatMessageTypeHuman,userprompt),
}
completion, err := llms.GenerateFromSinglePrompt(
	ctx,
	llm,
	content,
	llms.WithTemperature(0.8),
	llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
		fmt.Print(string(chunk))
		return nil
	}),
)

langchaingo基本是继承自其python的langchain框架,核心的能力自然是其链式的编排能力,比如可以写一个简单的链式编排

第一个链负责为主题生成一段介绍,第二个链负责对一链的内容进行翻译


template := "请用{style}风格,为{topic}写一段简短的介绍(不超过50字):"
prompt := prompts.NewPromptTemplate(
	[]string{"style", "topic"}, // 输入变量
	template,                   // 模板字符串
)
llmChain := chains.NewLLMChain(llm, prompt)
input := map[string]any{
	"style": "幽默风趣",
	"topic": "程序员",
}

template2 := "请将下面的内容翻译成英文:\n{content}"
prompt2 := prompts.NewPromptTemplate(
	[]string{"content"},
	template2,
)
llmChain2 := chains.NewLLMChain(llm, prompt2)
sequentialChain := chains.NewSequentialChain(
	[]chains.Chain{llmChain, llmChain2},
	[]string{"style", "topic"},  // 输入变量
	[]string{"content"},         // 中间变量(第一个Chain的输出作为第二个Chain的输入)
)
seqResult, err := chains.Run(ctx, sequentialChain, input)
if err != nil {
	log.Fatalf("运行Sequential Chain失败: %v", err)
}
fmt.Println("翻译结果:")
fmt.Println(seqResult)

总体来说,langchaingo的功能对于大部分业务开发都基本够用,但作为目前的ai框架趋势,我更推荐下面介绍的eino框架。

Eino @

eino是字节推出的开源ai agent开发框架,刚推出的时候我就关注了这个项目,因为其比langChain更加强大,如果你用过python的 langGraph框架,那eino可以说是将langChain和langGraph能力集成在一起开发了一个更加全面的框架。

Eino核心能力 @

  • 模型集成,包括qwen​、qianfan​、OpenAI​、Ollama​、gemini​、deepseek​、claude​、ark

  • 编排,支持Chain、Graph、Workflow编排

  • 核心组件,提供了构建一个高效AI Agent的全部能力

    • ChatModel: 聊天模型抽象,支持文本生成和流式输出
    • Tool: 工具调用抽象,支持函数调用和外部API集成
    • ChatTemplate: 聊天模板抽象,支持动态提示词生成
    • Retriever: 检索器抽象,支持向量检索和文档搜索
    • DocumentLoader: 文档加载器,支持多种数据源
    • Embedding: 嵌入模型抽象,支持文本向量化
    • Indexer: 索引器抽象,支持向量数据库操作

其模型集成主要通过chatModel来实现,例如

// init chat model
cfg := &openai.ChatModelConfig{
	APIKey:      "",
	BaseURL:     "",
	Model:       "",
	Temperature: 0.7,
	MaxTokens:   2048,
}
chatModel, err := openai.NewChatModel(ctx, cfg)
if err != nil {
	return nil, fmt.Errorf("failed to create chat model: %w", err)
}

eino回调能力 @

Eino 中的 Component 和 Graph 等实体,在固定的时机 (Callback Timing),回调用户提供的 function (Callback Handler),并把自己是谁 (RunInfo),以及当时发生了什么 (Callback Input & Output) 传出去

type Handler interface {
    OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context
    OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context
    OnError(ctx context.Context, info *RunInfo, err error) context.Context
    OnStartWithStreamInput(ctx context.Context, info *RunInfo,
       input *schema.StreamReader[CallbackInput]) context.Context
    OnEndWithStreamOutput(ctx context.Context, info *RunInfo,
       output *schema.StreamReader[CallbackOutput]) context.Context
}

例如通过compose.WithCallbacks 在 graph 运行时注入 Handler,这些 Handler 会在 graph 的本次运行整体上生效go

react agent的编排逻辑 @

image

react agent 底层使用 compose.Graph 作为编排方案,有 2 个节点: ChatModel、Tools,运行过程的所有历史消息都会放入 state 中。

在将所有历史消息传递给 ChatModel 之前,会 copy 消息交由 MessageModifier 进行处理,MessageModifier 会在每次把所有历史消息传递给 ChatModel 之前执行,处理的结果再传递给 ChatModel。

直到 ChatModel 返回的消息中不再有 tool call,则返回最终消息。

对于部分不支持toolCalls传递的模型,需要手动解析toolCalls参数,然后调用tool,并将结果添加进上下文(例如deepseek 就不支持)

eino框架主要role分为

  • assistant: agent回复问题的message
  • user:用户发起问题的message
  • system: 系统设定的prompt message
  • tool: 工具调用message

如果前一条消息的role为assistant 并且 toolCalls 大于0, 则后续一条消息的role必须为tool, 并且toolCallsId 与前一条消息的toolCalls关联

可以通过以下代码快速创建一个react agent

// init agent
agent, err := react.NewAgent(ctx, &react.AgentConfig{
	ToolsConfig:      compose.ToolsNodeConfig{Tools: tools},	// 可以提供mcp工具
	ToolCallingModel: chatModel,								// 之前创建的chatModel
	MaxStep:          20,
	MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {			// 模型被调用前的回调
		return input
	},
})
if err != nil {
	return nil, fmt.Errorf("failed to create agent: %w", err)
}

llm返回的内容可以通过流式和非流式接收

// 非流式
response, err := agent.Generate(ctx, messages)
if err != nil {
	return nil, fmt.Errorf("failed to generate initial response: %w", err)
}

// 流式
response, err := s.agent.Stream(ctx, messages)
if err != nil {
	return fmt.Errorf("failed to generate initial response: %w", err)
}
for {
	defer response.Close()
	content, rerr := response.Recv()
	if rerr == io.EOF {
		break
	}
	if rerr != nil {
		return fmt.Errorf("failed to receive response: %w", rerr)
	}
	if len(content.Content) > 0 {
		fmt.print(content.Content)
	}
}

tools工具集成 @

直接集成 @

eino框架可以直接集成tools,方便llm调用,接口如下

type InvokableRun func(ctx context.Context, arguments string, opts ...Option) (content string, err error)
type StreamableRun func(ctx context.Context, arguments string, opts ...Option) (content *schema.StreamReader[string], err error)

type BaseTool interface {
    Info() *schema.ToolInfo
}

// InvokableTool the tool for ChatModel intent recognition and ToolsNode execution.
type InvokableTool interface {
    BaseTool
    Run() InvokableRun
}

// StreamableTool the stream tool for ChatModel intent recognition and ToolsNode execution.
type StreamableTool interface {
    BaseTool
    Run() StreamableRun
}
调用mcp-server @

可以使用mcp—go编写的mcp-server来实现tools的调用
github.com/cloudwego/eino-ext/components/tool/mcp​提供了GetTools​的方法,便于将mcp-go​的tools​结构转换为eino​的tool结构

// 初始化mcp Client连接到mcp server
mcpClient, err := client.NewStdioMCPClient(
	config.MCP.ServerPath,
	config.MCP.ServerArgs,
)
if err != nil {
	return nil, fmt.Errorf("failed to create MCP client: %w", err)
}

initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
	Name:    "demo-mcp-client",
	Version: "1.0.0",
}
_, err = mcpClient.Initialize(ctx, initRequest)
if err != nil {
	return nil, fmt.Errorf("failed to initialize MCP client: %w", err)
}

// 获取mcp server的tools返回符合eino框架的tools结构
tools, err := mcp.GetTools(ctx, &mcp.Config{Cli: mcpClient})
if err != nil {
	return nil, fmt.Errorf("failed to get tools: %w", err)
}

文档索引 @

目前eino提供了相关接口,可以自己实现,也有封装好的组件,目前可以接入ES8​、Milvus​、Redis​、volc VikingDB​,对于retriever​还多提供了dify的支持

Embedding服务目前提供Ollama​、dashscope​、qianfan​(百度)、tencentcloud​(腾讯混元)、火山引擎Ark、OpenAI

indexer     indexer.Indexer     // rag文本向量索引器
retriever   retriever.Retriever // rag文本向量检索器
embedder    embedding.Embedder  // rag文本向量嵌入器

简单的可以使用redis存储文本数据

client := redis.NewClient(&redis.Options{
	Addr:     redisAddr,
	Protocol: 2, // 强制使用Redis协议2
	Password: password,
})

// 测试Redis连接
if err := client.Ping(ctx).Err(); err != nil {
	return nil, fmt.Errorf("redis连接失败: %v", err.Error())
}

// 初始化Embedder组件, 这个是我自己实现的,只需要实现其接口即可,也可以使用模型厂商提供的服务
// type Embedder interface {
//	 EmbedStrings(ctx context.Context, texts []string, opts ...Option) ([][]float64, error) // invoke
// }
embedder := NewSimpleEmbedder(1536)

idx, err := rindexer.NewIndexer(ctx, &rindexer.IndexerConfig{
	Client:    client,
	KeyPrefix: prefix + ":messages",
	Embedding: embedder,
})
if err != nil {
	return nil, fmt.Errorf("初始化索引器失败: %w", err)
}
// 创建检索器
ret, err := redisret.NewRetriever(ctx, &redisret.RetrieverConfig{
	Client:       client,
	Index:        prefix + ":messages",
	VectorField:  "vector",
	TopK:         10,
	ReturnFields: []string{"content", "conversation_id", "role", "user_id", "timestamp"},
	Embedding:    embedder,
	DocumentConverter: func(ctx context.Context, doc redis.Document) (*schema.Document, error) {
		return &schema.Document{
			ID:      doc.ID,
			Content: doc.Fields["content"],
			MetaData: map[string]any{
				"conversation_id": doc.Fields["conversation_id"],
				"role":            string(doc.Fields["role"]),
				"user_id":         doc.Fields["user_id"],
				"timestamp":       doc.Fields["timestamp"],
			},
		}, nil
	},
})
if err != nil {
	return nil, fmt.Errorf("初始化检索器失败: %w", err)
}

除此之外,目前eino也支持amazon s3、本地文件、网络URL 、HTML网页、PDF文件等文档加载,还提供了Bingsearch、Googlesearch、DuckDuckGoSearch等工具进行联网检索,由于文章篇幅有限,就不一一介绍了,感兴趣的朋友可以调研下