日习一技 - Coze Studio
type: channel
link: https://i-study-everyday.online/category/Coze-Studio/
description: Coze Studio 学习笔记
items: 学习 Coze Studio 的知识库检索逻辑
items: 学习 Coze Studio 的知识库入库逻辑（续）
items: 学习 Coze Studio 的知识库入库逻辑
items: 学习 Coze Studio 的工作流执行逻辑
items: 学习 Coze Studio 的工具使用
items: 再学 Coze Studio 的智能体执行逻辑
items: 学习 Coze Studio 的智能体执行逻辑
items: 学习 Coze Studio 的智能体会话接口
items: 学习 Coze Studio 的代码架构
items: 实战 Coze Studio 知识库使用

学习 Coze Studio 的知识库检索逻辑
type: item
date: 2025-08-20T00:55:00+00:00
description: 经过前面几天的学习，我们已经深入探讨了 Coze Studio 从知识库的创建到文档入库的完整流程。今天，我们将继续这一探索之旅，深入研究知识库的最后一个核心环节 —— 检索（Retrieval），看看在 Coze Studio 中，智能体是如何从知识库中精准高效地找到与用户问题最相关的信息的。Coze Studio 在这方面设计了一套颇为完善的检索流水线，它融合了查询重写、多路并行检索（向量、全文、NL2SQL）以及重排序等多种技术。下面就让我们一起看看它背后的实现细节。知识库检索配置在 Coze Studio 中，我们可以为智能体提供特定领域的背景知识，包括文本、表格和图片，让其可以回答领域内的问题：我们还可以对知识库进行一些配置，以便智能体更好地对知识库进行检索：这些配置参数包括：调用方式：分为 自动调用 和 按需调用 两种，自动调用指的是每次对话时都会发起一次检索，将检索结果放置在内置的系统提示词中，而按需调用则是将知识库检索封装成一个工具供智能体调用，只有当用户问题确实需要检索知识库时才触发调用。实际上，在开源版本中暂时只支持自动调用，我们之前在学习智能体执行逻辑时已经看到了，通过 Graph API 编排出来的智能体图，知识库检索是必然执行的节点之一，而且在源码中搜索 recallKnowledge 工具，也找不到其他地方用到；搜索策略：分为 语义、全文 和 混合 三种，语义表示基于向量的文本相关性查询，推荐在需要理解语义关联度和跨语言查询的场景使用，全文表示依赖于关键词的全文搜索，推荐在搜索具有特定名称、缩写词、短语或 ID 的场景使用，混合表示结合全文检索与语义检索的优势，并对结果进行综合排序。最大召回数量：从知识库中返回给大模型的最大段落数，注意默认是 1，可以适当调大一点；最小匹配度：根据设置的匹配度选取段落返回给大模型，低于设定匹配度的内容不会被召回；表格 SQL 查询：同时将查询的自然语言转为 SQL 语句进行查询，SQL 执行结果与 RAG 召回段落一同输入给模型；该选项仅对表格知识库有效；查询改写：结合对话上下文，对用户的问题进行改写，使得改写后的查询更适合检索；结果重排：通过分析用户查询的意图，对召回结果重新排序，使得最相关的内容排在前面；知识库检索流程回顾之前学习的智能体执行逻辑，其中有一路负责知识库的检索：// 新建知识库检索器 kr, err := newKnowledgeRetriever(ctx, &retrieverConfig{ knowledgeConfig: conf.Agent.Knowledge, }) // 第一个节点检索 _ = g.AddLambdaNode(keyOfKnowledgeRetriever, compose.InvokableLambda[*AgentRequest, []*schema.Document](kr.Retrieve), compose.WithNodeName(keyOfKnowledgeRetriever)) // 第二个节点组装成字符串 _ = g.AddLambdaNode(keyOfKnowledgeRetrieverPack, compose.InvokableLambda[[]*schema.Document, string](kr.PackRetrieveResultInfo), compose.WithOutputKey(placeholderOfKnowledge), )首先新建一个知识库检索器，将其 Retrieve() 方法作为第一个节点，知识库检索后，该方法返回 []*schema.Document 文档列表，因此还需要第二个节点，调用 PackRetrieveResultInfo 将其组装成字符串，替换系统提示词模版中的 {{ knowledge }} 占位符。可以看出只要智能体关联了知识库，用户每次对话时总是会触发一次知识库检索，检索的逻辑位于 knowledge 领域层，如下：func (k *knowledgeSVC) Retrieve(ctx context.Context, request *RetrieveRequest) (response *RetrieveResponse, err error) { // 检索上下文 retrieveContext, err := k.newRetrieveContext(ctx, request) // 查询重写 rewriteNode := compose.InvokableLambda(k.queryRewriteNode) // 向量化召回 vectorRetrieveNode := compose.InvokableLambda(k.vectorRet…

学习 Coze Studio 的知识库入库逻辑（续）
type: item
date: 2025-08-19T04:33:00+00:00
description: 书接上文，当用户在知识库上传文档、表格或图片，然后对知识库进行设置和确认后，Coze Studio 通过发送 IndexDocuments 事件开始了文档的异步处理。这个事件被发送到消息队列后，由 knowledge 领域的事件处理器消费，我们今天就来看下这块的逻辑。事件处理器事件处理器的代码位于 backend/domain/knowledge/service/event_handle.go 文件，这是一个 switch 结构，负责分发知识库相关的各类事件，比如批量文档处理（IndexDocuments）、单个文档处理（IndexDocument）、修改文档分片（IndexSlice）、删除知识库数据（DeleteKnowledgeData）、文档预览（DocumentReview）等：func (k *knowledgeSVC) HandleMessage(ctx context.Context, msg *eventbus.Message) (err error) { event := &entity.Event{} err = sonic.Unmarshal(msg.Body, event) // 事件处理器 switch event.Type { case entity.EventTypeIndexDocuments: // 批量文档处理 k.indexDocuments(ctx, event) case entity.EventTypeIndexDocument: // 单个文档处理 k.indexDocument(ctx, event) case entity.EventTypeIndexSlice: // 修改文档分片 k.indexSlice(ctx, event) case entity.EventTypeDeleteKnowledgeData: // 删除知识库数据 k.deleteKnowledgeDataEventHandler(ctx, event) case entity.EventTypeDocumentReview: // 文档预览 k.documentReviewEventHandler(ctx, event) } return nil }其中批量文档处理（IndexDocuments）事件的逻辑很简单，就是遍历所有文档，为每个文档单独发送一个单文档处理（IndexDocument）事件。文档处理流程单文档处理（IndexDocument）事件的代码位于 indexDocument() 函数，这个处理函数是整个知识库入库流程的核心，其执行流程可分为以下几个关键步骤：状态检查与清理：首先检查知识库和文档是否处于可写入状态，防止并发操作导致数据不一致；如果不是追加模式，会先清除该文档在数据库（knowledge_document_slice 表）和向量数据库中的旧数据；解析与分片：将文档状态设置为分片中（Chunking）；从对象存储（TOS）下载文档的原始文件；根据文档类型（文本、图片、表格）获取对应的解析器（Parser），对文件内容进行解析和分片，生成一系列文档分片（schema.Document）；数据持久化：为每个文档分片生成唯一的 ID；将文档分片批量存入 knowledge_document_slice 数据表中；对于表格类型，还会将解析出的结构化数据行插入到之前动态创建的物理数据表中；向量化与索引：将持久化后的文档分片转换为向量数据库要求的格式；调用 searchstore 的 Store 方法，将文档分片进行向量化，并存入 Milvus 等向量数据库中。这个过程会根据文档 ID 进行分区（Partition），便于后续的高效检索和管理；状态更新：所有文档分片成功存入向量数据库后，将其在 knowledge_document_slice 表中的状态更新为 Done；最后，将 knowledge_document 表中对应文档的状态更新为 Enable，表示该文档已处理完成并可供检索；接下来，我们详细看看每一步的实现逻辑。状态检查与清理// 检查知识库和文档状态 if valid, err := k.isWritableKnowledgeAndDocument(ctx, doc.KnowledgeID, doc.ID); err != nil { return err } else if !valid { return errorx.New(errno.ErrKnowledgeNonRetryableCode, ...) } // 清除旧数据 collectionName := getCollectionName(doc.KnowledgeID) if !doc.IsAppend …

学习 Coze Studio 的知识库入库逻辑
type: item
date: 2025-08-18T00:04:00+00:00
description: 经过前面几天的学习，我们已经深入探讨了 Coze Studio 的智能体、插件、工作流等核心功能的实现原理。今天，我们将继续探索 Coze Studio 的知识库功能，学习当我们在前端页面上点击 “创建知识库” 并上传文档时，后端服务是如何处理这些入库请求的。知识库创建接口我们知道，创建知识库是构建 RAG 应用的第一步，Coze Studio 支持 文本、表格 和 图片 三种不同类型的知识库：这三种知识库的创建流程基本类似，都分为两步：输入名称、描述、图标和导入类型，点击 “完成创建” 或 “创建并导入”，这一步调用 /api/knowledge/create 接口，创建的知识库实例对应 knowledge 数据表；上传文档、表格或图片，对知识库进行设置，预览，然后开始对文档进行处理，这一步调用 /api/knowledge/document/create 接口，上传的文档对应 knowledge_document 数据表；下面我们着重看下第二个接口的实现逻辑，接口层代码位于 backend/api/handler/coze/knowledge_service.go 文件：// @router /api/knowledge/document/create [POST] func CreateDocument(ctx context.Context, c *app.RequestContext) { // 绑定入参 var req dataset.CreateDocumentRequest err = c.BindAndValidate(&req) // 调用 knowledge 应用层 resp := new(dataset.CreateDocumentResponse) resp, err = application.KnowledgeSVC.CreateDocument(ctx, &req) c.JSON(consts.StatusOK, resp) }然后，调用 knowledge 应用层：func (k *KnowledgeApplicationService) CreateDocument(ctx context.Context, req *dataset.CreateDocumentRequest) (*dataset.CreateDocumentResponse, error) { // 调用 knowledge 领域层 resp := dataset.NewCreateDocumentResponse() createResp, err := k.DomainSVC.CreateDocument(ctx, &service.CreateDocumentRequest{ Documents: documents, }) return resp, nil }紧接着，做一些参数的转换，再调用 knowledge 领域层，这里是文档处理的核心：func (k *knowledgeSVC) CreateDocument(ctx context.Context, request *CreateDocumentRequest) (response *CreateDocumentResponse, err error) { // 根据不同的知识库类型，创建对应的文档处理器 docProcessor := impl.NewDocProcessor(ctx, &impl.DocProcessorConfig{ ... }) // 1. 前置的动作，上传 tos 等 err = docProcessor.BeforeCreate() // 2. 构建 落库 err = docProcessor.BuildDBModel() // 3. 插入数据库 err = docProcessor.InsertDBModel() // 4. 发起索引任务 err = docProcessor.Indexing() // 5. 返回处理后的文档信息 docs := docProcessor.GetResp() return &CreateDocumentResponse{ Documents: docs, }, nil }文档处理器Coze Studio 使用工厂模式根据文档来源和类型创建不同的文档处理器：customDocProcessor: 自定义内容 + 非表格类型customTableProcessor: 自定义内容 + 表格类型baseDocProcessor: 本地文件 + 非表格类型 或 默认localTableProcessor: 本地文件 + 表格类型其中，本地文件指的是用户上传文件，自定义内容指的是手动录入文档内容。所有处理器…

学习 Coze Studio 的工作流执行逻辑
type: item
date: 2025-08-15T07:33:00+00:00
description: 今天，我们来学习下 Coze Studio 中工作流的执行逻辑。我们知道，Eino 框架支持以 编排（Compose） 的方式对各种原子组件进行组合和串联，并且它支持 图（Graph）、链（Chain） 和 工作流（Workflow） 三种编排方式。其中，链基于图实现，是最简单的；工作流和图处于同一级别，他们之间的区别在于，工作流提供了字段级别的映射能力以及控制流与数据流分离等特性。在 Coze Studio 中，智能体是通过 Graph API 实现的，工作流是通过 Workflow API 实现的。工作流 vs. 图工作流和图的区别在于，它提供了字段级别的映射能力，节点的输入可以由任意前驱节点的任意输出字段组合而成。比如下面这个例子：假设 f1 和 f2 是两个现有的业务函数，f1 的输出为 F1 和 F2，而 f2 的输入为 F3，很显然两者并不匹配，如果使用 Graph API 编排，由于类型对齐的要求，我们有两种做法：定义一个公共结构体，将 f1 的输出类型和 f2 的输入类型改成这个公共结构体。不仅有开发成本，而且对业务逻辑造成了入侵；将 f1 的输出类型和 f2 的输入类型都改成 map。丢失了强类型对齐的特性。这时就可以使用 Workflow API 编排，将 f1 的输出字段 F1 直接映射到 f2 的输入字段 F3，同时保留 f1 和 f2 的原始函数签名。每个节点由业务场景决定输入输出，不需要考虑谁给我输入，以及谁用我的输出。工作流的另一个特点是控制流与数据流分离，看下面这个场景：节点 D 同时引用了 A、B、C 的某些输出字段。其中 A->D 的这条虚线，是单纯的 数据流，不传递 控制 信息，即 A 执行完成与否，不决定 D 是否开始执行。节点 D 到 E 之间的粗箭头，代表节点 E 不引用节点 D 的任何输出，是单纯的 控制流，不传递 数据，即 D 执行完成与否，决定 E 是否开始执行，但是 D 的输出不影响 E 的输入。图中其他的线，是控制流与数据流合一的。需要注意的是，数据流能传递的前提，是一定有一条控制流存在，比如 A->D 的数据流，依赖 A->branch->B->D 或者 A->branch->C->D 的控制流存在。即数据流只能引用前驱节点的输出。学习 Workflow API下面我们通过一个简单例子，演示下 Workflow API 的基本用法。假设我们现在有一个现成的业务函数 add()，实现 A 和 B 两个整数的相加：type AddParam struct { A int B int } func add(ctx context.Context, param AddParam) (int, error) { return param.A + param.B, nil }我们要使用这个函数，编排出一个任务链，实现 X+Y+Z 三个整数的加法：type Request struct { X int Y int Z int }示例代码如下：func main() { // 创建工作流 wf := compose.NewWorkflow[Request, int]() // 第一个 add 节点 wf.AddLambdaNode("add1", compose.InvokableLambda(add)). AddInput( compose.START, compose.MapFields("X", "A"), compose.MapFields("Y", "B"), ) // 第二个 add 节点 wf.AddLambdaNode("add2", compose.InvokableLambda(add)). AddInput(compose.START, compose.MapFields("Z", "A")). AddInput("add1", compose.ToField("B")) wf.End().AddInput("add2") // 编译工作流 run, err := wf.Compile(context.Background()) if err != nil { panic(err) } // 调用工作流 result, _ := run.Invoke(context.Background(), Request{ X: 1, Y: 2, Z: 3, }) println(result) }Workflow API 和 Graph API 的语法类似，整个编排流程还是很清晰的：使用 compose.NewWorkflow() 创建工作流，和 compose.NewGraph() 基本一致；使用 AddLambdaNode() 将用户自定义函数添加为工作流节点，除此之外，…

学习 Coze Studio 的工具使用
type: item
date: 2025-08-14T07:23:00+00:00
description: 昨天，我们详细拆解了 Coze Studio 智能体的核心执行逻辑，了解到其本质是一个基于 Eino 框架构建的智能体图。在分析的最后我们提到，其中的一些关键实现细节，例如 ReAct Agent 与工具的交互逻辑，仍有待深入。今天，我们就来填上这个坑，学习下 Coze Studio 是如何实现工具调用的。我们知道，Eino 提供了很多可复用的原子组件，通过 Graph API 可以对其进行编排，实现出复杂的业务逻辑。不过有些业务逻辑存在一定的通用性，Eino 将这些场景进行抽象，提供了一些构建大模型应用的模式，被称为 Flow。目前 Eino 提供了两种常用的 Agent 模式：ReAct AgentMulti Agent除此之外还提供了一些常用的召回策略，比如 MultiQueryRetriever 和 ParentIndexer 等。ReAct Agent我们今天主要关注 ReAct Agent 部分。ReAct 这个词出自 Shunyu Yao 等人的这篇论文 《ReAct: Synergizing Reasoning and Acting in Language Models》，它是由 Reason 和 Act 两个词组合而成，表示一种将 推理 和 行动 与大模型相结合的通用范式：传统的 Reason Only 型应用（如 Chain-of-Thought Prompting）具备很强的语言能力，擅长通用文本的逻辑推断，但由于不会和外部环境交互，因此它的认知非常受限；而传统的 Act Only 型应用（如 WebGPT、SayCan、ACT-1）能和外界进行交互，解决某类特定问题，但它的行为逻辑较简单，不具备通用的推理能力。ReAct 的思想，旨在将这两种应用的优势结合起来。针对一个复杂问题，首先使用大模型的推理能力制定出解决该问题的行动计划，这好比人的大脑，可以对问题进行分析思考；然后使用行动能力与外部源（例如知识库或环境）进行交互，以获取额外信息，这好比人的五官和手脚，可以感知世界，并执行动作；大模型对行动的结果进行跟踪，并不断地更新行动计划，直到问题被解决。通过这种模式，我们能基于大模型构建更为强大的 AI 应用，可以阅读原始论文了解更多关于 ReAct 的信息：https://react-lm.github.io/回顾 Function Call 原理原始的 ReAct 论文是通过 提示工程（Prompt Engineering） 实现的。后来，OpenAI 对 GPT 模型进行了一项重大更新，推出了 Function Call 功能，在 Chat Completions API 中添加了函数调用能力，这一功能推出后，迅速风靡世界，开发者可以通过 API 的方式实现类似于 ChatGPT 插件的数据交互能力，将大模型集成到自己的业务和应用中。随后，越来越多的大模型也都开始支持 Function Call 功能，目前已经成为大模型调用工具的事实规范。简单来说，Function Call 就是在调用大模型时提供几个工具选项，大模型判断用户的问题是否需要调用某个工具。如果不需要则直接回复，这和传统的调用没有区别；如果需要调用则返回合适的工具和对应的参数给用户，用户拿到后调用对应的工具，再将调用结果送给大模型，最后，大模型根据工具的调用结果来回答用户的问题。OpenAI 官方文档 中有一张详细的流程图：其中要注意的是，第二次调用大模型时，可能仍然会返回 tool_calls 响应，这时可以循环处理。定义 Eino 工具了解了 ReAct Agent 的原理后，接下来，我们再来看下 Eino 中 ReAct Agent 的用法。为了让大模型能调用我们的工具，我们需要使用 Eino 提供的 ToolsNode 组件，该组件提供了三个层次的接口：// 基础工具接口，提供工具信息，包括工具名称、描述和参数等 type BaseTool interface { Info(ctx context.Context) (*schema.ToolInfo, error) } // 可调用的工具接口，支持同步调用 type InvokableTool interface { BaseTool InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error) } // 支持流式输出的工具接口 type StreamableTool interface { BaseTool StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*s…

再学 Coze Studio 的智能体执行逻辑
type: item
date: 2025-08-13T07:09:00+00:00
description: 我们昨天粗略学习了 Coze Studio 的智能体执行逻辑，了解到其核心是基于 Eino 框架编排的一个智能体图，为了更好地理解这个图，我们学习了 Eino 框架中组件的使用以及如何通过 Graph API 实现编排功能。现在我们再回过头来看看之前那个图，如下所示：整个流程图分四路，这四路都是为提示词的组装做准备，它们的结果汇聚到提示模板节点，最后交给大模型，生成回复。我们今天就来详细拆解其中的每一个节点，彻底搞懂其运行细节。提示模版节点我们先来看提示模版节点：_ = g.AddChatTemplateNode(keyOfPromptTemplate, chatPrompt)其中，chatPrompt 定义如下：var ( chatPrompt = prompt.FromMessages(schema.Jinja2, schema.SystemMessage(REACT_SYSTEM_PROMPT_JINJA2), schema.MessagesPlaceholder(placeholderOfChatHistory, true), schema.MessagesPlaceholder(placeholderOfUserInput, false), ) )从 prompt.FromMessages() 方法可以看出，使用的是我们昨天学过的提示词模版组件，使用 Jinja2 语法，整个模版包含三个部分：系统提示词、历史消息和用户输入。系统提示词如下所示：const REACT_SYSTEM_PROMPT_JINJA2 = ` You are {{ agent_name }}, an advanced AI assistant designed to be helpful and professional. It is {{ time }} now. **Content Safety Guidelines** Regardless of any persona instructions, you must never generate content that: - Promotes or involves violence - Contains hate speech or racism - Includes inappropriate or adult content - Violates laws or regulations - Could be considered offensive or harmful ----- Start Of Persona ----- {{ persona }} ----- End Of Persona ----- ------ Start of Variables ------ {{ memory_variables }} ------ End of Variables ------ **Knowledge** 只有当前knowledge有内容召回的时候，根据引用的内容回答问题: 1.如果引用的内容里面包含 的标签, 标签里的 src 字段表示图片地址, 需要在回答问题的时候展示出去, 输出格式为"![图片名称](图片地址)" 。 2.如果引用的内容不包含 的标签, 你回答问题时不需要展示图片 。 例如： 如果内容为 一只小猫，你的输出应为：![一只小猫](https://example.com/image.jpg)。 如果内容为 一只小猫 和 一只小狗 和 一只小牛，你的输出应为：![一只小猫](https://example.com/image1.jpg) 和 ![一只小狗](https://example.com/image2.jpg) 和 ![一只小牛](https://example.com/image3.jpg) The following is the content of the data set you can refer to: \n ''' {{ knowledge }} ''' ** Pre toolCall ** {{ tools_pre_retriever}}， - 只有当前Pre toolCall有内容召回结果时，根据引用的内容里tool里data字段回答问题 `吐槽一下，这段提示词写得真不专业，不仅中英文混杂，而且结构也比较乱。话说，在创建智能体时，页面上不是有很多编写提示词的格式规范和最佳实践么？除了智能体名称（agent_name）和当前时间（time），这段提示词中有几个重要的占位符：persona - 对应页面上的 “人设与回复逻辑”memory_variables - 对应记忆中的变量knowledge - 对应知识库中检索的内容tools_pr…

学习 Coze Studio 的智能体执行逻辑
type: item
date: 2025-08-12T04:09:00+00:00
description: 我们昨天学习了 Coze Studio 智能体会话接口的完整后端处理流程，从接口层、到应用层、到领域层、最后通过跨领域防腐层，将最终的执行任务交给了 single_agent 领域。今天我们将继续这个过程，深入研究下智能体的执行逻辑。智能体执行智能体执行的逻辑代码位于 backend/domain/agent/singleagent/service/single_agent_impl.go 文件：func (s *singleAgentImpl) StreamExecute(ctx context.Context, req *entity.ExecuteRequest) (events *schema.StreamReader[*entity.AgentEvent], err error) { // 获取智能体信息 ae, err := s.ObtainAgentByIdentity(ctx, req.Identity) // 构建智能体图 conf := &agentflow.Config{ Agent: ae, UserID: req.UserID, Identity: req.Identity, ModelMgr: s.ModelMgr, ModelFactory: s.ModelFactory, CPStore: s.CPStore, } rn, err := agentflow.BuildAgent(ctx, conf) // 执行智能体图 exeReq := &agentflow.AgentRequest{ UserID: req.UserID, Input: req.Input, History: req.History, Identity: req.Identity, ResumeInfo: req.ResumeInfo, PreCallTools: req.PreCallTools, } return rn.StreamExecute(ctx, rn.PreHandlerReq(ctx, exeReq)) }首先根据配置信息构建智能体图，这里引入了 图（Graph） 这个新概念，Coze Studio 通过 Eino 智能体框架，将智能体编排成一个有向图，构建的大体流程如下：func BuildAgent(ctx context.Context, conf *Config) (r *AgentRunner, err error) { // 初始化节点... // 创建一个新图 g := compose.NewGraph[*AgentRequest, *schema.Message]( compose.WithGenLocalState(func(ctx context.Context) (state *AgentState) { return &AgentState{} })) // 向图中添加节点... // 连接节点之间的边... // 图编译 runner, err := g.Compile(ctx, opts...) // 返回图 return &AgentRunner{ runner: runner, }, nil }这个智能体图比较复杂，方法中充斥着大量节点初始化以及向图中添加节点和边的代码，此处做了省略，暂时只要关注 compose.NewGraph() 以及 g.Compile() 两行代码，这是使用 Eino 创建并编译智能体的常见方式，构建完成的智能体图如下所示：这个图结构体现了完整的智能体处理流水线，从输入预处理到最终响应生成：并行初始化：从开始节点同时触发 4 个输入处理节点，并行处理提高效率；汇聚处理：所有预处理结果汇聚到提示模板节点；条件分支：根据是否有工具选择不同的 Agent 处理方式，如果有工具，使用 ReAct Agent，否则直接调大模型；问题建议：根据配置决定是否启用建议功能；最后，完成图的编译后，在 StreamExecute() 方法中通过 runner.Stream() 流式调用它：func (r *AgentRunner) StreamExecute(ctx context.Context, req *AgentRequest) (sr *schema.StreamReader[*entity.AgentEvent], err error) { // 回调处理器 hdl, sr, sw := newReplyCallback(ctx, executeID.String()) go func() { // 注册回调 var composeOpts []compose.Option composeOpts = append(composeOpts, compose.…

学习 Coze Studio 的智能体会话接口
type: item
date: 2025-08-11T06:19:00+00:00
description: 前面我们已经学习了 Coze Studio 的代码架构，对项目整体有了一个大致的了解。今天，我们将深入智能体执行的核心，研究下用户在和智能体对话时，后端服务是如何处理会话请求的。接口层实现我们首先来看下智能体会话的接口层实现。正如上一篇所述，Coze Studio 中的接口均通过 IDL 定义，位于 idl/conversation/agentrun_service.thrift 文件：service AgentRunService { run.AgentRunResponse AgentRun(1: run.AgentRunRequest request) ( api.post='/api/conversation/chat', api.category="conversation", api.gen_path= "agent_run" ) run.ChatV3Response ChatV3(1: run.ChatV3Request request)( api.post = "/v3/chat", api.category="chat", api.tag="openapi", api.gen_path="chat" ) }这里还有一个 /v3/chat 接口，它是正式发布的智能体对外的 API 接口，其实现和 /api/conversation/chat 基本一致，这里不作过多介绍。然后使用 hz 工具将 IDL 自动生成 API 处理器，位于 backend/api/handler/coze/agent_run_service.go 文件：// @router /api/conversation/chat [POST] func AgentRun(ctx context.Context, c *app.RequestContext) { // 绑定并校验入参 var req run.AgentRunRequest c.BindAndValidate(&req) // 新建 SSE 发送器 sseSender := sseImpl.NewSSESender(sse.NewStream(c)) c.SetStatusCode(http.StatusOK) c.Response.Header.Set("X-Accel-Buffering", "no") // 调用 conversation 应用服务 conversation.ConversationSVC.Run(ctx, sseSender, &req) }API 接口层没有什么复杂的逻辑，主要是绑定并校验入参，然后调用应用层。这里值得一提是 X-Accel-Buffering 响应头的使用，这是一个特殊的 HTTP 响应头，主要用于控制反向代理（如 Nginx）的缓冲行为，当它的值被设置为 no 时，表示告诉代理服务器不要对当前响应进行缓冲。如果没有这个设置，Nginx 等代理可能会缓冲响应内容直到缓冲区填满或响应完成，这会导致客户端无法实时获取数据，产生延迟感。这个设置通常用于需要 流式传输 的场景，比如：实时日志输出、大型文件下载、SSE 服务器推送等。应用层实现接着再来看下应用层的实现，位于 backend/application/conversation/agent_run.go 文件：func (c *ConversationApplicationService) Run(ctx context.Context, sseSender *sseImpl.SSenderImpl, ar *run.AgentRunRequest) error { // 从当前会话中获取用户 ID userID := ctxutil.MustGetUIDFromCtx(ctx) // 如果 RegenMessageID > 0 说明是重新生成，将对应的运行记录和消息删除 if ar.RegenMessageID != nil && ptr.From(ar.RegenMessageID) > 0 { msgMeta, err := c.MessageDomainSVC.GetByID(ctx, ptr.From(ar.RegenMessageID)) if msgMeta != nil { err = c.AgentRunDomainSVC.Delete(ctx, []int64{msgMeta.RunID}) delErr := c.MessageDomainSVC.Delete(ctx, &msgEntity.DeleteMeta{ RunIDs: []int64{msgMeta.RunID}, }) } } // 查询 Agent 信息 agentInfo, caErr := c.c…

学习 Coze Studio 的代码架构
type: item
date: 2025-08-08T06:23:00+00:00
description: 经过几天的实战和学习，我们已经全面体验了 Coze Studio 从智能体、插件、工作流到知识库的各项核心功能。今天，我们开始研究下它的源码，看看这些功能背后的实现原理。项目架构Coze Studio 的架构设计严格遵循 领域驱动设计（DDD） 的核心原则进行构建，我们可以看下它的整体项目结构：├── backend/ # 后端服务 │ ├── api/ # API 处理器和路由 │ ├── application/ # 应用层，组合领域对象和基础设施实现 │ ├── conf/ # 配置文件 │ ├── crossdomain/ # 跨领域防腐层 │ ├── domain/ # 领域层，包含核心业务逻辑 │ ├── infra/ # 基础设施实现层 │ ├── pkg/ # 无外部依赖的工具方法 │ └── types/ # 类型定义 ├── common/ # 公共组件 ├── docker/ # Docker 配置 ├── frontend/ # 前端应用 │ ├── apps/ # 应用程序 │ ├── config/ # 配置文件 │ ├── infra/ # 基础设施 │ └── packages/ # 包 ├── idl/ # 接口定义语言文件我们主要关注 backend 目录下的内容，从子目录的名称可以很明显看出是 DDD 的分层架构：API 层（api）：实现 HTTP 端点，使用 Hertz 服务器处理请求和响应，包含中间件组件；应用层（application）：​组合各种领域对象和基础设施实现，提供 API 服务；领域层（domain）：包含核心业务逻辑，定义领域实体和值对象，实现业务规则和工作流；跨领域防腐层（crossdomain）：​定义跨领域接口，防止领域间直接依赖；基础设施层（infra）：又分为契约层和实现层；契约层（contract） ​定义所有外部依赖的接口，作为领域逻辑和基础设施之间的边界，包括存储系统、缓存机制、消息队列、配置管理等接口；实现层（impl） 为契约层定义的接口提供具体的实现；工具包（pkg）：无外部依赖的工具方法，可以被任何层直接使用；领域驱动设计领域驱动设计（Domain-Driven Design，简称 DDD） 是一种针对复杂业务系统的软件开发方法论，它的核心思想是 以业务领域为中心，软件的设计和实现都围绕业务领域的核心概念、规则和流程展开，而非单纯技术架构。通过抽象业务领域中的实体、关系和规则，构建 领域模型（Domain Model），并将模型映射为代码，使代码既能反映业务逻辑，又能被业务人员理解。Eric Evans 在 2004 年出版了《领域驱动设计》一书，提出了经典的 DDD 4 层架构：我们可以在 backend/domain 目录下找到 Coze Studio 的所有领域模型：├── agent # 智能体，只有单智能体 ├── app # 应用 ├── connector # 连接器，Chat SDK 或 API ├── conversation # 会话 ├── datacopy # 数据复制任务 ├── knowledge # 知识库 ├── memory # 记忆，包括数据库和变量 ├── openauth # 认证 ├── permission # 权限 ├── plugin # 插件和工具 ├── prompt # 提示词 ├── search # 搜索 ├── shortcutcmd # 快捷指令 ├── template # 模板 ├── upload # 一些默认图标的常量 ├── user # 用户 └── workflow # 工作流这里除了领域模型，还定义了对应的实体、值对象、聚合和领域服务等核心领域对象：实体（Entity）：有唯一标识、状态可变的对象，其身份比属性更重要；值对象（Value Object）：无唯一标识、不可变的对象，由属性定义（属性相同则视为相等）；聚合（Aggregate）：一组紧密关联的实体和值对象的集合，通过 聚合根（Aggregate Root） 对外暴露接口，保证数据一致性；领域服务（Domain Service）：封装跨实体/聚合的业务逻辑，无法归属到单个实体时使用；领域事件（Domain Event）：领域中发生的重要事件，用于解耦跨上下文的业务流程；仓储（Repository）：封装数据持久化逻辑，为领域模型提供数据访问接口（屏蔽数据库细节）；限界上下文（Bounded Context）：领域模型的边界，每个上下文内有独立的模型和通用语言，上下文间通过接口通信；限界上下文可作为微服务拆分的依据，每个微服务对应一个或多个限界上下文，降低服务间耦合；在 DDD 开发中，往往还会引入了一个跨领域…

实战 Coze Studio 知识库使用
type: item
date: 2025-08-07T02:05:00+00:00
description: 昨天我们学习了 Coze Studio 的工作流功能，通过在可视化画布上拖拽节点，迅速编排和搭建复杂的工作流。今天，我们将继续探索 Coze 的另一个核心功能 —— 知识库，学习如何为智能体注入私有或专业领域的知识，解决大模型幻觉和专业领域知识不足的问题。创建知识库大模型虽然知识渊博，但其知识截止于训练日期，并且对于私有或专业领域的知识一无所知。知识库功能正是为了解决这个问题而生的，它通过 RAG 技术，让智能体能够基于我们提供的专属资料库来回答问题。我们首先进入 “资源库” 页面，点击 “+ 资源” 并选择 “知识库”，创建知识库：Coze Studio 提供了三种不同类型的知识库：文本格式：文本知识库支持用户上传 PDF、TXT、DOC、DOCX、MD 等格式的文件，解析和分片后存储于向量数据库；它基于内容片段进行检索和召回，大模型结合召回的内容生成最终内容回复，适用于知识问答等场景；表格格式：表格知识库支持用户上传 CSV 和 XLSX 文件，导入表格数据，按行进行划分；它基于索引列的匹配进行检索，同时也支持基于 NL2SQL 的查询和计算；照片类型：照片知识库支持用户上传 JPG、JPEG 和 PNG 图片，通过大模型对图片进行标注；它基于标注信息的相似度匹配，找到与用户问题最相关的图片，给大模型用于内容生成；Coze Studio 的知识库功能依赖于 Embedding 服务和向量化存储组件。其中 Embedding 服务支持三种接入方式：OpenAI - 兼容 OpenAI 协议的 Embedding 接口ARK - 火山引擎提供的 Embedding 服务HTTP - 调用本地部署的模型服务，需要满足 Coze 自己的一套 接口协议，暂不支持 Ollama 或 Xinference 协议这里我使用的是 OpenAI 的 text-embedding-3-small 模型，下面是 .env 配置示例，注意它的向量维度为 1536：export EMBEDDING_TYPE="openai" export OPENAI_EMBEDDING_BASE_URL="" export OPENAI_EMBEDDING_MODEL="text-embedding-3-small" export OPENAI_EMBEDDING_API_KEY="sk-xxx" export OPENAI_EMBEDDING_BY_AZURE=false export OPENAI_EMBEDDING_API_VERSION="" export OPENAI_EMBEDDING_DIMS=1536 export OPENAI_EMBEDDING_REQUEST_DIMS=1536另外，向量化存储组件支持开源向量数据库 Milvus 和火山向量数据库 VikingDB，默认使用 Milvus，一般不修改：export VECTOR_STORE_TYPE="milvus" export MILVUS_ADDR="localhost:19530"如果你想切换成 VikingDB，可以使用下面的配置：export VECTOR_STORE_TYPE="vikingdb" export VIKING_DB_HOST="" export VIKING_DB_REGION="" export VIKING_DB_AK="" export VIKING_DB_SK="" export VIKING_DB_SCHEME="" export VIKING_DB_MODEL_NAME=""由于修改的是环境变量，通过 --force-recreate 重启服务：$ docker compose --profile '*' up -d --force-recreate --no-deps coze-server文本知识库创建知识库时选择 “文本格式”，并填写知识库名称和描述，导入类型选 “本地文档”，然后点击 “创建并导入” 按钮，进入 “新增知识库” 的 “上传” 页面：如果导入类型选 “自定义”，则进入 “新增知识库” 的 “文本填写” 页面，支持用户手动录入文档名称和文档内容。我们上传一个简单的 PDF 文件（可以包含文本、表格、图片等元素），进入 “创建设置” 页面：和其他的 RAG 系统类似，我们需要为知识库配置 文档解析策略 和 分段策略。Coze Studio 支持两种文档解析策略：精准解析：支持从文档中提取图片、表格等元素，需要耗费更长的时间；支持设置过滤内容，可以将一些特定的页面排除掉；快速解析：不会对文档提取图像、表格等元素，适用于纯文本；如果上传的文件是扫描件，往往需要开启 OCR 功能，Coze Studio 目前只支持火山引擎的 通用 OCR …