[{"content":"之后文章会这样组织： 某个系列的文章会放在一个 category 中 单篇文章属于多个领域时都会打上 tag\n","permalink":"https://rsc-blog.pages.dev/posts/2026/readme/","summary":"Readme","title":"Readme"},{"content":"博客网站 Jimmy Song：https://jimmysong.io/zh/ openai：https://developers.openai.com/blog Anthropic技术博客：https://www.anthropic.com/engineering Cursor技术博客：https://cursor.com/cn/blog Langchain技术博客：https://blog.langchain.com/\n好文 https://news.qq.com/rain/a/20260402A04RFB00\n图片处理 图片压缩转换 https://imageconverter.com/ 资源导航网站 https://www.jspoo.com/ ","permalink":"https://rsc-blog.pages.dev/posts/2026/awesome-free-websites/","summary":"好网站","title":"Awesome Websites 好网站"},{"content":"OncallAgent 是一个本地优先的 AIOps Agent 工作台。它把 Vue 3 工作区、FastAPI API 与 Agent 运行时、SQLite 持久化、Milvus 知识向量、Qwen 模型能力和外部 MCP 工具放进一条可在开发者机器上运行的链路。这里的“本地优先”不是“所有计算都离线”：应用进程和主要状态边界位于本机，但模型服务、CLS MCP 所连接的日志服务仍可能是远端依赖。理解这一点，才能正确解释 readiness 降级、网络失败和凭据配置。\n📷 [图片 token=DFZOb06q2oyLklxYCaOc5Fotnef（未能下载，见飞书原文）]\n从 AI Native 角度看，这套代码最值得学习的不是某一个模型调用，而是模型如何被约束在工程边界内：HTTP 与 SSE 有共享契约，身份经过统一依赖解析，业务记录通过 Repository 持久化，知识检索必须带 tenant 范围，工具调用与诊断结论可以落到审计和证据链。Agent 只是调用链中的一个执行者，不是绕过权限、存储和错误处理的特殊通道。\n📷 [图片 token=SXLfbdkb6ofAAJx2SWicTsF2nPg（未能下载，见飞书原文）]\n源码阅读应同时沿“控制面”和“运行面”展开。OpenSpec 主规格描述已经生效的行为，packages/api-contracts 固化跨语言协议；运行时则从前端路由进入 FastAPI，再进入服务、Repository、模型或外部工具。单独阅读任意一层都容易产生误判，例如看到 Milvus collection 并不代表所有业务数据都在那里，看到模型 provider 也不代表启动时必然发起网络连接。\n📷 [图片 token=QxuRbBEQnoIQAVx45Lac9qKAnKk（未能下载，见飞书原文）]\n学习目标 📷 [图片 token=Qq15b0z3HoGrDUxqGWvcKYh4nfc（未能下载，见飞书原文）]\n识别浏览器、API、Agent、SQLite、Milvus、Qwen、MCP 与本地容器基础设施的职责边界。\n能从一个用户操作追踪到共享契约、后端路由、服务、持久化与外部依赖。\n理解为什么 owner scope、统一错误、显式连接和 durable job 是 AI Native 系统的基础设施，而不是附加功能。\n能区分当前实现、主规格约束和外部依赖可用性，避免把设计意图误写成运行结果。\n功能入口与完整调用链 📷 [图片 token=Kck6bosHloOw3rxR1klcFvvVnNe（未能下载，见飞书原文）]\n桌面入口由 apps/frontend/src/router/index.ts 定义。公开路由是 /login 与 /register，工作区中的 /chat、/knowledge、/aiops 和 /mcp 都标记为需要认证。全局导航守卫先调用认证状态初始化；没有当前用户时跳转登录页，已认证用户访问公开认证页时回到对话页。这里的前端守卫改善交互，但真正的安全边界仍在后端，因为浏览器路由不能阻止直接构造 HTTP 请求。\n📷 [图片 token=WYxHbWf81oHLzrxsYuWcQLnjnfb（未能下载，见飞书原文）]\n后端装配集中在 apps/backend/src/super_ai/api/app.py 的 create_app。它创建 FastAPI，配置 CORS、请求观测中间件、统一异常处理器，装配 AuthService、SQLite repositories、Milvus vector store 和 durable background job runtime。应用 lifespan 启动、停止后台任务 runtime，并在退出时释放自己创建的 SQLAlchemy engine。模块导入阶段不会连接 Milvus 或调用模型；这些外部动作由具体请求、readiness 或任务执行显式触发。\n📷 [图片 token=PnwPbpAx3oG9JzxZ8TfcMzotnFb（未能下载，见飞书原文）]\n以“上传知识文档并索引”为例，完整链路如下：\n📷 [图片 token=Ugixb1WKQoRgPcxzgJCcnODVnMb（未能下载，见飞书原文）]\napps/frontend/src/knowledge/knowledgeClient.ts 通过带 bearer token 的请求上传 Markdown 或 PDF。\ncreate_app 中的 upload_knowledge_document 先由 _current_user 解析身份，再校验个人知识库 ID、文件策略、抽取文本、计算 SHA-256，并通过 owner-scoped document repository 保存元数据。\ncreate_document_index_task 创建业务索引任务，再由 DurableDocumentIndexTaskScheduler 进入 BackgroundJobRuntime；API 以 202 返回，而不是等待嵌入和写向量完成。\napps/backend/src/super_ai/documents/indexing.py 的 DocumentIndexingService.run_task 按文档持久化的切分策略生成 chunk，调用 EmbeddingModel.aembed_documents，显式初始化 Milvus，按 tenant、知识库和文档范围删除旧 chunk，再批量插入新向量。\napps/backend/src/super_ai/vector_store/milvus.py 把 owner 与 tenant 字段展开为标量和 metadata；任务最终状态与文档索引状态仍写回 SQLite。\n对话链路体现另一类 AI Native 组合。POST /chat/sessions/{session_id}/messages:stream 先用当前用户 ID 查询会话；找不到同 owner 的会话就返回统一 403，不会启动 Agent。通过后，ChatStreamingService 先持久化用户消息，再让 LangChainChatAgentRunner 使用 provider 提供的聊天模型和可用工具。模型可自行选择知识检索、时间或已启用 MCP 工具，过程转成共享 SSE 事件；只有完整回答成功后才保存 assistant 消息。普通聊天没有自定义诊断状态图，而 AIOps 诊断由 apps/backend/src/super_ai/aiops/diagnostics.py 的图运行时承担 Planner、Executor、Replanner 与 Report 阶段，两者不应混为一谈。\nVue 路由与 Store → 共享 HTTP / SSE 客户端 → FastAPI create_app 路由与认证依赖 → 领域服务 / Agent runner / durable job → SQLite Repository → Milvus、Qwen、MCP、Alertmanager 等显式外部边界 📷 [图片 token=Kuw9b9MeboptOFxYGnVcQsuGn1b（未能下载，见飞书原文）]\n核心源码地图 📷 [图片 token=HgXxb3JKqox0lNxhqEMccKGunrh（未能下载，见飞书原文）]\n源码位置 关键符号 职责 apps/frontend/src/router/index.ts createAppRouter 定义公开页、受保护工作区路由和认证导航守卫。 apps/frontend/src/api/apiClient.ts createApiClient、ApiClientError 注入 bearer token，解析统一 HTTP envelope，并把错误转换为类型化异常。 apps/frontend/src/api/sseClient.ts createSseClient 读取 event stream、拼接跨 chunk 帧并验证共享 SSE 基础字段。 apps/backend/src/super_ai/api/app.py create_app、_current_user 后端组合根：路由、依赖注入、异常、中间件、readiness 和后台任务装配。 apps/backend/src/super_ai/memory/repositories.py MemoryRepositories、TenantScopeError 定义聊天、文档、诊断、审计、后台任务等业务持久化协议。 apps/backend/src/super_ai/memory/sqlite.py create_sqlite_memory_repositories 用 SQLAlchemy 实现 owner-scoped Repository 查询与状态变更。 apps/backend/src/super_ai/jobs/runtime.py BackgroundJobRuntime 执行持久后台任务，支持领取、租约、心跳、重试、超时与取消。 apps/backend/src/super_ai/chat/streaming.py ChatStreamingService、LangChainChatAgentRunner 组装聊天 Agent，转换事件，持久化消息与工具审计。 apps/backend/src/super_ai/vector_store/milvus.py MilvusConnectionManager、MilvusVectorStore 显式管理 Milvus 连接、collection、索引、范围查询与文档级删除。 packages/api-contracts/src/index.ts FOUNDATION_HEALTH_CONTRACT 及模块导出 共享 HTTP、错误、OpenAPI、SSE、认证、文档、检索等类型入口。 infra/compose.yaml etcd、minio、milvus、attu、alertmanager 只托管本地容器基础设施，不运行前端、后端或 CLS MCP Server。 代码调用流程图 📷 [图片 token=Vb3IbuXqno7rurxttP1cF5zAnHd（未能下载，见飞书原文）]\n下面这张图把页面动作、HTTP 路由、SQLite 任务、后台运行时和 Milvus 写入串成一条真实调用链。阅读源码时可以沿箭头逐层进入，而不是只在目录之间来回跳转。\n📷 [图片 token=S7iFb6IY6o3JV2xitPYcFsi7nZd（未能下载，见飞书原文）]\n关键实现拆解 📷 [图片 token=TFhxbCjkjobLM5xblcrcUgdYn7d（未能下载，见飞书原文）]\n组合根与惰性外部资源 📷 [图片 token=Rkf6boq99o8fG5xIpUxcCVSanxe（未能下载，见飞书原文）]\ncreate_app 接受 session_factory、vector_store、embedding_model、llm_provider、Agent runner 等可替换参数，这是测试无需真实网络的关键。默认向量存储由 build_default_milvus_vector_store 创建，但该函数只加载设置并创建对象；真正客户端由 MilvusConnectionManager.connect 在首次显式操作时构造。默认 LLM provider 也通过 _llm_provider 按需构建。这样的依赖边界使模块可导入、路由可测试，也使 readiness 能逐项报告故障。\n📷 [图片 token=QQeZbQQbfowL7Hxu9j1cLaQHnHc（未能下载，见飞书原文）]\n请求中间件为每个请求建立 request ID，记录方法、路径、状态和耗时。apps/backend/src/super_ai/observability.py 的 emit_event 会调用 _redact 清理敏感键；日志调用点只记录任务 ID、文档 ID、状态、耗时与异常类别，而不应记录用户消息、工具参数正文或文档正文。观测性在这里是边界检查的一部分，因为 Agent 系统最容易从“方便调试”的原始 payload 中泄露敏感数据。\n📷 [图片 token=Fn5Vbmv3soS3oex3zAEcNMh9n8f（未能下载，见飞书原文）]\n看什么：先看 create_app 的参数和 lifespan，确认哪些依赖可以注入，以及真正会在应用生命周期中启动和停止的对象。\n📷 [图片 token=Uyn4bjvh7oO19Ux8c1Icqf8RnJV（未能下载，见飞书原文）]\ndef create_app( *, database_url: str | None = None, project_config_path: str | Path | None = None, session_factory: async_sessionmaker[AsyncSession] | None = None, vector_store: MilvusHealthCheckProvider | None = None, embedding_model: EmbeddingModel | None = None, rerank_model: RerankModel | None = None, llm_provider: LlmProvider | None = None, chat_agent_runner: ChatAgentRunner | None = None, aiops_diagnostic_runner: AiopsDiagnosticRunner | None = None, alert_provider: ActiveAlertProvider | None = None, index_task_scheduler: DocumentIndexTaskScheduler | None = None, ) -\u0026gt; FastAPI: # 1. 组合根接收协议对象，测试可替换网络与存储边界。 # … 省略与本节无关的配置路径解析 @asynccontextmanager async def lifespan(application: FastAPI) -\u0026gt; AsyncGenerator[None, None]: runtime = cast(BackgroundJobRuntime, application.state.background_job_runtime) # 2. 生命周期只显式管理 durable runtime 与自有数据库 engine。 await runtime.start() try: yield finally: await runtime.stop() 📷 [图片 token=Yy7mbjwSHoiw3JxGz0qcuk6Anyd（未能下载，见飞书原文）]\n这段代码证明“创建应用”和“访问外部依赖”是两个动作：注入项可在测试中替换，lifespan 负责后台 worker，而 Milvus、LLM 和 MCP 仍由各自的显式调用触发。失败边界也因此可分离：装配失败、后台 runtime 失败、readiness 失败和一次业务调用失败不是同一个状态。\n📷 [图片 token=U40pbVNucof1dwxayCMcCqlnnNf（未能下载，见飞书原文）]\n看什么：再用局部图观察默认对象从“已装配”到“首次使用”的变化，避免把 Python import 或 FastAPI 构造误读为已经联网。\n📷 [图片 token=CvAubZyHpoGoB8xerGncbN7Jnvh（未能下载，见飞书原文）]\n图中只有最后两条分支会产生外部网络结果；任何分支失败都应落入对应的安全诊断或业务错误，而不是让模块导入产生不可控副作用。SQLite engine 则由组合根明确区分“自己创建”和“调用者注入”，退出时只释放前者。\n📷 [图片 token=YpPfbTcjgoSLUBxqyKWcdnW3nsc（未能下载，见飞书原文）]\n状态分层：SQLite 是业务主存储，Milvus 是知识向量专用存储 📷 [图片 token=Na2NblSOtoPL5lxc9LhcfA5Rntc（未能下载，见飞书原文）]\napps/backend/src/super_ai/memory/models.py 定义用户、会话、消息、知识文档、索引任务、诊断任务、证据、报告、工具审计和后台任务等 ORM 模型。它们由 Alembic 管理模式并通过 Repository 访问。Milvus collection 则只包含知识文档 chunk 的内容、向量、来源、时间与权限标量；用户账户、聊天消息或诊断报告不会因为“AI 系统”而被塞入向量库。\n📷 [图片 token=PQELbjyJ3oR6uCxZiYYc397dnIf（未能下载，见飞书原文）]\n这一分层还有恢复语义：上传文档后，SQLite 先持久化文档和索引任务；durable job 可以在进程重启后重新领取。Milvus 写入成功后才把文档置为 indexed、任务置为 succeeded。如果嵌入或 Milvus 失败，任务和文档都进入失败状态，用户可以显式重试。当前实现不是跨 SQLite 与 Milvus 的分布式事务，因此重建前使用文档范围删除并以确定性 chunk ID 重新插入，降低重复与残留风险。\n📷 [图片 token=UHwLbBpLIoeE3tx6wEVcolRhnQO（未能下载，见飞书原文）]\n📷 [图片 token=UbbMbSOo2ox9lCx7lincQcMOnoe（未能下载，见飞书原文）]\n看什么：索引核心先完成确定性切分和向量数量校验，再触碰 Milvus；删除条件同时包含 tenant、知识库与文档。\n📷 [图片 token=B4skbOf0JoKRPBx4XZOccH8snmc（未能下载，见飞书原文）]\n# 1. 使用文档持久化的策略生成可复现 chunks。 chunks = chunk_document_text( _indexable_text(document), strategy=strategy, chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) if not chunks: raise DocumentIndexingError(\u0026#34;Document has no indexable text.\u0026#34;) # 2. 向量数量必须与 chunk 数量完全一致。 vectors = await self._embedding_model.aembed_documents( [chunk.content for chunk in chunks] ) if len(vectors) != len(chunks): raise DocumentIndexingError( \u0026#34;Embedding provider returned an unexpected vector count.\u0026#34; ) # 3. 重建只删除当前 tenant、知识库和文档范围。 self._vector_store.initialize() self._vector_store.delete_document_chunks( tenant_id=owner_user_id, knowledge_base_id=document.knowledge_base_id, document_id=document.id, ) 📷 [图片 token=HDb7b2UmEoRNLxxgSTGcjzxxn4b（未能下载，见飞书原文）]\n片段证明 Milvus 内容是从 SQLite 文档事实派生出来的可重建索引，而不是反向决定文档存在性。空文本、向量数量不符、初始化或删除失败都会进入索引任务失败路径；删除后进程终止仍可能造成短暂空索引，所以当前边界是可观察、可重试的最终一致，而非跨库原子事务。\n📷 [图片 token=If0ObuthYoKX92xlyJ3cchgwn2Z（未能下载，见飞书原文）]\n看什么：下面的状态图把业务任务、文档状态和外部写入放在同一条时间线上，重点看失败发生在哪一侧。\n📷 [图片 token=RdbabaKAgovN7Ex1cMGc6JuNnqh（未能下载，见飞书原文）]\n状态图没有把 retry 画成原任务回滚：当前实现会创建新的尝试并保留旧失败记录。Repository 更新若在服务 try 块之外失败，则由 durable job 记录失败，业务记录可能停留在中间态，这也是排障时要同时查看两类任务的原因。\n📷 [图片 token=Hd52bUmXLofs33xoVYhcJ1EGn6b（未能下载，见飞书原文）]\n两类 Agent 流程 📷 [图片 token=LqxHboIYeo5qx4xM1lyc7fzcnOz（未能下载，见飞书原文）]\n聊天侧遵循“模型选择工具”的直接 Agent 模式。ChatStreamingService 负责可靠的外围生命周期：会话权限、用户消息持久化、system prompt 与 Skill 装配、工具调用审计、引用和最终消息。知识检索不是每条问题的强制前置步骤，无知识命中时也不能制造回退内容。\n📷 [图片 token=UBPPbOOSBoQ18zxQz5FchTHvnHg（未能下载，见飞书原文）]\nAIOps 侧需要计划、执行、重规划和证据报告，因而使用 LangGraph 诊断流程和 durable job。诊断结果要由真实工具结果、知识 SOP 与持久化证据支撑。外部 MCP、Prometheus 或 Alertmanager 不可用时，系统应如实失败或降级；当前代码没有授权以伪造日志、告警或成功执行来补齐演示。\n📷 [图片 token=SfPRbo7EcoSGREx5LKocdkcCnde（未能下载，见飞书原文）]\n看什么：AIOps 图的循环只发生在 replanner 之后，最终报告必须经过显式的 report 节点。\n📷 [图片 token=Mf3gb5IlloxE06xW5yzczy0unBd（未能下载，见飞书原文）]\ndef _build_graph(self) -\u0026gt; Any: graph = StateGraph(AiopsDiagnosticState) # 1. 四个节点分别承担计划、执行、重规划和报告职责。 graph.add_node(\u0026#34;planner\u0026#34;, self._planner) graph.add_node(\u0026#34;executor\u0026#34;, self._executor) graph.add_node(\u0026#34;replanner\u0026#34;, self._replanner) graph.add_node(\u0026#34;report\u0026#34;, self._report) graph.add_edge(START, \u0026#34;planner\u0026#34;) graph.add_edge(\u0026#34;planner\u0026#34;, \u0026#34;executor\u0026#34;) graph.add_edge(\u0026#34;executor\u0026#34;, \u0026#34;replanner\u0026#34;) # 2. 只有 replanner 决定继续执行或进入报告。 graph.add_conditional_edges( \u0026#34;replanner\u0026#34;, self._route_after_replanner, {\u0026#34;executor\u0026#34;: \u0026#34;executor\u0026#34;, \u0026#34;report\u0026#34;: \u0026#34;report\u0026#34;}, ) graph.add_edge(\u0026#34;report\u0026#34;, END) return graph.compile() 📷 [图片 token=GURJbmboEohr5nxHrT0clzG9n8e（未能下载，见飞书原文）]\n这段源码证明诊断并非普通聊天上附加几段 prompt，而是有明确阶段和循环出口的状态图。它只证明控制流，不证明某次诊断证据充分；工具失败、无 SOP 或证据不足仍要由节点状态、审计和最终报告如实表达。\n📷 [图片 token=LD6Ubj9aPoCWiix0Mocchfhinig（未能下载，见飞书原文）]\n看什么：对比两条运行路径时，关注“谁决定调用工具”和“终态保存在哪里”。\n📷 [图片 token=BZWPbpm1ioJuGExkmsIcAlYVnei（未能下载，见飞书原文）]\n普通聊天的工具选择是 Agent 决策，诊断的阶段转移是图定义；两者都受当前 owner、真实工具和结构化错误约束。连接中断时，聊天通过持久会话协调结果，诊断则还可依赖 durable job 与持久事件继续运行。\n📷 [图片 token=LxJ0buCBXoaWosxyRE0c1h6CnCh（未能下载，见飞书原文）]\n按纵向切片理解仓库 📷 [图片 token=VWZYbBz84oSP2YxwPogcEyyUnHN（未能下载，见飞书原文）]\n仓库目录不是孤立的技术分层，而是多条纵向功能切片共用一组基础边界。认证切片从 apps/frontend/src/authClient.ts 到认证路由、AuthService 和 SQLiteAuthRepository；知识切片从 knowledge client 到上传、索引任务、embedding 和 Milvus；聊天切片从 Pinia store 到 SSE client、流式服务、Agent runner 与消息 Repository；诊断切片则连接告警入口、durable job、图执行、证据与报告。每条切片都复用统一错误、request ID、当前用户依赖和共享契约，这正是单体仓库的优势。\n📷 [图片 token=SaPWbJXY0ojrzixi5jEc235enic（未能下载，见飞书原文）]\n新增能力时，可以用四个问题检查是否形成了完整切片。第一，前端看到的请求、状态和错误是否已有共享 DTO；第二，路由是否只负责认证、参数与服务编排，而没有直接实现复杂业务；第三，持久化是否经过协议边界并带 owner scope；第四，外部模型或工具失败后是否有安全错误、可观测状态和恢复动作。只完成页面或模型调用，通常还不能算一项完整能力。\n📷 [图片 token=X1bgbJ4Y4odObHxFHyacVTjXnPc（未能下载，见飞书原文）]\n看什么：索引 API 在同一个处理器里先做 owner-scoped 文档检查，再保存业务任务并调用调度器，202 只表示已接受。\n📷 [图片 token=AlcmbRN0loEFN3xcgD9cT01Mnch（未能下载，见飞书原文）]\ndocument = await _memory_repositories(request).documents.get_document( owner_user_id=user.id, knowledge_base_id=knowledge_base_id, document_id=document_id, ) if document is None: raise ApiErrorException(\u0026#34;AUTH_FORBIDDEN\u0026#34;) # 1. 先保存 pending 业务任务，HTTP 202 不等于索引完成。 task = await _memory_repositories(request).document_index_tasks.create_task( owner_user_id=user.id, task_id=f\u0026#34;index_task_{uuid4().hex}\u0026#34;, knowledge_base_id=knowledge_base_id, document_id=document_id, status=\u0026#34;pending\u0026#34;, ) # 2. 持久化后再把 task id 交给 durable scheduler。 await _schedule_index_task(request, owner_user_id=user.id, task_id=task.id) return success_response( request, {\u0026#34;task\u0026#34;: _document_index_task_payload(task), \u0026#34;scheduled\u0026#34;: True}, status_code=202, ) 📷 [图片 token=RV0ZbgeNrozK0Bxb9RKcSDRZnIh（未能下载，见飞书原文）]\n代码把切片的关键门槛放在路由层：身份来自依赖，文档与任务都带同一个 owner，外部索引不占用请求。若文档不属于当前用户，调度发生前即返回 403；若调度失败，不能把已创建的业务记录谎称为已索引，后续必须从任务与后台 job 状态继续观察。\n📷 [图片 token=Hv2BbfCx6owuqlxDItNcGKQtnXw（未能下载，见飞书原文）]\n看什么：从浏览器动作到最终向量写入逐层核对 owner、DTO、状态和失败回传，不要跳过共享契约或 Repository。\n📷 [图片 token=KZXxbjuZEojdevxyUs7c8G5rntc（未能下载，见飞书原文）]\n时序图显示 HTTP 返回与后台终态明确分离。页面需要轮询或订阅持久状态；直接把 202 当成功会隐藏 embedding、Milvus 或 worker 的失败，也会让用户在向量尚未就绪时误判检索结果。\n📷 [图片 token=F7PFbtIBwoE3VyxjTPbcPabYnhc（未能下载，见飞书原文）]\n控制面与运行面的相互约束 📷 [图片 token=Dg99bRlnnowViFxEZZRcSC6xnSd（未能下载，见飞书原文）]\nopenspec/specs 和共享契约可以视为工程控制面：它们规定哪些行为必须持续成立。FastAPI、Vue、SQLite、Milvus 和外部工具组成运行面：它们在请求与任务中产生真实状态。规格不能代替代码执行，运行结果也不能反向修改契约含义。变更时先更新可观察行为的规范与协议，再实现并用测试锁定，是避免 Agent 系统在快速迭代中失去边界的基本方法。\n📷 [图片 token=IrDGbOwLPou2qbxTP4ncORTQnef（未能下载，见飞书原文）]\n配置也是控制面的一部分。它选择 provider、模型、向量维度、外部 endpoint 与超时，但不表示依赖已经健康。readiness 把静态配置与动态可达性分别检查，业务任务再记录一次具体执行成功或失败。因此“配置有效”“依赖就绪”“一次索引成功”“一次诊断有充分证据”是四个不同结论，不能互相替代。\n📷 [图片 token=LLyVbnC4GohdqxxgdxgcD3EGn8b（未能下载，见飞书原文）]\n看什么：把一次变更从规范到运行结果画成闭环，特别关注机器契约和测试在中间承担的防漂移作用。\n📷 [图片 token=ATzBbGnAOoBzS6xOfHDc8jBonbb（未能下载，见飞书原文）]\n这张图证明不了运行依赖已经健康，但能说明每层应提供什么证据：规格定义期望、共享包定义形状、实现产生状态、测试防止漂移。任何一层变更若跳过相邻边界，都可能出现“类型通过但 Python payload 漂移”或“路由可用但 OpenAPI 未声明权限”的缺口。\n📷 [图片 token=DNL7b68TKo35G1xxN5TcGIKhneb（未能下载，见飞书原文）]\n数据、契约与状态 📷 [图片 token=TlXNb08uxovwyxxZfNEcr40kn0f（未能下载，见飞书原文）]\npackages/api-contracts/src/responses.ts 把 HTTP 响应分成 ok: true 的数据 envelope 与 ok: false 的结构化错误 envelope，并都携带 request metadata。packages/api-contracts/src/sse.ts 用 type 区分内容增量、推理增量、工具调用、引用、任务状态、报告、完成与错误。后端 Python 没有直接导入 TypeScript，但 apps/backend/src/super_ai/api/responses.py、apps/backend/src/super_ai/error_catalog.py 和事件序列化必须与共享形状对齐，双方测试负责防漂移。\n📷 [图片 token=C4XSbetRyouUkHxa4GFcS7annCh（未能下载，见飞书原文）]\n状态也按领域区分。HTTP health 是轻量 liveness；/ready 并行探测 SQLite、Milvus、LLM 与 MCP，可能以 503 返回一个仍为成功 envelope 的降级诊断数据；/config/check 同时检查配置可解析性与依赖可用性。业务任务则有自己的状态机，例如文档索引的 pending、running、succeeded、failed，后台任务还包含 queued、租约、取消与重试信息。不要用单一“服务正常”替代这些不同层次的状态。\n📷 [图片 token=JD5cbFrsyoG0JpxedbhcXZXZnsh（未能下载，见飞书原文）]\n权限、安全与失败边界 📷 [图片 token=IDU5bCaRjobChdxtYonckFxunre（未能下载，见飞书原文）]\n当前 user ID 就是 tenant scope，尚未引入独立组织 tenant。每个受保护路由通过 _current_user 获得 UserRecord，随后把 user.id 作为 owner_user_id 传给 Repository。跨 tenant 的资源 ID 通常表现为 owner-scoped 查询无结果，路由再映射为 AUTH_FORBIDDEN。Milvus 查询还必须同时带 tenant ID 与允许的知识库 ID；允许集合为空时直接返回空列表，不发送无范围 search 或 query。\n📷 [图片 token=RNC3b9hmtoPgpfx7LtKcjBbgnZe（未能下载，见飞书原文）]\n配置只从本地 config/project.json 与 config/user.project.json 递归合并，后者覆盖同路径字段。运行时不从环境变量补项目配置。模型 API key、CLS 凭据和真实资源 ID 不应进入版本控制、HTTP 响应或日志。/ready 与 /config/check 只暴露 provider、模型、基础 URL、collection 名、endpoint、延迟或工具数等必要上下文，失败时使用组件级安全消息。\n📷 [图片 token=Ho5cbljwSoEeg6xfIrOcWfgonjh（未能下载，见飞书原文）]\n外部失败不等于“没有问题”。Qwen 失败会让模型或嵌入流程失败，Milvus 失败会阻断索引和检索，MCP 失败会阻断真实工具调用，Alertmanager 不可用会阻断告警读取。调用层必须保留失败状态并提供恢复路径；AIOps 结论尤其不能在证据不足时生成确定根因。启动脚本会安装依赖、迁移数据库并启动服务，因此它是有副作用的运行入口，不应被当成静态验证命令。\n📷 [图片 token=KlEObLn9VoJCNrxg48fcbU2pnwc（未能下载，见飞书原文）]\n阅读顺序与小结 📷 [图片 token=KVxTbKxycoNsagxDoMKcQrAzn2e（未能下载，见飞书原文）]\n先读 README.md 与 openspec/specs/project-foundation/spec.md，建立产品能力和仓库边界。\n再读 packages/api-contracts/src/index.ts、packages/api-contracts/src/responses.ts、packages/api-contracts/src/sse.ts 与 packages/api-contracts/src/openapi.ts，明确跨层协议。\n从 apps/frontend/src/router/index.ts 进入一个页面 client，再追到 create_app 对应路由。\n沿路由继续到服务、apps/backend/src/super_ai/memory/repositories.py、SQLite 实现及外部 provider。\n最后沿本文流程图回到 OpenSpec 主规格与真实实现，核对权限、失败和状态边界。\nOncallAgent 的架构核心可以概括为：浏览器负责交互，FastAPI 负责边界与编排，服务和 Agent 负责受约束的智能行为，SQLite 保存可恢复的业务事实，Milvus只保存带权限范围的知识 chunk 向量，Qwen 与 MCP 是可失败的外部能力，共享契约和测试负责让这些层长期对齐。阅读源码时始终追问“身份从哪里来、事实保存在哪里、事件遵循什么契约、失败会留下什么状态”，就能比从页面或模型调用开始获得更稳定的全景。\n📷 [图片 token=HewQbjTEToNo6cxNfIyc2ceinGe（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/01.%20OncallAgent%20%E6%9E%B6%E6%9E%84%E5%85%A8%E6%99%AF%E4%B8%8E%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%9C%B0%E5%9B%BE/","summary":"OncallAgent 是一个本地优先的 AIOps Agent 工作台。它把 Vue 3 工作区、FastAPI API 与 Agent 运行时、SQLite 持久化、Milvus 知识向量、Qwen 模型能力和外部 MCP 工具放进一条","title":"01. OncallAgent 架构全景与源码阅读地图"},{"content":"很多人第一次使用 Codex 时，会把所有要求都写进聊天框：项目使用什么技术栈、代码应该放在哪里、修改后运行哪些测试、哪些目录不能碰、接口错误应该怎样返回。当前任务可能因此顺利完成，但换一个会话、换一个开发者，或者几天后重新打开仓库，这些背景又需要解释一遍。\nAGENTS.md 就是为了解决这个问题而存在的。它不是业务代码，也不是应用运行时配置，而是一份跟随仓库保存、专门提供给编码 Agent 阅读的长期工程说明。Codex 在开始工作前会自动加载适用的指令，让每次任务从相对一致的仓库认知出发。\n📷 [图片 token=JplCbO9cioQ2QRxF61ScLQswnyj（未能下载，见飞书原文）]\n[!SUCCESS] 可以把 AGENTS.md 理解为“面向 AI 编码助手的仓库 README”。README 主要告诉人怎样理解和运行项目，AGENTS.md 则进一步告诉 Codex 应该怎样阅读、修改、验证和交付这个项目。\n为什么仅靠提示词还不够 提示词适合描述当前任务，例如“修复登录过期后页面没有跳转的问题”。但仓库里还有大量不会随着这次任务改变的长期事实：后端使用什么包管理器、数据访问必须经过哪一层、API 契约放在哪里、是否允许新增环境变量、什么情况必须创建数据库迁移，以及怎样才算真正完成。\n如果这些信息只存在于某一次对话中，就会出现三个常见问题。首先是重复沟通，每次都要重新解释目录和命令；其次是行为漂移，不同会话对同一仓库采用不同做法；最后是隐性风险，Codex 可能在没有意识到安全边界的情况下写入凭据、绕过租户过滤，或者把测试替身的结果描述成真实联调成功。\n📷 [图片 token=YxxDb8T9Tojet4xHFmvcE4OpnEt（未能下载，见飞书原文）]\nAGENTS.md 把这些长期规则放进版本库，让它们与代码一起演进。它不会替你完成架构设计，也不能代替测试和代码审查，但它能够在 Agent 动手之前，把“这个仓库认为什么是正确做法”放入上下文。\n📷 [图片 token=PkDBbo5sLoGj5KxWrKtcpatAnVb（未能下载，见飞书原文）]\nAGENTS.md 到底是什么 从 Codex 的角度看，AGENTS.md 是一种开放格式的持久指令文件。文件内容就是普通 Markdown，不要求固定模板，也不会被应用打包或部署。它只在 Codex 等支持该约定的 Agent 读取仓库时发挥作用。\n它最适合保存四类信息：稳定的仓库事实、反复使用的工作流程、可以执行的验证命令，以及必须长期遵守的安全和工程边界。一次性的需求、某个临时 Bug 的细节和只对当前会话有效的偏好，仍然应该放在本次提示词或对应的 OpenSpec 变更里。\n📷 [图片 token=XLC3bgWHwojhrOxdm1Jc6nMHnxh（未能下载，见飞书原文）]\n需要特别区分：AGENTS.md 描述的是“在这个仓库里应该怎样工作”，不是“本次具体要开发什么功能”。后者应该由当前需求和 OpenSpec 负责。\n📷 [图片 token=A5NMb73GUo2euDxJS9fcLIxjnGf（未能下载，见飞书原文）]\nCodex 怎样发现并应用这些规则 根据 Codex 官方说明，指令发现会在一次运行开始时构建。Codex 先读取全局规则，再从项目根目录沿着目录层级走向当前工作目录，逐层收集可用的指令文件。越接近当前目录的文件越晚加入上下文，因此在规则冲突时拥有更高优先级。\n📷 [图片 token=YUWLbOaCWouSkNx2NPOc0Qgwngb（未能下载，见飞书原文）]\n作用范围 典型位置 适合保存的内容 个人全局规则 ~/.codex/AGENTS.md 个人长期偏好，例如沟通语言、常用审查方式和跨仓库工作习惯 仓库公共规则 仓库根目录的 AGENTS.md 项目结构、技术栈、通用工作流、验证命令和安全边界 子目录规则 apps/backend/AGENTS.md 等 只适用于该子树的语言规范、测试命令或团队约束 📷 [图片 token=ZqKPbAK7po3eFgxqB5acSmy8n0g（未能下载，见飞书原文）]\n在同一个目录中，Codex 会优先检查 AGENTS.override.md，找不到时再检查 AGENTS.md。同一目录最多选择一个指令文件。全局层和项目层都可以使用 override，但它会遮盖同层的普通文件，应当只在确实需要替代规则时使用。\n📷 [图片 token=Ywkzb0rIrosFEoxGznBcTefRn9g（未能下载，见飞书原文）]\nCodex 会跳过空文件。所有被发现文件的合并内容还受到 project_doc_max_bytes 限制，官方当前默认值为 32 KiB。文件过大时，与其不断提高上限，更好的做法通常是保留精炼的根规则，把只适用于某个区域的内容下沉到对应子目录，或者引用独立的架构与审查文档。\n指令链通常在一次 Codex 运行或 TUI 会话开始时生成。如果刚修改了 AGENTS.md，却发现当前会话仍在执行旧规则，可以新开一次运行或重启当前会话，让 Codex 重新发现指令。\n📷 [图片 token=Us6Kbvv16odP7Exv7AZcCr2RnJb（未能下载，见飞书原文）]\n一份有效的 AGENTS.md 应该包含什么 没有唯一模板，但内容应该帮助 Codex 在行动前回答几个问题：这是一个什么项目，事实应该去哪里查，代码分别位于哪里，什么流程必须遵守，哪些命令能够验证结果，以及有哪些绝对不能越过的边界。\n适用范围和项目定位。 说明当前文件影响整个仓库还是某个子目录，并用准确、克制的语言描述项目。这样可以避免 Codex 把完整项目当成空白脚手架，也能防止它引入不属于当前系统的产品定位和技术栈。\n事实来源和阅读顺序。 指出主规格、活动变更、共享契约、实现和测试分别承担什么角色。发生冲突时，应说明以什么为准，历史归档是否仍允许继续修改。\n仓库结构。 列出真正重要的目录及职责，不需要复制整棵文件树。目录说明应该帮助 Agent 快速定位前端、后端、共享包、基础设施、脚本、规格和文档。\n标准开发流程。 说明修改前需要检查什么，什么类型的变更必须先创建 OpenSpec change，怎样拆分纵向功能，何时同步契约、迁移、测试和文档，以及交付前应检查哪些差异。\n📷 [图片 token=HOmNbrliMo6NDsxhLlOcK3HSnVh（未能下载，见飞书原文）]\n构建与验证命令。 提供可以直接运行的安装、格式检查、类型检查、测试和构建命令，并注明正确工作目录。不要只写“运行相关测试”，因为不同 Agent 对“相关”的理解可能不同。\n语言与架构约定。 例如 Python 导入方式、前端状态管理模式、路由和服务的职责边界、数据库访问路径，以及能否引入新的包管理器或传输层。\n安全与数据边界。 明确凭据如何管理、日志必须怎样脱敏、用户和 tenant 数据怎样隔离、真实工具结果能否伪造、失败状态应该如何呈现。这类规则越具体，越能在代码生成早期阻止高风险偏差。\n📷 [图片 token=FnhHbWUX3obpokxkh6scSGUgnue（未能下载，见飞书原文）]\n完成定义和交付要求。 规定什么情况下必须运行后端测试、前端测试、契约检查、迁移或文档构建，以及只能报告真实执行过的验证结果。必要时还可以规定提交信息和 PR 描述格式。\n📷 [图片 token=M4uHbq53noOUJ0xZXEIciHqwnaf（未能下载，见飞书原文）]\n哪些内容不适合写进去 AGENTS.md 并不是越长越好。把所有知识都塞进去，会占用上下文，也会让真正重要的规则被淹没。\n不要写 API key、密码、访问令牌、真实连接地址或其他敏感信息。\n不要复制整份产品需求、数据库字典或架构文档；应写清事实来源并链接到对应文档。\n不要放只针对一次任务的临时要求，它们应该留在本次提示词或 OpenSpec change 中。\n不要写“保持高质量”“充分测试”“遵循最佳实践”这类无法执行和验收的空话。\n不要保留已经失效的命令、目录和技术栈。错误的长期指令比没有指令更危险。\n不要把无法自动验证的偏好伪装成强制规则，也不要让文字规则替代权限控制、测试和静态检查。\n📷 [图片 token=VhnabRi4foQf4qxzH1Kc1nr3nRc（未能下载，见飞书原文）]\nOncallAgent 仓库中的真实做法 当前 OncallAgent 仓库在根目录维护了一份 AGENTS.md，且暂时没有更深层的同名文件，因此它对整个仓库生效。文件不是泛泛介绍产品，而是围绕 Codex 实际开发时最容易出错的地方组织内容。\n首先，它定义了项目事实来源：已经生效的行为查看 openspec/specs/，正在推进的变更查看 openspec/changes/[change]/，HTTP 与 SSE 协议查看 packages/api-contracts/，最后再由实现和测试确认当前真实行为。归档目录只用于追溯，不继续在历史变更上开发。\n📷 [图片 token=Ux4Mbn6rDoGsmoxbKObczIzrnc5（未能下载，见飞书原文）]\n其次，它说明了真实仓库结构和技术边界。例如前端采用 Vue 3、Vite、TypeScript、Pinia 与 Vue Router；后端采用 FastAPI、LangChain/LangGraph、SQLAlchemy/Alembic 和 uv；后端源码使用 src layout，内部 Python 包名是 super_ai，导入时不能写成 src.super_ai。\n📷 [图片 token=MnVebzy1WoNbr3xR8s1cOXOHn6c（未能下载，见飞书原文）]\n更重要的是，它把容易造成严重错误的约束写成了明确规则：共享契约是前后端协议的唯一事实来源；所有用户数据都必须携带 owner 或 tenant 范围；Milvus 只能保存知识 chunk 向量；MCP 只能装配真实发现且已启用的用户工具；诊断证据不足时必须说明不确定性，不能虚构日志、根因或执行成功状态。\n📷 [图片 token=NkjVbooUGokzJyx6lZ9cnh59nGf（未能下载，见飞书原文）]\n最后，它给出了可以运行的质量门禁，例如共享契约类型检查、前端类型检查和测试、后端 Ruff、Pyright、Pytest、Alembic 升级验证、OpenSpec 全量校验以及文档构建。Codex 因此不仅知道“要测试”，还知道不同改动应该运行哪一组命令。\n📷 [图片 token=GE4qbPzInoAIiFx00BBcBBi9nAc（未能下载，见飞书原文）]\n有了 AGENTS.md 后应该怎样使用 创建初始文件。 可以在 Codex CLI 中使用 /init 生成一个起步版本，也可以手动在仓库根目录创建。自动生成的内容只是骨架，必须根据真实代码、命令和团队规则调整，不能未经核对直接提交。\n从仓库根目录启动 Codex。 这样 Codex 能识别正确的项目根和根级规则。如果只在某个子目录启动，也应确认向上查找后得到的是预期仓库，而不是另一个父目录。\n正常描述本次任务。 不需要在每条消息里再次粘贴技术栈和测试命令。提示词重点描述本次目标、背景和特殊限制，长期规则由 AGENTS.md 提供。\n让 Codex 先核对事实。 对非平凡任务，可以直接要求它说明当前适用的指令、相关 OpenSpec、可能影响的模块和准备运行的验证，再开始写代码。\n根据重复错误持续更新。 如果 Codex 多次采用错误包管理器、漏掉租户过滤或遗漏某类测试，就把经过团队确认的纠正写进最接近适用范围的 AGENTS.md。一次偶发误解不必立刻增加规则，重复出现的问题才值得长期固化。\n把文件纳入代码审查。 AGENTS.md 会影响以后所有 Agent 任务，因此它的修改应像构建脚本和工程配置一样接受审查。新增规则要说明它解决了什么反复发生的问题，删除规则要确认仓库行为已经变化。\n📷 [图片 token=FRdybXYkqoEvTKxGy6fcfD4QnTb（未能下载，见飞书原文）]\n可以使用以下命令检查 Codex 是否读取了预期规则：\ncodex --ask-for-approval never \u0026#34;Summarize the current instructions.\u0026#34; 如果仓库未来新增了后端专用规则，还可以从子目录验证：\ncodex --cd apps/backend --ask-for-approval never \u0026#34;Show which instruction files are active.\u0026#34; 这里的 apps/backend/AGENTS.md 只是说明分层方式的假设示例。当前 OncallAgent 仓库只有根目录文件，不应把尚未创建的子目录规则描述成现有实现。\n📷 [图片 token=NkmWbdd8Yoaqexxn75QcOTnFnbh（未能下载，见飞书原文）]\nAGENTS.md 与 OpenSpec 怎样分工 载体 主要回答的问题 适合保存的内容 生命周期 当前提示词 这一次希望完成什么 当前目标、背景、特殊限制和期望输出 一次任务或一次会话 AGENTS.md 在这个仓库里应该怎样工作 工程规则、目录路由、命令、安全边界和完成定义 长期存在，随仓库演进 OpenSpec 某个功能为什么改、要怎样变化 Proposal、Design、Tasks、规格增量和验收场景 从变更提出到归档，并沉淀进主规格 契约、实现与测试 系统实际上怎样运行 可执行代码、协议定义、迁移和验证证据 与产品实现持续同步 📷 [图片 token=HW3qbdOyCoU5LFx7uVVcGy5lnme（未能下载，见飞书原文）]\n一个典型流程是：Codex 先通过 AGENTS.md 理解长期规则，再根据用户需求读取或创建相关 OpenSpec change，然后按照 Tasks 修改契约、实现和测试，最后运行 AGENTS.md 中规定的质量门禁并验证规格一致性。\n两者不能相互替代。如果只写 OpenSpec 而没有仓库规则，Codex 知道要做什么，却可能使用错误的实现方式；如果只有 AGENTS.md 而没有变更规格，Codex 知道怎样写代码，却仍然不清楚这次功能的边界和验收条件。\n它能带来哪些实际好处 减少重复上下文。 开发者不必在每个任务中重新粘贴目录、命令和基础约束，可以把注意力放在本次需求真正特殊的地方。\n让不同会话更一致。 团队成员、本地 Codex 和后续会话共享同一套仓库规则，减少因为个人提示词差异造成的实现分叉。\n更早阻止错误方向。 当包管理器、导入方式、契约来源和数据边界在行动前已经明确，Agent 更不容易先生成大量错误代码，再通过返工纠正。\n提升验证可信度。 明确命令和完成定义后，Codex 更容易报告真实运行过的测试，也更难把“看起来正确”误写成“已经验证通过”。\n沉淀团队反馈。 反复出现的代码审查意见可以转化为长期规则，使同类问题在下一次任务开始前就进入上下文。\n形成安全护栏。 凭据、日志脱敏、tenant 隔离、真实工具调用和证据不足处理等约束，可以贯穿多个功能变更，而不依赖开发者每次临时提醒。\n📷 [图片 token=Znqab2bNTo22w7xxuN5cz2QSn0g（未能下载，见飞书原文）]\n这些收益不应被包装成无法验证的效率百分比。AGENTS.md 提供的是更稳定的执行边界，真正的质量仍然需要代码审查、测试、类型检查、权限机制和运行时验证共同保证。\n📷 [图片 token=Bn8PbIF4go2Swvx0sKOcSyUrnBK（未能下载，见飞书原文）]\n怎样维护才不会逐渐失效 维护 AGENTS.md 的关键不是一次写得很长，而是持续保持准确。仓库目录、依赖管理、启动方式或测试命令变化时，应同步更新；团队重复留下同一种 PR 反馈时，可以评估是否固化；某条规则已经被 lint、hook 或权限系统可靠执行后，可以把文件中的文字压缩为验证入口。\n每隔一段时间可以做一次小型审计：命令是否还能运行，路径是否真实存在，禁止项是否仍符合架构，OpenSpec 和共享契约的事实来源是否改变，是否出现互相冲突的规则，以及文件是否接近加载大小上限。\n根文件应尽量保存全仓库共同规则。只有当某个子目录确实拥有不同语言、命令或风险边界时，才增加嵌套文件。规则下沉得太细会提高维护成本，也可能让开发者难以判断当前到底应用了哪一层。\n📷 [图片 token=NlQpbOqMnoVxiIxl99wcqgvmnre（未能下载，见飞书原文）]\n可以直接修改使用的基础模板 下面的模板强调结构而不是篇幅。使用时应删除不适用的章节，并把所有示例命令替换成项目中真实可运行的命令。\n# 项目仓库指南 ## 适用范围 本文件适用于整个仓库。 若子目录存在更具体的 AGENTS.md，以离目标文件最近的规则为准。 用一段话说明项目定位、当前状态和修改时最重要的前提。 ## 事实来源 - 产品与工程主规格：specs/ - 活动变更：changes/ - 共享契约：packages/contracts/ - 当前行为：实现与测试 说明发生冲突时的判断顺序，以及历史归档是否允许继续修改。 ## 仓库结构 - apps/backend：后端应用及测试 - apps/frontend：前端应用及测试 - packages：共享包与协议 - docs：用户文档和工程文档 只保留能帮助 Agent 正确定位代码的重要目录。 ## 标准工作流 1. 修改前检查工作区、相关规格、实现和测试。 2. 明确影响范围，避免顺手修改无关文件。 3. 行为变化先更新规格或创建变更记录。 4. 同步实现、契约、迁移、测试和文档。 5. 运行与影响范围匹配的验证。 6. 交付前检查 diff 和工作区状态。 ## 常用命令 安装： - 项目真实安装命令 检查： - 格式检查命令 - 类型检查命令 - 单元测试命令 - 构建命令 注明每条命令应该在哪个目录运行。 ## 代码约定 - 语言版本和包管理器 - 模块导入与目录边界 - API、服务、Repository 的职责 - 前端状态和传输层约定 - 数据库迁移要求 ## 配置与安全 - 配置文件入口和覆盖顺序 - 凭据与本地配置的禁止提交规则 - 日志和错误脱敏要求 - 权限、用户和 tenant 数据边界 - 外部工具调用的真实性要求 ## 测试与交付 - 不同变更类型必须运行哪些检查 - 什么情况需要迁移、截图或联调 - 只能报告真实执行并通过的验证 - 提交信息和 PR 说明要求 📷 [图片 token=Y8pIbxcjWoWSiRxmay2c86Wnn9e（未能下载，见飞书原文）]\nAGENTS.md 最有价值的地方，不是让提示词变得更长，而是让那些已经验证有效的工程经验不再依赖某个人记住。它把仓库结构、开发纪律和安全边界变成 Codex 每次开始工作前都能获得的共同上下文；OpenSpec 再在这套长期规则之上，描述每一次具体变更。两者配合后，AI Coding 才从一次性的对话技巧，逐渐变成可以复用、检查和维护的工程流程。\n📷 [图片 token=IhYAbSQcpoF3v3xzSgjcqx8Un2g（未能下载，见飞书原文）]\n官方参考 OpenAI：Custom instructions with AGENTS.md\nOpenAI Codex Manual\nAGENTS.md 开放格式说明\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/02%EF%BD%9CAI%20Coding%20%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A6%E6%9D%9F/AGENTS.md%E8%AF%A6%E8%A7%A3%EF%BC%9A%E8%AE%A9%20Codex%20%E7%9C%9F%E6%AD%A3%E7%90%86%E8%A7%A3%E5%B9%B6%E9%81%B5%E5%AE%88%E4%BB%93%E5%BA%93%E8%A7%84%E5%88%99/","summary":"很多人第一次使用 Codex 时，会把所有要求都写进聊天框：项目使用什么技术栈、代码应该放在哪里、修改后运行哪些测试、哪些目录不能碰、接口错误应该怎样返回。当前任务可能因此顺利完成，但换一个会话、换一个开发者，或者几天后重新打开仓库，这些背","title":"AGENTS.md详解：让 Codex 真正理解并遵守仓库规则"},{"content":"delta spec 是 OpenSpec 适配存量项目的关键。它不重新抄写整个系统规范，只描述这一次 change 相对当前主规格新增、修改、删除或重命名了什么。这样多个 change 可以各自在独立目录里工作，主规格在归档前保持稳定，评审者也能直接看到“差异”而不是在长文档里找变化。\n📷 [图片 token=DzT6bpezcoF8HrxIc8vcKkg5n7b（未能下载，见飞书原文）]\n文件位置和 capability 的关系 openspec/changes/\u0026lt;change-name\u0026gt;/ └── specs/ └── \u0026lt;capability\u0026gt;/ └── spec.md 目录名 \u0026lt;capability\u0026gt; 对应长期系统能力。例如主案例的 change 名是 limit-qwen-embedding-batch-size，但 delta spec 位于 specs/qwen-openai-provider/spec.md。前者是一次修复，后者是被修改的长期能力。\n这个区分让主规格按领域组织，而不是按工单组织。一次变更结束后，change 进入 archive；qwen-openai-provider 仍然是系统当前能力的一部分，后续关于聊天模型、Embedding 或 rerank 的变更可以继续修改同一 capability。\n📷 [图片 token=BijtbCzjkoSGF2xLg8bc1QcXnPb（未能下载，见飞书原文）]\n四种变化操作 📷 [图片 token=RyrIbSx78ogmTwx66dFcX6PQn5d（未能下载，见飞书原文）]\nADDED：新增以前不存在的行为 ## ADDED Requirements ### Requirement: Qwen embedding batch compatibility 后端 SHALL 确保每个请求最多包含 10 条文本…… #### Scenario: Large embedding input is split - **WHEN** 文档索引包含超过 10 个 chunk - **THEN** 客户端 MUST 拆分为兼容批次并保持顺序 归档同步时，这类 requirement 会加入主规格。如果同名 requirement 已经存在，本地 sync Skill 会把它视为隐式修改，而不是机械追加重复内容。\n📷 [图片 token=SGvpbynZ0ofNVLxORjmcwNAmn2g（未能下载，见飞书原文）]\nMODIFIED：修改已有行为的一部分 MODIFIED 可以只写需要变化的描述或新增场景，不必复制整个旧 requirement。Agent 会读取 delta 和 main spec，智能合并变化，同时保留 delta 没有提到的其他场景。这样 delta 表达的是变化意图，不是整块覆盖文件。\n📷 [图片 token=TkSFblV0Eo52ycx3TDQcYmi3nmb（未能下载，见飞书原文）]\nREMOVED：明确废弃行为 REMOVED 应写出要删除的完整 requirement 名称和必要原因。同步后，主规格中对应 requirement 应消失。OncallAgent 的 wiki-sync 会校验最新 REMOVED 操作是否真的已经从主规格移除，避免 change 虽然归档，旧行为却仍被主规格宣称有效。\n📷 [图片 token=IU6kbn89Lou9IQxYvLic1kpAnTd（未能下载，见飞书原文）]\nRENAMED：改变 requirement 名称 RENAMED 用 FROM/TO 指出重命名。它表达的是同一行为契约的名称演进，而不是删除旧行为后随便新增一个不相关行为。同步器需要同时处理旧名称消失和新名称出现。\n📷 [图片 token=PRrpbt5VKovTlHx2OYvcuQbanHf（未能下载，见飞书原文）]\nRequirement 应该写什么 Requirement 描述系统必须具备的行为，常用 SHALL、MUST、MUST NOT 表示强约束。它应回答“系统对调用者作出什么承诺”，而不是“代码用哪个类完成”。\n主案例的 requirement 同时包含两个承诺：单个 Embedding 请求最多 10 条文本；任意数量输入都返回完整且顺序对应的向量集合。前半句约束外部请求兼容性，后半句保护业务语义。如果只写“设置 chunk_size=10”，即使代码参数存在，也无法保证输入输出顺序和完整性。\n📷 [图片 token=Nlyyb95GVo5NPcxtDPKcY9XInqg（未能下载，见飞书原文）]\nScenario 为什么比一句需求更重要 Scenario 把抽象 requirement 转换成可验证例子。OncallAgent 常用 WHEN/THEN，必要时可以增加 GIVEN 和 AND：\n#### Scenario: Large document indexing completes - **WHEN** 可访问文档被拆分为超过 10 个有效 chunk， 且 Embedding 与 Milvus 可用 - **THEN** 索引任务 MUST 生成全部向量、写入全部 chunk， 并标记为 succeeded 这个场景覆盖的不只是拆批，还覆盖最终业务结果。Provider 单测可以证明请求被拆成 10+1，文档索引测试则证明 11 个业务 chunk 最终完整落库。一个 requirement 可以由多层测试共同提供证据。\n📷 [图片 token=IKNybtbQ4ozzToxQYYicOpeynEb（未能下载，见飞书原文）]\n为什么 spec 不写实现细节 如果 spec 写“在 provider.py 第 150 行给 OpenAIEmbeddings 传 chunk_size=10”，文件改名、封装替换或依赖升级都会让规格失效，即使系统行为仍然正确。实现位置、类名和参数属于 design；spec 只要保证每批不超过上限、顺序完整和索引成功。\n这种分离让实现可以重构。未来即使不再使用 LangChain，只要新 Provider 仍兑现同样行为，主规格无需重写；如果厂商上限变化，才创建新的 delta 修改行为契约。\n📷 [图片 token=AVlmbU1lVoD3YOxPCdycgCtintg（未能下载，见飞书原文）]\n怎样审查 delta spec **操作类型是否准确。**新增能力用 ADDED，修改已有约束用 MODIFIED。不能为了省事全部写 ADDED，否则主规格可能出现重复 requirement。\n**场景是否覆盖边界。**主案例同时写小输入、超过上限的大输入和最终索引完成，覆盖正常路径、边界分支与业务结果。\n行为是否可观察。“代码更优雅”“架构更合理”无法作为 Scenario 结果。应能通过测试、API、状态或持久化结果判断。\n**是否遗漏权限和失败语义。**OncallAgent 的知识、MCP、聊天和 AIOps 数据都按 owner/tenant 隔离；涉及这些能力时，Scenario 必须包含授权和越权行为。真实工具失败也要如实返回，不能把失败写成成功。\n📷 [图片 token=P2Skb6FdMoudZTxCgd3c5tF1nQc（未能下载，见飞书原文）]\n面试表达 delta spec 只描述这次变化，不复制整份主规格。它按 capability 归档，用 ADDED、MODIFIED、REMOVED、RENAMED 表达意图，再用 Requirement 和 WHEN/THEN Scenario 定义可验收行为。这样我能把“代码改了什么”提升为“系统承诺发生了什么变化”，归档时再智能合并到主规格。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/delta%20spec.md%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"delta spec 是 OpenSpec 适配存量项目的关键。它不重新抄写整个系统规范，只描述这一次 change 相对当前主规格新增、修改、删除或重命名了什么。这样多个 change 可以各自在独立目录里工作，主规格在归档前保持稳定，评","title":"delta spec.md文件作用介绍"},{"content":"本页把 2026-07-11-limit-qwen-embedding-batch-size 从问题、产物、实现、测试、主规格到 WIKI 串成一条完整证据链。这个 change 规模很小，却包含边界定义、分层设计和多层验证，适合用来说明 OpenSpec 不是“写文档”，而是怎样控制一次真实工程变化。\n📷 [图片 token=IdlEbXy31o2av9xulTSczAEJn4d（未能下载，见飞书原文）]\n问题从哪里来 OncallAgent 支持 Markdown/PDF 知识文档上传、切分、Embedding、Milvus 写入和后续 RAG 检索。文档索引服务会把拆分后的完整 chunk 列表交给 EmbeddingModel.aembed_documents。\n阿里云百炼 text-embedding-v4 的 OpenAI-compatible 接口限制单次最多 10 条文本。默认客户端批量值更大时，一个产生 11 个以上 chunk 的文档会在向量生成阶段收到 HTTP 400，导致索引失败。\n📷 [图片 token=G85mbkAaZoSgq6xDvayc2iLBnf7（未能下载，见飞书原文）]\n注意，这个问题的表象是“大文档索引失败”，但根因不在切分算法，也不在 Milvus，而在模型 Provider 对上游批量约束适配不完整。准确归因决定了 change 应该修改哪一层。\n📷 [图片 token=MtcKb7uvmo9o6MxsHNgcVsddnuf（未能下载，见飞书原文）]\n.openspec.yaml：声明工作流 schema: spec-driven created: 2026-07-11 它说明该 change 使用 spec-driven 产物链。没有业务描述，没有模型密钥，也没有运行时配置。归档后继续保留，用于说明历史 change 的流程上下文。\nproposal：先控制范围 Why 写清 10 条限制、当前一次提交全部 chunk、11 条以上触发 HTTP 400 和索引失败。\nWhat Changes 要求单批限制为 10，超过 10 自动分批，输入输出顺序一致，并增加回归测试。\nCapabilities 没有新增能力，只修改已有 qwen-openai-provider。\nImpact 锁定后端 Embedding 构造、Provider 单测和索引回归测试；明确不修改 HTTP API、SSE、Milvus schema 和前端。\n这一步最重要的作用是防止范围膨胀。修复不需要重新设计知识库页面，也不需要让索引 API 暴露 batch size。\n📷 [图片 token=PiMQbedFVoZVecxgTp2cvnJMnif（未能下载，见飞书原文）]\ndelta spec：把成功写成可验收行为 delta ADDED 了 Qwen embedding batch compatibility，定义三个 Scenario：\n**小输入。**不超过 10 条时，在一个兼容请求中完成。\n**大输入。**超过 10 个 chunk 时，每批最多 10 条，并按输入顺序返回每个向量。\n**最终业务结果。**可访问文档超过 10 个有效 chunk、Embedding 与 Milvus 可用时，全部向量和 chunk 必须写入，任务状态为 succeeded。\n第三个场景很关键。只验证请求拆批还不够，最终用户关心的是索引任务完整成功，不能出现 11 个输入只保存前 10 个的静默数据丢失。\n📷 [图片 token=SfNCbZTsAoPmhPxzYHoc2xO9nXf（未能下载，见飞书原文）]\ndesign：决定约束归属 方案最终在 OpenAIEmbeddings 构造时设置 chunk_size=10。理由是批量上限属于 Provider 约束，应该由 Provider 统一适配，让所有调用方共享安全行为。\n索引服务继续一次调用完整 chunk 列表。LangChain 客户端负责拆分上游请求并按原顺序合并响应。业务层依赖稳定的 EmbeddingModel 抽象，不需要知道百炼是 10 条、其他供应商又是多少条。\nTrade-off 是大文档会产生更多 HTTP 请求，速度可能降低；但索引是异步后台任务，当前优先正确性。若未来上限提高，固定 10 会牺牲吞吐，可以再通过新 change 配置化。\n📷 [图片 token=Dz2MbrlDyoJHofxoAK1csgm8nyh（未能下载，见飞书原文）]\ntasks：把设计变成四个证据点 1.1 默认 Qwen 客户端单批限制为 10 1.2 增加默认客户端批量配置和行为单测 2.1 增加超过 10 chunk 的索引回归测试 2.2 运行 Ruff、Pyright、Pytest、OpenSpec 验证 前两项证明 Provider 适配，第三项证明业务结果，第四项完成质量门禁。归档中的复选框均为完成状态，但它们是历史记录；\n📷 [图片 token=EH8ubXqP8ocHBgxLLS7cwRtVn2g（未能下载，见飞书原文）]\n实现：变化集中在 Provider 当前代码在 apps/backend/src/super_ai/llm/provider.py 定义：\nQWEN_EMBEDDING_BATCH_SIZE = 10 构造 OpenAIEmbeddings 时传入：\nchunk_size=QWEN_EMBEDDING_BATCH_SIZE check_embedding_ctx_length=False 这与 design 一致：适配留在 Provider，索引服务没有新增厂商分支。check_embedding_ctx_length=False 还保证 OpenAI-compatible Qwen 接收原始字符串输入，而不是被默认逻辑转换成 token ID 数组。\n📷 [图片 token=HrrgbG8z5oHMhKxtZhJcpVzUnKf（未能下载，见飞书原文）]\n测试：为什么需要两层 Provider 行为测试 创建默认 Embedding 模型，用 11 条文本调用，并替换底层异步客户端收集真实请求。断言 chunk_size == 10，请求输入是 [前10条, 后1条]，最终向量仍按 0 到 10 顺序返回。它直接证明拆批发生在 Provider/LangChain 层。\n索引服务回归测试 用 11 个字符生成 11 个单字符 chunk，断言索引任务 succeeded，业务层一次交给 fake Embedding 11 条，向量库最终插入 11 个 chunk。这个测试故意不在业务层模拟 10+1，因为 design 要求业务层不感知厂商上限。\n两类测试并不重复：一个验证适配细节，一个验证业务完整性。把它们混成单一大测试，失败时反而难以判断是拆批、顺序、索引状态还是存储出了问题。\n📷 [图片 token=A7cpbXiWCobPI2x4cB7c9Mqjnrg（未能下载，见飞书原文）]\nsync 与 archive：从变化变成当前事实 delta 已合入 openspec/specs/qwen-openai-provider/spec.md，主规格现在包含同名 Requirement 与三个 Scenario。随后整个 change 进入：\nopenspec/changes/archive/ 2026-07-11-limit-qwen-embedding-batch-size/ 主规格告诉新任务“当前 Provider 必须兼容 10 条上限”；archive 则保留当时为什么这样做、有哪些非目标、承担哪些代价和怎样验证。\n📷 [图片 token=XBH5bqRVXoiGISx19MVcRqGOnHe（未能下载，见飞书原文）]\nwiki-sync：把历史变成可浏览页面 仓库生成了：\ndocs/changes/archive/ 2026-07-11-limit-qwen-embedding-batch-size/index.md 聚合页包含 archived frontmatter，通过 @include 引用 proposal、design、tasks 和 qwen-openai-provider delta spec；总索引和 Sidebar 也包含同一条目。正文没有被复制，OpenSpec 仍是唯一事实来源。\n📷 [图片 token=FbGSb2F64o7YGvxfDnFcN5H5nLb（未能下载，见飞书原文）]\n这段经历怎样讲得可信 我们遇到的不是一般“大文档性能问题”，而是 Provider 未适配百炼 Embedding 单次 10 条上限。先用 proposal 排除 API、Milvus 和前端变化，再用 spec 同时约束请求上限、顺序完整性和最终索引成功。设计上把 chunk_size 放在 Provider，让业务层继续提交完整列表；测试则分成 Provider 10+1 拆批与索引 11 chunk 完整落库两层。归档后 delta 进入主规格，wiki-sync 通过 include 展示完整决策链。这说明修复的不只是一个 400，而是把供应商约束收敛在正确边界。\n可信的关键是能说明因果链、边界、取舍和证据，而不是堆砌“规范驱动”“Agent 自动化”等词语。\n📷 [图片 token=QPoPbGVyuoYE6Vx5ld6cMOPtngb（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E7%9C%9F%E5%AE%9E%E6%A1%88%E4%BE%8B/limit-qwen-embedding%E6%A1%88%E4%BE%8B%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"本页把  2026-07-11-limit-qwen-embedding-batch-size  从问题、产物、实现、测试、主规格到 WIKI 串成一条完整证据链。这个 change 规模很小，却包含边界定义、分层设计和多层验证，适合用来说","title":"limit-qwen-embedding案例文件作用介绍"},{"content":"这一页不只列命令，而是把一次 OpenSpec 变更拆成“输入、产物、审查点和完成证据”。理解这条链以后，面对不同版本或不同 schema，即使文件名和快捷入口发生变化，也能判断当前应该做什么。\n演练素材来自 OncallAgent 已归档的 2026-07-11-limit-qwen-embedding-batch-size。它解决百炼 text-embedding-v4 单次最多接收 10 条文本的问题：Provider 对更长输入透明分批并保持向量顺序，文档索引业务层仍一次提交完整 chunk 列表。\n📷 [图片 token=IaKjbkK2HoNq0yxyRZncjcCLnJe（未能下载，见飞书原文）]\n[!CAUTION] 这个 change 已经在当前仓库完成并归档。下面是历史复盘，不应在当前分支再次创建同名变更。真正练习时应选择尚未实现的真实需求，或在隔离分支回到变更前状态。\n先看完整交付链 仓库调查 → explore 澄清 → new + continue，或 propose → proposal / delta specs / design / tasks → apply → verify → sync main specs → archive → wiki-sync → docs build 📷 [图片 token=Bscjbs2uaoRGuExICHVcyQuAnve（未能下载，见飞书原文）]\n这条链不是要求每一步都由人手敲命令，而是说明每个阶段必须产生什么证据。Codex 可以执行大量机械工作，但不能跳过边界确认、规格审查和真实验证。\n步骤一：从仓库事实开始，而不是先写方案 在仓库根目录先检查工作树、仓库规则和已经生效的规格：\ngit status --short sed -n \u0026#39;1,260p\u0026#39; AGENTS.md sed -n \u0026#39;1,220p\u0026#39; openspec/config.yaml openspec --version 📷 [图片 token=CVZGb5JcVotTDBxcAOvcvc71ndc（未能下载，见飞书原文）]\n针对本案例，还要阅读 openspec/specs/qwen-openai-provider/spec.md、apps/backend/src/super_ai/llm/provider.py、apps/backend/tests/test_llm_provider.py 和 apps/backend/tests/test_document_indexing.py。目的不是提前找一个文件去修改，而是先确认现有行为、责任边界和测试模式。\n📷 [图片 token=TDVQbXqZsoS5qzx7MvlcTlUln3e（未能下载，见飞书原文）]\n步骤二：先把需求写成可以审查的输入 历史案例可以整理成下面这段输入：\nOncallAgent 通过 LangChain OpenAIEmbeddings 调用百炼 OpenAI-compatible text-embedding-v4。提供商单次最多接收 10 条文本。要求 Provider 对任意长度输入按每批最多 10 条进行拆分，并保持返回向量与输入顺序一致；文档索引层继续一次提交完整 chunk 列表。增加 11 条文本的 Provider 测试和 11 chunk 的索引回归。非目标是不修改 chunking 策略、HTTP/SSE、Milvus schema、前端，也不新增通用动态限流。\n📷 [图片 token=Wi6MbMKptoeyakxls54cq5S6nCf（未能下载，见飞书原文）]\n这段输入包含目标、背景、限制、允许修改的层和非目标。它仍然不是正式规格，但已经足以让 proposal 收敛范围。若只说“修一下 Embedding 报错”，Codex 很可能在索引服务、分块策略或重试逻辑上同时发散。\n步骤三：需求模糊时先 explore 当问题还不清楚，可以在 Codex 中使用 /opsx:explore，或明确要求使用 openspec-explore。这一阶段允许阅读代码、规格和测试，比较方案并暴露未知项，但不应直接写应用代码。\n探索完成的验收标准不是“生成了一篇分析”，而是能明确回答：真正的问题是什么；哪些行为已经存在；目标与非目标是什么；涉及哪些 capability；还缺少什么事实或决策。目标清楚后再进入正式 change。\n📷 [图片 token=Z7Dsb4T8fonutkxPinMcEU6on9e（未能下载，见飞书原文）]\n步骤四：选择快速路径或渐进路径 路径 适用情况 本地行为 propose 问题和边界已经比较清楚 创建 change，并按依赖生成达到 apply-ready 所需的全部 artifacts new + continue 希望逐份审查，或关键决策尚未收敛 new 只建立脚手架；continue 每次创建一个当前 ready 的 artifact ff 希望快速创建一个新 change 的全部前置产物 本地 ff 自己包含 new，不应机械接在已经执行的 new 后面 📷 [图片 token=Q9xtbqdecokqIexyYNicKjhwnrg（未能下载，见飞书原文）]\n快速路径示例：\n/opsx:propose limit-qwen-embedding-batch-size 没有斜杠入口时，可以直接说：“使用 openspec-propose 创建该 change，先读取主规格、实现和测试，按本地 schema 生成 apply-ready artifacts。”\n渐进路径示例：\n/opsx:new \u0026lt;change-name\u0026gt; /opsx:continue \u0026lt;change-name\u0026gt; /opsx:continue \u0026lt;change-name\u0026gt; # 持续到 status 显示 applyRequires 全部完成 📷 [图片 token=IyiSbAUb6onnwWxegOtcaActnfe（未能下载，见飞书原文）]\n当前本地 Skills 的底层机制会调用 openspec status --change \u0026quot;\u0026lt;name\u0026gt;\u0026quot; --json 和 openspec instructions \u0026lt;artifact-id\u0026gt; --change \u0026quot;\u0026lt;name\u0026gt;\u0026quot; --json。Codex 应使用返回的 planningHome、changeRoot、artifactPaths 与 resolvedOutputPath，不能把某篇教程中的路径写死。\n📷 [图片 token=WPyHbnlxHo2DYuxsReicw1LEnad（未能下载，见飞书原文）]\n步骤五：逐份审查四类核心产物 **先审 proposal。**它应说明百炼批量上限为什么会导致较大文档索引失败，明确只改变 Provider 与相关测试，并把 HTTP、SSE、Milvus schema 和前端列为非影响范围。若 proposal 还在讨论具体循环代码，说明意图层和设计层混在了一起。\n📷 [图片 token=IjvVbVcb3oH8UExZ47qcg1KXnNe（未能下载，见飞书原文）]\n**再审 delta spec。**本案例修改 qwen-openai-provider capability，包含一条 Requirement 和三个 Scenario：\n输入不超过 10 条时，使用一个兼容批次。\n输入超过 10 条时，每批不超过 10 条，并按原顺序返回完整向量集合。\n文档超过 10 个有效 chunk 时，索引任务最终为 succeeded，并写入全部 chunk。\n**然后审 design。**关键决策是把限制放在 Provider 构造处：OpenAIEmbeddings(chunk_size=10)。索引服务仍调用一次 aembed_documents 并提交完整列表，由 LangChain 客户端透明分批。这样厂商约束不会泄漏到业务层，未来其他调用方也自动复用。\n📷 [图片 token=Cdz0bZudEoZ7HcxBNYgcCIrgnfc（未能下载，见飞书原文）]\n**最后审 tasks。**历史 tasks 将工作拆为四个证据点：配置批量上限；验证默认客户端；验证超过 10 个 chunk 的完整索引；运行质量门禁。每项都能判断是否完成，并且测试与验证没有被藏在“完成开发”一句话里。\n产物审查的真正门槛是：每个 Scenario 能否指向一项实现动作和至少一种验证证据。只检查文件是否存在，无法阻止一套形式完整但内容空洞的 artifacts。\n📷 [图片 token=MubybvSz5oXRTixzz23cfpvOnZe（未能下载，见飞书原文）]\n步骤六：进入 apply，但持续允许规格修正 /opsx:apply limit-qwen-embedding-batch-size 本地 apply Skill 会先读取 status，再调用 openspec instructions apply --change \u0026quot;\u0026lt;name\u0026gt;\u0026quot; --json，随后读取返回的全部 contextFiles。spec-driven change 通常会提供 proposal、delta specs、design 和 tasks，但其他 schema 可能不同，不能只读 tasks.md。\n📷 [图片 token=FfP7bccJwoR1YaxwCiJc4zW2nEc（未能下载，见飞书原文）]\n本案例的合理实施顺序是：先在 Provider 定义批量常量并交给 OpenAIEmbeddings；再补 Provider 层的 11 条输入测试；然后补索引服务的 11 chunk 回归。完成并验证一项后才能把对应 - [ ] 改为 - [x]。\n如果实现过程中发现 LangChain 不保持顺序，或 Provider 配置无法覆盖异步接口，就不能为了勾完 tasks 硬写补丁。应暂停，更新 design、Scenario 或任务，再继续实现。OpenSpec 的价值正是让方向变化可见。\n📷 [图片 token=PJk7bHdYcohKzdx2Bk0ceGeLnZb（未能下载，见飞书原文）]\n步骤七：把验证写成真实证据 针对这一历史案例，相关检查至少应包含：\ncd apps/backend uv run python -m pytest tests/test_llm_provider.py -k batches_by_ten uv run python -m pytest tests/test_document_indexing.py -k more_than_ten_chunks uv run ruff check . uv run pyright uv run pytest openspec validate --all 📷 [图片 token=Nn5jbeRnDoLgWExVKOAcfkfeneT（未能下载，见飞书原文）]\nProvider 测试证明 11 条输入被拆成 10+1 请求，向量顺序仍为 0 到 10；索引回归证明业务层仍一次提交 11 个 chunk，并把全部结果写入 FakeVectorStore。两项证据合起来覆盖 design，但它们使用 fake client 和 fake vector store，不能被描述成真实百炼或 Milvus 集成测试。\n📷 [图片 token=EM1Jbto12oiPVexLWnhc8zlWngg（未能下载，见飞书原文）]\n随后使用 /opsx:verify \u0026lt;name\u0026gt;，从三个维度审查：Completeness 检查任务和规格覆盖；Correctness 检查 Requirement/Scenario 与实现测试；Coherence 检查代码是否遵守 design 和仓库模式。verify 是系统化审查，不是测试命令的别名，也不是形式化证明。\n📷 [图片 token=MkhtbwfdFokge5xKxKfcm9XYneh（未能下载，见飞书原文）]\n步骤八：把 delta 合入 main specs /opsx:sync \u0026lt;change-name\u0026gt; 本地 sync 是 agent-driven 的智能合并，不是把 delta 文件覆盖到主规格。ADDED 新增 Requirement，MODIFIED 只调整目标部分并保留未提及场景，REMOVED 删除行为，RENAMED 处理名称变化。\n📷 [图片 token=LDNPbsBRqoIx1uxrBS7cCWSKnEg（未能下载，见飞书原文）]\n本案例的完成证据是 openspec/specs/qwen-openai-provider/spec.md 已包含 “Qwen embedding batch compatibility” Requirement 及三个 Scenario，同时既有 Provider 要求仍然存在。这个终态证明 delta 已沉淀到当前事实，但不反推本轮实际执行过某条 sync 命令。\n步骤九：归档完整决策历史 /opsx:archive \u0026lt;change-name\u0026gt; 当前本地 archive Skill 会检查 artifacts、未完成 tasks 和 delta 同步状态，然后把整个 change 移到 openspec/changes/archive/YYYY-MM-DD-\u0026lt;name\u0026gt;/。.openspec.yaml、proposal、design、tasks 和 delta specs 都随目录保留。\n📷 [图片 token=IABQb678wojVMBxNU3gcjfCYnQc（未能下载，见飞书原文）]\n归档不是删除，也不是 Git 回滚。归档目录只用于追溯；未来要调整批量策略，应建立新的 change，而不是修改旧档案来改写历史。\n步骤十：执行仓库特有的 wiki-sync python3 .codex/skills/wiki-sync/scripts/sync_wiki.py archive limit-qwen-embedding-batch-size npm run docs:build 📷 [图片 token=WD7KbjJKBopx7CxDttTcvRsnnMg（未能下载，见飞书原文）]\nwiki-sync 与 OpenSpec archive 是两个独立动作。脚本会扫描并重建 active/archive 页面、总索引和 Sidebar，检查 delta 与 main specs、include 路径和页面集合。聚合页通过 @include 引用 OpenSpec 原文件，不复制第二份正文。\nnpm run docs:build 验证 VitePress 能否生产构建，但不能替代 wiki-sync 对 include 目标和规格同步状态的检查。这里的 WIKI 是仓库文档站，也不等于当前飞书知识库。\n📷 [图片 token=Vn3Kb8i3yoR6FYxUGBNcamNqnkg（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E5%BC%80%E5%8F%91%E9%97%AD%E7%8E%AF/OpenSpec%E5%85%A8%E6%B5%81%E7%A8%8B%E5%AE%9E%E6%93%8D%E4%BB%8B%E7%BB%8D/","summary":"这一页不只列命令，而是把一次 OpenSpec 变更拆成“输入、产物、审查点和完成证据”。理解这条链以后，面对不同版本或不同 schema，即使文件名和快捷入口发生变化，也能判断当前应该做什么。 演练素材来自 OncallAgent 已归档","title":"OpenSpec全流程实操介绍"},{"content":"OncallAgent 仓库里不仅有前后端代码、测试和 OpenSpec 规范，还包含一套可以在浏览器中打开的项目知识站。VitePress 的任务，就是把 docs/ 中分散的 Markdown、安装教程和 OpenSpec 变更记录组织成带导航、目录与搜索的网页。它没有增加新的 AIOps 功能，却补上了仓库工程化交付中很重要的一层：让规范不只“存在于文件里”，还能够被人快速阅读和检查。\n📷 [图片 token=DIoWbGnqXojknyx155hccB6gnMf（未能下载，见飞书原文）]\n[!SUCCESS] VitePress 知识站和 OncallAgent 产品前端是两个不同入口。启动文档站不会启动 Vue 业务界面、FastAPI、Milvus、LLM、MCP 或告警服务，也不会自动把内容发布到公网或飞书。\n一、为什么代码仓库还需要一个知识站 直接阅读仓库文件当然可行，但当 OpenSpec 变更多起来以后，开发者需要在 proposal.md、design.md、tasks.md、delta specs 和主规格之间频繁跳转。对于刚接触项目的同学，这种目录式阅读很容易失去上下文；对于面试展示，也很难在几分钟内讲清一次需求经历了怎样的规划、实现、验证和归档。\n📷 [图片 token=PyeObOiTJoAkaSx09JGciZdqn9g（未能下载，见飞书原文）]\nVitePress 把这些内容转换成可浏览的页面，并提供顶部导航、侧边栏、本页目录和本地搜索。读者可以先看项目首页和安装说明，再进入变更索引查看某次 OpenSpec 的提案、设计、任务与规格。这样既保留了 Git 中可审查的 Markdown，又获得了更适合学习和演示的阅读体验。\n在这套设计里，OpenSpec 仍然是变更事实来源，VitePress 只是展示层。它不会替代规格，也不会把同一份正文复制成另一套需要人工维护的文档。\n📷 [图片 token=Az96b3bIfoPPDNx2NuucpbCBnuc（未能下载，见飞书原文）]\n二、从仓库结构理解 VitePress 的接入方式 OncallAgent 把 VitePress 安装在根 npm workspace 中，当前 package.json 声明的开发依赖为 vitepress ^1.6.4。因此所有文档命令都要在仓库根目录执行，而不是进入 apps/frontend。\n📷 [图片 token=KtfGb4ABKoS90Px3VRRcklrxnob（未能下载，见飞书原文）]\npackage.json 文档站命令和 VitePress 依赖 docs/index.md 知识站首页 docs/foundation.md 项目基础说明 docs/setup/ 各平台安装指南 docs/tutorials/ 实践教程 docs/changes/index.md OpenSpec 变更总索引 docs/changes/archive/... 已归档变更的聚合页面 docs/.vitepress/config.mts 导航、侧边栏、搜索等配置 docs/openspec -\u0026gt; ../openspec 指向 OpenSpec 源文件的符号链接 📷 [图片 token=TyfjbjSsvo6llUxKWVJcBpOln5e（未能下载，见飞书原文）]\ndocs/.vitepress/config.mts 不是普通的手写导航文件。它带有由 wiki-sync 生成的标记，变更列表和 Sidebar 会根据 OpenSpec 目录确定性重建。只为了增加一个变更入口而手工修改生成区域，下一次同步时很可能被覆盖。\ndocs/openspec 是关键桥梁。VitePress 页面通过相对路径和 @include 读取根目录中的 OpenSpec artifact，所以网页展示的是源文件内容，而不是一份复制后的“影子文档”。\n📷 [图片 token=Mwg8bdRmfoHqQdx2ChScKX5Enqc（未能下载，见飞书原文）]\n**名称提示。**当前仓库首页和生成脚本中仍保留 Super AI WIKI 这一历史标题。启动后看到它并不代表安装错误；它是尚未统一为 OncallAgent WIKI 的命名遗留。教程以项目统一名称 OncallAgent 进行说明，但不会把尚未修改的仓库状态写成已完成。\n三、第一次启动前先确认环境 文档站只依赖 Node.js 和根目录中的 npm 依赖。先打开 PowerShell、终端或 Codex 的仓库终端，进入包含 package.json、apps/、docs/ 和 openspec/ 的项目根目录，然后检查版本。\nnode --version npm --version 如果两条命令都能输出版本号，再在仓库根目录安装依赖：\nnpm install 这里不需要单独执行全局 npm install -g vitepress。仓库已经把 VitePress 声明为开发依赖，使用项目内版本更容易让不同同学获得一致结果。若 node 或 npm 无法识别，应先正确安装 Node.js，再重新打开终端。\n📷 [图片 token=VvVybUQVHoMP7txC2S4cgTSJnBf（未能下载，见飞书原文）]\n四、启动知识站并找到要看的页面 环境准备完成后，在仓库根目录运行：\nnpm run docs:dev 终端会打印本地访问地址，通常类似 http://localhost:5173。请以终端实际输出为准：如果端口已被占用，VitePress 可能选择其他端口。复制地址到浏览器后，就可以在本机查看知识站。开发服务器运行期间不要关闭该终端；结束时按 Ctrl+C。\n📷 [图片 token=VWewbfRQFoiZKGxMJKwc5HWenTb（未能下载，见飞书原文）]\n第一次打开后，建议按下面的顺序观察：\n在首页确认项目基础、安装运行、AIOps 实践和 AI Coding 教程等入口。\n进入“变更 WIKI”，打开 /changes/ 查看 OpenSpec 总索引。\n展开“已归档”，选择一次变更，观察 proposal、design、tasks 和 delta spec 如何连续展示。\n使用右上角本地搜索查找 capability、change 名称或技术关键词。\n查看页面右侧目录，理解标题结构如何帮助定位长文档内容。\n如果当前没有 active change，“进行中”分组为空是正常状态，不代表页面生成失败。是否存在进行中条目，应以 openspec/changes/ 当前目录为准。\n📷 [图片 token=UYHibJBzto4qhDxOkMfcWZ2gnmA（未能下载，见飞书原文）]\n五、开发、构建与预览解决的是三类问题 命令 作用 适用场景 npm run docs:dev 启动开发服务器 边修改 Markdown 边在浏览器中查看，支持热更新。 npm run docs:build 执行生产构建 检查 Markdown、导航和 VitePress 配置能否生成静态站点。 npm run docs:preview 预览构建产物 用接近最终静态站点的方式检查已经完成的构建结果。 📷 [图片 token=DHGbb5JCMo5UcuxK68ScQCzhnWd（未能下载，见飞书原文）]\n完成文档或导航调整后，至少应执行一次生产构建：\nnpm run docs:build npm run docs:preview 构建产物位于 docs/.vitepress/dist/，缓存位于 docs/.vitepress/cache/。这两个目录都被 Git 忽略，因为它们可以由源文件重新生成，不属于需要提交的项目源码。docs:preview 展示的是已有构建产物；源文档发生变化后，应重新运行 docs:build。\n📷 [图片 token=PwSTbenbjoyJbExAfavcTKD3nWg（未能下载，见飞书原文）]\n仓库当前提供的是本地开发、构建和预览能力，没有预设公网部署流程。课堂展示时可以在本机打开并投屏，不能把“本地可预览”描述成“已经部署上线”。\n六、OpenSpec、wiki-sync 与 VitePress 如何接成闭环 AGENTS.md 约束 Codex 的仓库行为 ↓ OpenSpec 保存 proposal、design、tasks 和 specs ↓ wiki-sync 生成变更聚合页、索引与 Sidebar ↓ VitePress 提供导航、目录、搜索和浏览页面 ↓ docs:build 验证文档站可以被正确构建 📷 [图片 token=LwkWb8uSTonRkZxKKVOcYxwWn5f（未能下载，见飞书原文）]\n新 change 已经具备 proposal、design、tasks 和 delta specs 后，可以同步 active 页面；归档完成后，再同步 archive 页面。仓库提供的确定性入口如下：\npython3 .codex/skills/wiki-sync/scripts/sync_wiki.py active \u0026lt;change-name\u0026gt; python3 .codex/skills/wiki-sync/scripts/sync_wiki.py archive \u0026lt;change-name-or-archive-name\u0026gt; python3 .codex/skills/wiki-sync/scripts/sync_wiki.py all 📷 [图片 token=Dbb3bmdEOoa8v1xvmtqcd5CSnoe（未能下载，见飞书原文）]\n裸创建 change 时如果还只有 .openspec.yaml，不要急着同步页面，因为 VitePress 聚合页需要引用完整 artifact。应先通过 OpenSpec 工作流补齐规划产物，再运行对应同步命令。关于同步文件、include 和归档校验的完整解释，可继续阅读 wiki-sync文件作用介绍。\nwiki-sync 的完整性校验和 npm run docs:build 不能互相替代。前者检查页面是否与 OpenSpec 目录对应、include 是否完整、导航顺序是否一致；后者检查 VitePress 能否真正完成构建。一次可靠交付需要保留两道门禁。\n📷 [图片 token=ACFobADYkoI2qsxdRWicKHu6nEe（未能下载，见飞书原文）]\n七、遇到问题时按现象排查 **提示找不到 vitepress。**先确认当前目录是仓库根目录，再运行 npm install。不要在 apps/frontend 中安装第二套 VitePress，也不需要依赖全局安装。\n**浏览器打不开默认端口。**查看终端实际打印的 Local 地址，不要固定认为一定是 5173。开发服务器退出后，原地址也会失效。\n**新增 change 后侧边栏没有变化。**VitePress 只展示已有文档源，不会自己扫描 OpenSpec 并改写导航。应在 artifact 完整后运行 wiki-sync，再观察 docs/changes/ 和生成配置的变化。\n📷 [图片 token=KU2AbxJIxoXoOax2RU8cfJmqnne（未能下载，见飞书原文）]\n**页面出现 include 缺失。**检查 docs/openspec 是否仍正确指向 ../openspec，以及 active、archive 页面是否引用了真实存在的 artifact。不要用复制正文的方式临时掩盖断链。\n**构建成功，但索引内容看起来不完整。**构建成功只能证明 VitePress 能处理当前输入，不能证明所有 change 都已经生成页面。继续运行 wiki-sync 的目录、include 和导航一致性检查。\n**页面标题显示 Super AI WIKI。**这是当前配置与生成脚本中的历史命名，不是浏览器缓存或安装错误。只有同时修改首页、配置模板和生成逻辑并通过构建后，才能称其已经统一为 OncallAgent WIKI。\n📷 [图片 token=S3Awb3Tw5oNQ43xamTIcnln3nxd（未能下载，见飞书原文）]\n八、VitePress 在仓库 Harness 中承担什么角色 Repository Harness 不是某一个命令或某一个工具，而是一组让 AI 和开发者都能稳定工作的仓库级约束。AGENTS.md 说明应该怎样工作，OpenSpec 记录准备做什么以及如何验收，wiki-sync 把变更映射成可浏览页面，VitePress 负责把这些页面呈现出来，docs:build 再提供可重复执行的文档质量门禁。\n📷 [图片 token=QwDEbjqvVo4dqKx73uCcsGDpnVe（未能下载，见飞书原文）]\n因此，VitePress 的价值不在于“做了一个好看的官网”，而在于把仓库中的规范、历史和教程变成可查、可讲、可验证的工程资产。对学生来说，它能帮助理解一次变更的完整上下文；对项目展示来说，它让面试官看到的不只是最终功能，还有需求如何被记录、执行和验收。这正是 OncallAgent 从“能运行的项目”走向“可持续协作的 AI Native 仓库”所需要的展示层。\n📷 [图片 token=W7zGb6PoLoThdaxxziAcgyegnFh（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E5%BD%92%E6%A1%A3%E4%B8%8E%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/VitePress%EF%BC%9A%E5%90%AF%E5%8A%A8%E4%B8%8E%E6%B5%8F%E8%A7%88%20OncallAgent%20%E9%A1%B9%E7%9B%AE%E7%9F%A5%E8%AF%86%E7%AB%99/","summary":"OncallAgent 仓库里不仅有前后端代码、测试和 OpenSpec 规范，还包含一套可以在浏览器中打开的项目知识站。VitePress 的任务，就是把  docs/  中分散的 Markdown、安装教程和 OpenSpec 变更记录","title":"VitePress：启动与浏览 OncallAgent 项目知识站"},{"content":" [!SUCCESS] **先给结论：**原有知识库和新增知识库都要用，但学习目的不同。原有知识库帮助你理解项目、讲清项目；新增知识库训练你怎样使用 Codex、OpenSpec 等工具规范地完成开发。新知识库不能只浏览一遍，更不能停留在看文档、看源码，至少要亲手完成一次真实的 AI Coding 实践。\n很多同学看到两套资料后，第一反应是：“是不是两套都要从头到尾学完，才能参加面试？”这个问题不能简单回答“是”或“不是”。你不需要逐篇打卡、把所有文章全部背完，但必须弄清两套资料分别培养什么能力，并把新知识库中的核心方法真正用起来。\n原有项目知识库主要围绕 OncallAgent 的项目背景、设计立意、核心概念、功能模块和实现思路展开，帮助你回答“这是一个什么项目、为什么要做、主要解决什么问题”。新增工程化知识库关注的则是“在 AI 参与开发的情况下，怎样把一个想法规范地变成可以落地的工程任务”。它训练的不是多记几个技术名词，而是使用 AI、组织 AI、约束 AI 和推进交付的能力。\n📷 [图片 token=Wgd6bL06hoDDcfxocJlcl2iInOg（未能下载，见飞书原文）]\n一个帮助你理解项目，一个训练你驾驭 AI 两套知识库不是简单的新旧替换关系，也不是同一份内容换一种写法。它们分别回答两类不同的问题。\n学习维度 原有项目知识库 新增工程化知识库 核心定位 项目讲解与项目立意 AI Coding 规范化与工程落地 主要问题 OncallAgent 是什么，为什么这样设计，各模块怎样协作 怎样让 Codex 理解需求、遵守规则、按计划完成开发并减少返工 学习重点 项目背景、AI 概念、RAG、Agent、MCP、架构思路、功能流程和项目表达 Codex、OpenSpec、SDD、AGENTS.md、需求澄清、方案规划、任务拆分、实现、验证和归档 正确学法 理解项目逻辑，运行项目，建立完整的项目认知 打开真实项目，亲手发起变更，让 AI 在规范和流程下完成一次工程实践 面试作用 帮助你讲清项目价值、业务场景、技术方案和核心功能 帮助你讲清自己怎样使用和驾驭 AI，怎样把模糊需求推进为可交付任务 可以把两者理解为两层能力：原有知识库让你“有项目可讲”，新增知识库让你“有 AI 工程协作过程可讲”。前者仍然重要，但在 AI 已经进入日常开发的环境里，只会介绍项目功能、背诵架构名词，已经不足以完整展示自己的开发能力。\n📷 [图片 token=YyFCbzErDoD4ENxXdV2c5u0hncg（未能下载，见飞书原文）]\n为什么新的知识库一定要学 过去学习一个项目，常见方式是先看教程，再看源码，最后把项目运行起来。这样能够帮助你理解系统，但它容易让学习停留在“别人已经做完了，我负责阅读和复述”的状态。进入 AI Coding 之后，开发者更重要的能力，是能不能把 AI 变成一个可协作、可管理的工程伙伴。\n📷 [图片 token=Sx9ibqQGxo4CKsxuIp9cztfVnbS（未能下载，见飞书原文）]\nCodex 可以快速阅读项目、生成代码、修改文件和运行检查，但它并不会自动知道你的真实目标。需求说得不清楚，它就可能快速跑偏；边界没有定义，它可能顺手修改不该动的地方；完成标准没有说明，它也可能在“看起来已经做好”的位置提前停止。真正有价值的能力，不是会不会把一句需求发给 AI，而是能否持续掌握方向。\n📷 [图片 token=TkRSbK1SyoJMqZxzgtAcNeqRnGh（未能下载，见飞书原文）]\nOpenSpec、SDD 和 AGENTS.md 等内容，正是为了解决这类问题。OpenSpec 帮助你把想法整理成 proposal、spec、design 和 tasks，再进入实现、验证与归档；AGENTS.md 把仓库中长期有效的规则告诉 Codex；SDD 则让开发围绕明确的规格推进，而不是完全依赖临时对话。学习这些内容，本质上是在学习怎样给 AI 建立目标、边界、节奏和完成标准。\n📷 [图片 token=TqOvbfQJNop0KOxg3ZvcNT7mnZb（未能下载，见飞书原文）]\n**因此，新知识库不是原有项目讲解的附录，而是面向 AI 时代的一套开发能力训练。**即使你已经把原有项目学得很熟，也应该继续完成这部分学习。\n看完文档，不等于掌握 AI Coding 这套新知识库最容易出现的误区，是把 Codex、OpenSpec 和 AGENTS.md 当成新的面试知识点：看完概念、记住几个命令、浏览一下生成的文件，然后就认为自己已经掌握了。实际上，这样得到的仍然只是“知道”，还没有变成“会用”。\n📷 [图片 token=DaYdbNNDgonvnzxEN9Xc7Fvinvd（未能下载，见飞书原文）]\n真正的学习必须进入项目。你需要亲自打开 OncallAgent，选择一个范围合适的真实需求，例如完善一处用户提示、调整一个页面状态、补充一个小功能，或者修复一个能够复现的问题。然后让 Codex 和 OpenSpec 陪你走完整个过程。\n实践中，你会遇到文档里无法替代的真实问题：最初的需求是否足够清楚，哪些内容应该明确排除，方案是不是设计得过重，任务拆分是否可以逐项完成，AI 有没有误解你的意思，什么时候应该暂停生成并重新调整方向。只有亲手处理过这些问题，你才会逐渐理解“驾驭 AI”到底意味着什么。\n📷 [图片 token=WR7tbgiwHos8Hzx3iOCcGH7Hnte（未能下载，见飞书原文）]\n**源码解析文章可以作为地图，帮助你找到练习入口；但阅读地图不是走完路线。**学习结果不应该是“我看过多少篇文章”，而应该是“我是否真的使用这套流程完成过一次变更”。\n至少亲手完成一次 OpenSpec 开发闭环 第一次实践不需要选择很大的功能。需求越小，越容易把注意力放在方法本身。建议按照下面的顺序完成一次闭环：\n**先让 Codex 了解项目规则。**打开真实仓库，阅读项目说明和 AGENTS.md，确认本次练习允许修改什么、不应该修改什么。\n**把想法说清楚。**说明目标、使用场景、范围、非目标和完成条件。如果自己还没有想明白，先与 Codex 讨论，不要急着生成代码。\n**使用 OpenSpec 建立变更。**依次理解 proposal、spec、design 和 tasks 各自解决的问题，不要只关注文件有没有生成。\n**按任务推进实现。**让 Codex 围绕已经确认的方案逐步完成，过程中发现方向不对就及时停下来修正规格或任务。\n**完成验证和复盘。**检查需求是否真正完成，回顾 AI 在哪里理解正确、在哪里发生偏差、自己又是怎样调整的。\n**归档这次变更。**把这次练习从临时对话沉淀为项目可以继续使用的工程记录。\n走完一次之后，再选择第二个不同类型的小任务练习会更有效。例如第一次做小功能，第二次处理缺陷或优化。重复几次以后，你会发现自己与 AI 的沟通会从“想到什么说什么”，逐渐变成有目标、有步骤、有边界的工程协作。\n📷 [图片 token=E9i2bHRgiodq7JxhyfmcfSaanAd（未能下载，见飞书原文）]\n根据自己的基础安排学习顺序 **第一次学习 OncallAgent。**先使用原有知识库建立项目认知，理解项目为什么存在、主要解决什么问题，以及 RAG、Agent、MCP 等概念在项目中扮演什么角色。具备基本认识后，就进入新知识库学习 Codex 与 OpenSpec，并尽快开始第一次实践。不要等到“所有项目文章都看完”才动手。\n📷 [图片 token=U4OWbj4xYo1nH4xL0ZmcwweHn8B（未能下载，见飞书原文）]\n**已经学完原有项目。**不需要重新学习已经掌握的项目讲解，可以直接把新知识库作为下一阶段主线。从 Codex、SDD、OpenSpec 和 AGENTS.md 开始，然后选择一个自己能够控制范围的小需求完成闭环。你的重点应从“这个项目是怎样设计的”转向“我怎样借助 AI 继续开发这个项目”。\n**正在准备面试。**原有知识库用于整理项目介绍、设计立意和功能理解，新知识库用于准备 AI Coding 实践。不要只背 OpenSpec 会生成哪些文件，而要真正做一次，并把过程中最有价值的判断记录下来。时间有限时，也应优先完成一个小而完整的实践，而不是快速浏览更多文章。\n📷 [图片 token=DiFVbHSuJoimQSxTOkjc8Njynkf（未能下载，见飞书原文）]\n面试中真正值得讲的是你的使用过程 面试官如果继续追问 AI Coding，往往不会满足于“我用过 Codex”或“我安装了 OpenSpec”。更有区分度的内容，是你怎样让 AI 在一个真实项目中稳定工作。你应该能够自然地回答下面这些问题：\n为什么没有拿到需求就直接让 AI 写代码？\n你是怎样向 AI 描述目标、边界和完成条件的？\nproposal、spec、design、tasks 分别帮助你解决了什么问题？\nAI 曾经在哪个环节理解偏了，你是怎样发现并纠正的？\n哪些判断必须由你自己完成，哪些工作适合交给 Codex？\n为什么完成实现后还需要验证、复盘和归档？\n使用这套流程以后，返工、沟通和任务失控的问题发生了什么变化？\n这些问题不能靠背诵概念回答。只有真正做过，你才能把需求怎样变化、方案怎样调整、AI 怎样执行、自己怎样做判断讲得具体。到了这个程度，OpenSpec 就不再是简历上的一个工具名称，而会成为你讲述 AI 工程实践的一条完整主线。\n📷 [图片 token=MDqGbbqW1owintxyamlcT6Nrnrh（未能下载，见飞书原文）]\n学习完成的标准不是“看完”，而是“做过” 你不必为了面试把两套知识库的每一篇文章全部学完，但新的知识库一定要进入实践。至少应达到以下状态：你亲手在真实项目中使用过 Codex；独立走过一次 OpenSpec 变更流程；能够用自己的语言解释每个阶段为什么存在；遇到 AI 跑偏时知道怎样暂停、澄清和调整；能够完整讲述一次从需求提出到完成归档的经历。\n达到这个标准后，你在面试中谈论的不再只是“OncallAgent 有哪些功能”，还可以继续讲“我是怎样利用 AI 推进开发的”“怎样防止 AI 随意发挥”“怎样把一次对话变成规范化工程过程”。这正是新知识库希望补充的核心能力。\n最后可以记住一句话：原有知识库重点回答“项目是什么”，新增知识库重点回答“我是怎样使用 AI 把项目做出来并继续做下去的”。项目知识要理解，AI Coding 方法更要亲手实践。只有真正用过，面试时才能讲得具体、从容，也经得起继续追问。\n📷 [图片 token=AxKrb3LL5oMUelxOu4YczXHtnkd（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/%E4%B8%A4%E5%A5%97%E8%B5%84%E6%96%99%E6%80%8E%E4%B9%88%E5%AD%A6%EF%BC%9A%E5%AE%9A%E4%BD%8D%E5%8C%BA%E5%88%AB%E3%80%81%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E4%B8%8E%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87/","summary":"!SUCCESS   先给结论： 原有知识库和新增知识库都要用，但学习目的不同。原有知识库帮助你理解项目、讲清项目；新增知识库训练你怎样使用 Codex、OpenSpec 等工具规范地完成开发。新知识库不能只浏览一遍，更不能停留在看文档、看","title":"两套资料怎么学：定位区别、学习路线与面试准备"},{"content":"前两章讲了大模型的底层逻辑，也讲了怎么写好 Prompt。到现在，你已经知道大模型是个\u0026quot;超级大脑\u0026quot;，能理解意图、会逻辑推理、能生成高质量内容。\n但它只有嘴、没有手：它能说\u0026quot;你应该查一下慢查询日志\u0026quot;，但它自己查不了；它能分析\u0026quot;这个 API 可能是瓶颈\u0026quot;，但它发不了报警、关不了开关、也打不开任何一个文件。\n学会了 Prompt，你能精准控制大模型的输出，但有一个问题 Prompt 解决不了：就算大模型能给出完美的分析，它依然没法主动去查数据库、发通知、调用 API。\u0026ldquo;说得再好\u0026rdquo;，也只是在说。\n这就是 Agent 要解决的问题：让大模型从\u0026quot;只能说\u0026quot;变成\u0026quot;能干\u0026quot;。\n什么是 Agent？ Agent（智能体），是一个以大模型为\u0026quot;大脑\u0026quot;，能自主感知环境、做出决策、调用工具、完成多步骤任务的程序。\n📷 [图片 token=M6iBbYhCXobgA1xcdIQcFMbOnlc（未能下载，见飞书原文）]\n拆开来看几个关键词：\n自主（Autonomously）：不需要人在每一步发指令。你给它一个目标，它自己判断下一步做什么\n多步骤（Multi-step）：一个任务可能要走 5 步、10 步，Agent 自己把这些步骤串起来\n工具调用（Tool use）：调用真实的函数，查数据库、搜网页、发通知、写文件，不只是\u0026quot;说说\u0026quot;，而是真的去做\n用一个类比帮你建立直觉：\n大模型单独使用，相当于一个坐在椅子上只会出主意的顾问。你问什么他答什么，但他不会起身去做任何事。Agent 是给这个顾问配了一双手：他不仅能给建议，还能拿起电话查数据、发邮件、去仓库核实情况，然后根据新信息继续推进。\n一句话总结：Agent = 大模型（大脑）+ 工具（双手）+ 执行循环（思考-行动-观察-再思考）\n为什么需要 Agent 之前我们说过大模型的三大短板：\n没有执行能力：只能生成文字，不能操作外部系统\n知识有截止日期：不知道最新数据，看不到你的私有系统\n没有持久记忆：每次调用对它都是全新的，不能积累任务中间状态\n举一个真实的 OnCall 场景：「数据库今晚 23:00 报警，帮我查清楚原因」\n大模型单独面对这个任务能做什么？只能说\u0026quot;可能是慢查询、可能是连接池满、可能是锁争用……\u0026quot;，它根本拿不到你的报警日志，查不了慢查询记录，也看不到当时的系统状态。没有 Agent，大模型只是个猜测机。\n那传统脚本能解决这个问题吗？也不行，方向不一样。\n传统脚本和 Workflow 是写死的逻辑，\u0026ldquo;如果 CPU \u0026gt; 80% 就发警报，如果连接超时就重启服务\u0026rdquo;。这些流程是你提前写好的，遇到没预料到的情况，它们不知道该怎么办。一个新类型的故障，脚本一行代码都没有，Workflow 一个分支都没有，就是束手无策。\nAgent 的价值在于：把大模型的灵活推理能力和真实工具的执行能力结合在一起，让系统能处理那些你没法提前穷举所有情况的复杂任务。\nAgent 的核心组成 Agent 由四个组件构成，缺一不可：\n📷 [图片 token=QtW4bBtJUo3Ulmxs41icix49nUg（未能下载，见飞书原文）]\n大模型（LLM），大脑 负责理解任务、分析当前状态、决定下一步做什么。所有的\u0026quot;推理\u0026quot;都在这里发生，该查什么、查完之后怎么解读、下一步往哪走，全是大模型来判断。\n工具（Tools），双手 一个个可以被调用的真实函数：查数据库、调用搜索引擎、读写文件、发通知、调用 API……\n大模型告诉 Agent \u0026ldquo;调用这个工具、传这些参数\u0026rdquo;，代码层面真正去执行。工具的结果会返回给大模型，供它继续推理。大模型不\u0026quot;直接\u0026quot;操作外部系统，它通过工具来\u0026quot;动手\u0026quot;。\n记忆（Memory），记事本 短期记忆：当前任务里的对话历史 + 每次工具调用的结果，存在 context window 里。Agent 执行第 5 步时，\u0026ldquo;记得\u0026quot;第 1 步查到了什么，靠的就是这个\n长期记忆：跨会话需要记住的信息，存在外部数据库，需要时检索出来注入 context\n为什么需要记忆？多步骤任务里，每一步的结果都是下一步判断的依据。没有记忆，每步都是\u0026quot;从零开始\u0026rdquo;，Agent 根本没法串联多步骤任务。\n执行循环（Loop），节拍器 Agent 的\u0026quot;引擎\u0026quot;。不断重复\u0026quot;思考 → 行动 → 观察\u0026quot;这个循环，直到任务完成。没有这个循环，Agent 只是一次性的问答，无法串联多步骤。这个循环是 Agent 和普通大模型调用之间最根本的区别。\nAgent 是怎么工作的，ReAct 循环 目前最主流的 Agent 工作范式叫做 ReAct，全称 Reasoning + Acting：推理（Think）→ 行动（Act）→ 观察（Observe）→ 再推理……\n📷 [图片 token=O42cb2tpkoMmWpxivH5c0K3dn3f（未能下载，见飞书原文）]\n用一个完整的 OnCall 场景走一遍：\n用户：帮我调查今晚 23:00 的数据库报警 [第 1 轮] Think：需要先拿到具体的报警详情 Act：调用 get_alarm_details(time=\u0026#34;23:00\u0026#34;) Observe：返回\u0026#34;连接数突增，连接池耗尽，持续 8 分钟后自动恢复\u0026#34; [第 2 轮] Think：连接数突增，需要看是哪些查询造成的 Act：调用 query_slow_log(timerange=\u0026#34;22:50-23:10\u0026#34;) Observe：发现 3 条全表扫描的慢查询，来自同一个用户 ID [第 3 轮] Think：根因已经清楚，生成分析报告 Act：输出最终分析结论（Final Answer） 几个关键机制：\n结果回流：每一轮的工具调用结果都会追加到 context window 里，大模型下一轮能\u0026quot;看到\u0026quot;所有历史。所以它能基于已有发现继续推理，而不是每次都从零开始。这就是为什么前面\u0026quot;记忆\u0026quot;那个组件如此重要。\n动态决策：每一步该怎么走，是大模型实时推理出来的，不是你提前写好的。发现是慢查询，它就去查慢查询日志；如果发现是网络问题，它就会去查网络相关的工具。路径是活的，不是死的。\n何时停止：大模型判断任务已完成时输出\u0026quot;最终答案\u0026quot;，或者达到预设的最大轮数上限。两个条件任意满足一个，循环结束。\n这就是\u0026quot;自主\u0026quot;的含义，你给任务目标，它自己决定怎么走到终点。\nAgent 的另一种工作模式，Plan and Execute ReAct 很好用，但有一个先天的弱点。\nReAct 是「边想边做」，每一步只看眼前，这一步的结果决定下一步的方向。对于 2-3 步能解决的短任务，这完全没问题。但如果任务很复杂、步骤很多呢？\n📷 [图片 token=B3P1bajLMoXYfhx2bNkcSLGFnmf（未能下载，见飞书原文）]\n问题一：长任务容易「迷路」\nReAct 跑了 8 轮之后，context window 里已经堆满了历史操作记录。大模型要在这一大堆信息里同时维持「当前在第几步」「最终目标是什么」「还缺哪些信息」，注意力越来越稀释，越往后越容易偏离最初的目标。\n问题二：容易绕弯路\n没有全局规划，每步只看眼前，就像在迷宫里随机探索，走了 5 步之后才发现方向不对，只能回头重来，白白消耗了好几轮 LLM 调用。\n问题三：token 消耗滚雪球\nReAct 每一轮都要把完整的对话历史带上。步骤越多，每轮的 context 越长，后期每次 LLM 调用的 token 消耗都很高，成本直线上升。\n这三个问题催生了另一种工作模式：Plan and Execute（规划-执行模式）。\n核心思路用一句话说清楚：先让大模型把整个任务规划成一份清单，再逐步按清单执行，而不是走一步看一步。\n📷 [图片 token=AtM8bKUSCohO6JxlyFzcjc19nvh（未能下载，见飞书原文）]\n用出行来类比两种模式的区别：\nReAct = 边开车边看导航：每走到一个路口，才决定下一步拐哪。灵活，但如果最开始方向就偏了，容易绕很多冤枉路\nPlan and Execute = 出发前规划好完整路线：先打开地图，把走哪条路、哪里转弯、预计几点到全部规划好，再出发。执行时按路线走，不用每个路口重新想\nPlan and Execute 的两个阶段 第一阶段：Planner（规划阶段）\n大模型拿到任务，先不调任何工具，专注做一件事：把任务完整拆解成一份有序的子任务清单。\n用户：「帮我调查今晚 23:00 的数据库报警，写一份完整的故障分析报告」\n用户：「帮我调查今晚 23:00 的数据库报警，写一份完整的故障分析报告」 Planner 输出的计划： 步骤 1：获取 23:00 报警的详细信息（报警类型、持续时间、影响范围） 步骤 2：查询报警时间段内的慢查询日志，定位异常 SQL 步骤 3：查询报警时间段内的数据库连接数、CPU、内存指标 步骤 4：关联步骤 2 和步骤 3 的结果，分析根因 步骤 5：生成完整的故障分析报告，包含时间线、根因和改进建议 第二阶段：Executor（执行阶段）\n计划有了，开始逐步执行。每个步骤可以是一次简单的工具调用，也可以是一个小的 ReAct 循环，步骤复杂就跑几轮，步骤简单就一步到位。\n📷 [图片 token=Ceo9bcmNlo7L4xxcHIzcSDfwn1c（未能下载，见飞书原文）]\n执行步骤 1： → 调用 get_alarm_details(time=\u0026#34;23:00\u0026#34;) ← 返回「连接数突增，连接池耗尽，持续 8 分钟后自动恢复」 执行步骤 2： → 调用 query_slow_log(timerange=\u0026#34;22:50-23:10\u0026#34;) ← 发现 3 条全表扫描的慢查询，来自同一个用户 ID 执行步骤 3： → 调用 get_db_metrics(timerange=\u0026#34;22:50-23:10\u0026#34;) ← CPU 正常，连接数峰值达到上限 500，内存无异常 执行步骤 4： → 大模型综合步骤 2、3 结果进行分析 ← 结论：慢查询导致连接长时间占用，连接池耗尽触发报警 执行步骤 5： → 生成故障分析报告 ← 完整报告输出，任务完成 还有一个重要机制：Re-planning（重新规划）。执行中途如果遇到了计划没预料到的情况，比如步骤 2 查不到慢查询，但发现了大量锁等待，Executor 可以把这个新信息反馈给 Planner，重新生成后续步骤的计划，而不是一条路走到黑。\n用一张图把整个流程串起来：\n📷 [图片 token=MpFwb9oKHodjlCxP2OVcBqVCn3y（未能下载，见飞书原文）]\n用户任务 ↓ [Planner] 大模型一次性生成完整计划 ↓ 执行计划 = [步骤1, 步骤2, 步骤3, 步骤4, 步骤5] ↓ [Executor] 按顺序执行每个步骤 步骤1 → 调工具 → 结果 步骤2 → 调工具 → 结果 ← 遇到意外？触发 Re-planning，重新调整后续计划 步骤3 → 调工具 → 结果 ... ↓ 最终结果汇总 → 输出给用户 ReAct vs Plan and Execute：怎么选？ 613kxO 对比维度 ReAct Plan and Execute 决策时机 每一步实时决策 开始前一次性规划 全局视野 弱（只看当前这步） 强（提前看到全貌） 灵活性 强（随时根据结果调整） 弱（计划一旦生成，调整成本高） 适合任务步骤 少（3 步以内） 多（5 步以上的复杂任务） token 消耗 随步数线性增长，后期很贵 规划阶段集中消耗，执行阶段可控 实现难度 低（结构简单） 中（需要管理计划状态和 Re-planning 逻辑） 简单记忆：\n任务步骤少、目标模糊、需要随机应变 → 用 ReAct\n任务步骤多、目标明确、需要全局把控 → 用 Plan and Execute\n系统复杂、两者都需要 → Plan and Execute 做外层框架，每个子任务内部跑 ReAct\n最后这种「外层 Plan and Execute + 内层 ReAct」的搭配，在实际生产系统里最常见，用规划保证大方向不跑偏，用 ReAct 保证每个子任务执行时足够灵活。\nAgent vs Workflow，什么时候用哪个 这是初学者最容易搞混的问题，也是实际选型最关键的判断。\n📷 [图片 token=UKJ7bLvZ5oAjjRx2kTDcuslUnzx（未能下载，见飞书原文）]\nWorkflow（工作流） 是你提前写好所有步骤和分支逻辑的流程：先做 A，再做 B，如果 B 的结果是 X 就走流程 C，否则走流程 D。大模型只是其中某个步骤里被调用一次，整体流程是硬编码的。\nAgent 是你只给任务目标，大模型自己实时决定每一步做什么、调用什么工具、根据结果决定下一步。执行路径是动态的，每次运行可能走不同的路径。\nTw6CHR Workflow Agent 决策者 你（写死的代码逻辑） 大模型（实时推理） 执行路径 固定 动态 适合场景 步骤明确、重复性高 任务开放、情况多变 可预测性 高 低 成本 低 高（多轮 LLM 调用） 两者没有高下之分，适合不同的场景：\n流程能写清楚、步骤是固定的 → 优先用 Workflow，更稳、更省钱、更可预期\n任务是开放式的、需要根据中间结果动态决策 → 用 Agent\n最常见的实际架构：Workflow 做骨架，Agent 负责其中需要判断的环节，两者结合，不是非此即彼\nAgent 开发的真实挑战 学完了怎么好用，也要知道坑在哪，不然上手就容易踩雷。\n📷 [图片 token=PywAbDiIKoK5UBxdPprctdRMnYf（未能下载，见飞书原文）]\n挑战 1：幻觉传导\n大模型在第一步做出错误判断，后续所有步骤都建立在这个错误假设上，最终结论可能完全跑偏。单步幻觉在普通问答里代价不大，但在 Agent 的多步骤链条里，第一步的错误会被逐步放大。\n应对：工具结果要可验证；对高风险操作（写入数据库、发通知、执行脚本）加人工确认步骤。\n挑战 2：工具调用失败\n工具不是百分之百可靠，网络超时、返回格式异常、权限不足……Agent 没有合适的 fallback 设计，一个工具失败就可能卡死或走偏。\n应对：每个工具都要有明确的错误返回格式；System Prompt 里说清楚\u0026quot;如果工具返回错误应该怎么处理\u0026quot;。\n挑战 3：成本和速度\n每一轮循环都是一次 LLM 调用，有 token 消耗和时延。一个任务如果跑 10 轮，消耗是单次问答的 10 倍。用 Agent 是有代价的。\n应对：合理设置最大循环轮数上限；能用 Workflow 解决的不要用 Agent。\n挑战 4：循环风险\nAgent 可能陷入\u0026quot;查了 A，发现需要查 B，查完 B 又觉得需要查 A……\u0026ldquo;的无效循环。\n应对：设置最大迭代次数，超出就强制结束；让大模型在每步推理时明确判断\u0026quot;当前信息是否已经足够得出结论，还有没有必要继续\u0026rdquo;。\n一个真实 Agent 的样子 说了这么多，Agent 代码层面长什么样？用伪代码展示一个极简的 Agent 骨架：\ndef run_agent(user_message): messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: SYSTEM_PROMPT}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_message}, ] while True: # 1. 调用大模型，让它思考下一步 response = llm.call(messages) # 2. 如果大模型说\u0026#34;我完成了\u0026#34;，退出循环 if response.is_final_answer: return response.content # 3. 否则，执行大模型指定的工具 tool_result = execute_tool(response.tool_name, response.tool_args) # 4. 把工具结果追加到 messages，供下一轮参考 messages.append({\u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;content\u0026#34;: tool_result}) 这就是 Agent 的骨架，整个循环的核心逻辑就这么点：\n📷 [图片 token=KinXbwcojo6EyCxvr0ycByQkntM（未能下载，见飞书原文）]\n调大模型，让它决策\n判断是否完成，完成就退出\n没完成就执行工具，把结果追加进 messages\n带着新结果再调大模型，进入下一轮\n真实系统会在这个基础上加：错误处理、最大轮数限制、日志记录、人工审批步骤等。但骨架就这些，不复杂。你读懂了这段伪代码，Agent 的核心机制就装进脑子里了。\n总结 整理一下这一章的核心认知：\nAgent 是什么：以大模型为大脑，能自主调用工具、完成多步骤任务的程序。让大模型从\u0026quot;只能说\u0026quot;变成\u0026quot;能干\u0026quot;。\n核心组成：大模型（大脑）+ 工具（双手）+ 记忆（记事本）+ 执行循环（节拍器）\n核心机制：Think → Act → Observe 循环，每轮工具结果都回流给大模型，直到完成\n和 Workflow 的本质区别：决策者不同，Workflow 是你提前写好的代码逻辑，Agent 是大模型实时推理。两者不是竞争关系，实际系统里经常结合使用\n后续章节呼应：\nFunction Calling：大模型怎么\u0026quot;告诉\u0026quot;代码层调用哪个工具，这是 Agent 工具调用的底层机制，也是这段伪代码里 response.tool_name 和 response.tool_args 怎么来的\nRAG：Agent 怎么访问私有知识库，本质上是一种特殊的工具，但重要到单独讲\nMCP：工具的标准化协议，让 Agent 能接入更广泛的生态工具，不用每次都自己写\n带着这章建立起来的 Agent 整体架构认知，后续每个章节都是在填充这个架构里的某个具体模块。你知道它在整体里处于什么位置，学起来就不会迷失方向。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%20Agent%EF%BC%9F/","summary":"前两章讲了大模型的底层逻辑，也讲了怎么写好 Prompt。到现在，你已经知道大模型是个\u0026quot;超级大脑\u0026quot;，能理解意图、会逻辑推理、能生成高质量内容。  但它只有嘴、没有手：它能说\u0026quot;你应该查一下慢查询日志\u0026quot;，但它自己查不了；它能分析\u0026quot;这个 API","title":"什么是 Agent？"},{"content":"Eino Eino[‘aino] (近似音: i know，希望框架能达到 “i know” 的愿景) 旨在提供基于 Go 语言的终极大模型应用开发框架。 它从开源社区中的诸多优秀 LLM 应用开发框架，如 LangChain 和 LlamaIndex 等获取灵感，同时借鉴前沿研究成果与实际应用，提供了一个强调简洁性、可扩展性、可靠性与有效性，且更符合 Go 语言编程惯例的 LLM 应用开发框架。\n实现一个简单的LLM对话 构造Message func createMessages(ctx context.Context) []*schema.Message { // 创建模板，使用 FString 格式 template := prompt.FromMessages(schema.FString, // 系统消息模板。这里本质就是创建 system prompt schema.SystemMessage(\u0026#34;你是一个{role}。你需要用{style}的语气回答问题。你的目标是帮助程序员保持积极乐观的心态，提供技术建议的同时也要关注他们的心理健康。\u0026#34;), // 插入需要的对话历史（新对话的话这里不填） schema.MessagesPlaceholder(\u0026#34;chat_history\u0026#34;, true), // 用户消息模板。这里本质就是创建 user prompt schema.UserMessage(\u0026#34;问题: {question}\u0026#34;), ) // 下面的template.Format就是用一个map，把对应的key填充到对应的{key}的位置里 messages, _ := template.Format(ctx, map[string]any{ \u0026#34;role\u0026#34;: \u0026#34;程序员鼓励师\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;积极、温暖且专业\u0026#34;, \u0026#34;question\u0026#34;: \u0026#34;我的代码一直报错，感觉好沮丧，该怎么办？\u0026#34;, // 对话历史（这个例子里模拟两轮对话历史） \u0026#34;chat_history\u0026#34;: []*schema.Message{ schema.UserMessage(\u0026#34;你好\u0026#34;), schema.AssistantMessage(\u0026#34;嘿！我是你的程序员鼓励师！记住，每个优秀的程序员都是从 Debug 中成长起来的。有什么我可以帮你的吗？\u0026#34;, nil), schema.UserMessage(\u0026#34;我觉得自己写的代码太烂了\u0026#34;), schema.AssistantMessage(\u0026#34;每个程序员都经历过这个阶段！重要的是你在不断学习和进步。让我们一起看看代码，我相信通过重构和优化，它会变得更好。记住，Rome wasn\u0026#39;t built in a day，代码质量是通过持续改进来提升的。\u0026#34;, nil), }, }) return messages } 创建大模型的入口 注意这里的api key记得替换成你的api key哦\nfunc openAIForDeepSeekV3Quick(ctx context.Context) (cm model.ToolCallingChatModel, err error) { config := \u0026amp;openai.ChatModelConfig{ APIKey: \u0026#34;\u0026#34;, Model: \u0026#34;deepseek-v3-1-terminus\u0026#34;, BaseURL: \u0026#34;https://ark.cn-beijing.volces.com/api/v3\u0026#34;, } cm, err = openai.NewChatModel(ctx, config) if err != nil { return nil, err } return cm, nil } 与LLM交互 func main() { ctx := context.Background() // 使用模版创建messages log.Printf(\u0026#34;===create messages===\\n\u0026#34;) messages := createMessages(ctx) log.Printf(\u0026#34;messages: %+v\\n\\n\u0026#34;, messages) // 创建llm log.Printf(\u0026#34;===create llm===\\n\u0026#34;) cm, err := openAIForDeepSeekV3Quick(ctx) if err != nil { panic(err) } log.Printf(\u0026#34;create llm success\\n\\n\u0026#34;) log.Printf(\u0026#34;===llm generate===\\n\u0026#34;) result, err := cm.Generate(ctx, messages) if err != nil { panic(err) } log.Printf(\u0026#34;result: %+v\\n\\n\u0026#34;, result.Content) } 📷 [图片 token=OKXtblhQ1oiixQxiTVrctAHCnfe（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Go%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8eino%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95AI%E5%AF%B9%E8%AF%9D%28Go%29/","summary":"Eino Eino\\ ‘aino\\  (近似音: i know，希望框架能达到 “i know” 的愿景) 旨在提供基于 Go 语言的终极大模型应用开发框架。 它从开源社区中的诸多优秀 LLM 应用开发框架，如 LangChain 和 Ll","title":"使用eino框架3分钟实现一个简单AI对话(Go)"},{"content":"FastAPI 是什么 FastAPI 是一个现代、高性能的 Python Web 框架，用于构建 API 服务。它基于 Python 类型提示，能够自动生成交互式 API 文档（Swagger UI），开发体验非常友好。\n核心特点：\n高性能：基于 Starlette 和 Pydantic，性能与 Go、Node.js 框架相当\n开发快速：利用类型提示自动补全和校验，减少大量样板代码\n自带文档：启动即自动生成 Swagger UI 和 ReDoc 交互式文档，无需额外配置\n易于上手：几行代码即可启动一个 HTTP 服务\n官方文档：https://fastapi.tiangolo.com/\n源码：[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/) 使用fastapi实现一个http接口\n安装依赖 pip install fastapi uvicorn fastapi：Web 框架本身\nuvicorn：ASGI 服务器，用于运行 FastAPI 应用\n新增 Chat 接口 我们通过一个 chat 接口来演示如何使用 FastAPI 实现 HTTP GET 接口。\n1. 创建 FastAPI 应用实例 from fastapi import FastAPI app = FastAPI() 一行代码即可创建应用实例，不需要额外的目录结构和脚手架。\n2. 定义 Chat 接口 @app.get(\u0026#34;/api/chat\u0026#34;) async def chat(id: str = \u0026#34;\u0026#34;, question: str = \u0026#34;\u0026#34;): return { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;chat demo\u0026#34; } } @app.get(\u0026quot;/api/chat\u0026quot;)：声明这是一个 GET 请求，路径为 /api/chat\n函数参数 id 和 question 会自动映射为 URL 查询参数（Query Parameters）\n直接返回字典，FastAPI 会自动序列化为 JSON 响应\n3. 完整代码 将以上内容组合，完整的 main.py 如下：\nfrom fastapi import FastAPI app = FastAPI() @app.get(\u0026#34;/api/chat\u0026#34;) async def chat(id: str = \u0026#34;\u0026#34;, question: str = \u0026#34;\u0026#34;): return { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;chat demo\u0026#34; } } 运行 uvicorn main:app --reload --port 6872 main:app：main.py 文件中的 app 实例\n--reload：代码修改后自动重启，适合开发阶段使用\n--port 6872：指定端口号为 6872\n看到如下输出就说明启动成功了：\nINFO: Uvicorn running on http://127.0.0.1:6872 (Press CTRL+C to quit) INFO: Started reloader process 调用接口 浏览器直接访问 打开浏览器，访问：\nhttp://127.0.0.1:6872/api/chat?id=1\u0026amp;question=hello 返回结果：\n{ \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;chat demo\u0026#34; } } 使用 curl 命令 curl \u0026#34;http://127.0.0.1:6872/api/chat?id=1\u0026amp;question=hello\u0026#34; 交互式文档 FastAPI 自动生成了 Swagger UI，访问以下地址即可在页面上直接测试接口：\nhttp://127.0.0.1:6872/docs 这是 FastAPI 的一大亮点——无需任何额外配置，赶快试一试吧～\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Python%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8fastapi%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAhttp%E6%8E%A5%E5%8F%A3%EF%BC%88Python%EF%BC%89/","summary":"FastAPI 是什么 FastAPI 是一个现代、高性能的 Python Web 框架，用于构建 API 服务。它基于 Python 类型提示，能够自动生成交互式 API 文档（Swagger UI），开发体验非常友好。 核心特点： -","title":"使用fastapi框架3分钟实现一个http接口（Python）"},{"content":"Spring AI Alibaba —— 小试牛刀 对于没有编程基础的同学，直接看b站视频学习：https://www.bilibili.com/video/BV1eyWbzEEnw\n对于有编程基础的同学，请看官方文档学习：\nhttps://java2ai.com/\nhttps://github.com/spring-ai-alibaba/examples/blob/main/spring-ai-alibaba-helloworld/README.md\nSpring AI Alibaba 是基于 Spring AI 框架对阿里云百炼大模型服务的集成实现。它让 Java 开发者能够以极简的方式接入大模型，就像写一个普通的 Spring Boot 接口一样简单。\n下面我们通过一个最简单的对话接口，带你快速上手。\n源码：[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/) 使用spring ai alibaba实现一个ai对话接口\n1. 创建项目 \u0026amp; 引入依赖 使用 Maven 创建一个 Spring Boot 项目，在 pom.xml 中添加以下关键依赖：\n\u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.5\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;17\u0026lt;/java.version\u0026gt; \u0026lt;spring-ai-alibaba.version\u0026gt;1.0.0.2\u0026lt;/spring-ai-alibaba.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Spring Boot Web --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring AI Alibaba DashScope Starter --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud.ai\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-ai-alibaba-starter-dashscope\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-ai-alibaba.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 2. 配置 API Key 和模型 在 src/main/resources/application.properties 中添加配置：\nserver.port=8080 spring.ai.dashscope.api-key=你的API Key spring.ai.dashscope.chat.options.model=qwen3-max API Key 获取方式：前往 阿里云百炼控制台 创建 API Key，替换上面的占位符即可。\n3. 编写启动类 标准的 Spring Boot 启动类，没有任何特殊配置：\npackage com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } } 4. 定义请求 \u0026amp; 响应模型 ChatRequest —— 接收用户提问：\npackage com.example.model; public class ChatRequest { private String id; private String question; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getQuestion() { return question; } public void setQuestion(String question) { this.question = question; } } ChatResponse —— 封装模型回答：\npackage com.example.model; public class ChatResponse { private String answer; public ChatResponse(String answer) { this.answer = answer; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } } Result —— 统一响应包装：\npackage com.example.model; public class Result\u0026lt;T\u0026gt; { private String message; private T data; public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; ok(T data) { Result\u0026lt;T\u0026gt; r = new Result\u0026lt;\u0026gt;(); r.message = \u0026#34;OK\u0026#34;; r.data = data; return r; } public static Result\u0026lt;?\u0026gt; error(String msg) { Result\u0026lt;?\u0026gt; r = new Result\u0026lt;\u0026gt;(); r.message = msg; return r; } public String getMessage() { return message; } public T getData() { return data; } } 5. 编写对话接口（核心） 这是整个项目最核心的部分，也是 Spring AI Alibaba 真正展现威力的地方——只需 3 行代码就能完成一次大模型对话：\npackage com.example.controller; import com.example.model.ChatResponse; import com.example.model.Result; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(\u0026#34;/api\u0026#34;) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } @GetMapping(\u0026#34;/chat\u0026#34;) public Result\u0026lt;ChatResponse\u0026gt; chat(@RequestParam String question) { String answer = chatClient.prompt(question).call().content(); return Result.ok(new ChatResponse(answer)); } } 关键代码解析 代码 说明 ChatClient.Builder Spring AI 自动注入的构建器，框架根据 application.properties 的配置自动创建对应的大模型客户端 chatClientBuilder.build() 构建 ChatClient 实例，后续所有对话都通过它发起 chatClient.prompt(question) 将用户的问题作为 prompt 发送给大模型 .call() 同步调用大模型，等待返回结果 .content() 提取大模型返回的文本内容 整个调用链路一气呵成：构造 prompt → 调用模型 → 提取回答，一行代码搞定。\n6. 启动 \u0026amp; 测试 启动应用后，打开浏览器或使用 curl 访问：\nmvn spring-boot:run curl \u0026#34;http://localhost:8080/api/chat?question=你好，介绍一下你自己\u0026#34; 返回结果示例：\n{ \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;你好！我是通义千问，一个由阿里云开发的大语言模型...\u0026#34; } } ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Java%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8SpingAi%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95AI%E5%AF%B9%E8%AF%9D%28Java%29/","summary":"Spring AI Alibaba —— 小试牛刀 对于没有编程基础的同学，直接看b站视频学习：https://www.bilibili.com/video/BV1eyWbzEEnw 对于有编程基础的同学，请看官方文档学习： 1.  htt","title":"使用SpingAi框架3分钟实现一个简单AI对话(Java)"},{"content":"对话Agent本质上是一个基于AI技术构建的智能交互系统。你可以把它看作是一个能够像真人一样理解问题、调用知识库并给出精准回答的中台助手。它最重要的使命就是帮助团队挡掉高频的重复咨询，加速问题解决，从而提高整体的工作效率。\n简单来说，对话Agent核心就是AI+知识库的模式。它把团队中散落的文档、经验和历史数据，转化成可随时调用的活知识，让你的经验和知识不再因为人员流动而流失。\n📷 [图片 token=O3wBbKBR9oGRHzxKOLrc60N9nce（未能下载，见飞书原文）]\n📷 [图片 token=MEF0bJdXRoTRPhxTk6WcRZb0nlc（未能下载，见飞书原文）]\n为什么我们需要对话Agent？ 也许你会问，直接让人看文档、翻手册不行吗？为什么要专门做一个Agent？\n在日常的研发和运维工作中，我们常常会遇到一些让人头疼的日常痛点：\n重复性工作消耗人力：中台或研发同学可能将50%的时间花费在回复那些重复的、低价值的问题上（比如API怎么接、流程是什么），这极大地挤占了他们编写代码和解决核心问题的时间，导致晚上加班写代码\u0026hellip;\n**工单堆积，噪音太多：**简单咨询和复杂问题混在一起，研发被琐事淹没，核心任务的进度就会被拖延。\n对话Agent正是针对这些痛点而生的解决方案。它通过自动化前置处理，能有效地：\n解放人力：自动承接80%以上的重复咨询，让团队成员能专注于更有意义、更具挑战性的工作。\n实现秒级响应：问题或工单进来时即刻触发检索，平均响应时间可以压缩到10秒以内，比人工翻文档再回答快得多。\n**沉淀组织经验：**所有的问答和解决方案都会自动存入知识库，让团队经验可复用、不流失。\n过滤噪音：提前拦截并解决简单问题，让真正需要人工介入的复杂工单优先得到处理，从而提升整体协作效率。\n对话Agent的典型应用场景 对话Agent的优势在于它的业务属性非常通用，核心逻辑是问题匹配+知识检索，这让它可以无缝对接多个高频交互场景：\n业务方支持 **传统模式：**业务方-\u0026gt;中台同学（反复提问）-\u0026gt;手动翻文档-\u0026gt;回复。\n**Agent模式：**业务方-\u0026gt;对话Agent（自动回复）-\u0026gt;解决不了再找中台。\n**效果：**自动解答接入文档、接口规范等问题，将中台同学从人工客服中解放出来。\n值班自救 **传统模式：**值班人员收到告警-\u0026gt;凌晨迷糊查手册-\u0026gt;尝试修复-\u0026gt;可能延误。\n**Agent模式：**值班人员将告警复制给Agent-\u0026gt;Agent自动检索《故障处理手册》并推送解决方案-\u0026gt;人工快速确认。\n效果：在不到10秒内返回修复步骤，大大压缩了值班处理告警的时间，降低了响应延误风险。\n工单预处理 **传统模式：**新工单-\u0026gt;研发逐条查看-\u0026gt;简单问题占用时间。\nAgent模式：新工单-\u0026gt;Agent检索历史案例库-\u0026gt;简单问题自动回复/自动总结-\u0026gt;复杂问题再流转给人工。\n**效果：**提前过滤掉简单问题，让团队能将精力聚焦在真正需要人工解决的疑难杂症上。\n这种一次开发，多场景复用的特性，让对话Agent成为了一个非常理想的中台服务项目。\n对话Agent的核心技术与功能亮点 对话Agent的业务逻辑不仅清晰，而且背后的技术深度和亮点也不含糊。\n核心功能：智能问答交互 Agent接收用户提问后，会基于知识库（如文档、手册、历史工单）进行**RAG（检索增强生成）**检索与推理，返回精准答案。\nRAG检索：挑战在于如何让AI精准找到文档中的关键信息，实现从搜得到到搜得准的飞跃。\nPrompt工程：决定了AI的回答是生硬的机器客服还是亲切的真人同事。\n学习迭代：对人工处理的工单或问题，Agent会自动沉淀为新的知识点，让自己越用越聪明。\n交互体验优化 为了让Agent的体验更像一个活的知识助手，我们还需要关注交互细节：\n上下文记忆（多轮对话）：Agent需要记住用户历史的提问（例如先问如何接入，接着问那SDK版本要求呢），并能关联上下文进行回答，避免重复解释。\n实时响应（流式输出）：通过SSE技术，让回答像人类对话一样逐句逐字呈现，避免用户长时间等待，极大地提升了用户体验。\n容错处理：当知识库中没有匹配答案时，Agent不应该生硬地回复不知道，而应引导用户补充问题细节或自动提示转人工服务。\n项目价值与面试杀手锏 做一个对话Agent项目不仅能提升你团队的日常效率，它更是你简历上一个能打的项目亮点。\n提升团队效率，实现降本增效 这是最直接的价值体现：\n响应速度飞跃：业务方咨询的响应速度从等待1小时变成秒回。\n效率成倍提升：研发值班处理告警的时间从5分钟翻手册压缩到1分钟AI给方案。\n**工作更聚焦：**真正到研发手里的都是经过AI过滤后的新问题和复杂问题。\n面试答辩中的突出亮点 相比于空泛地介绍我做了个API接口，对话Agent项目能让你聊出有数据、有技术、有场景的细节：\n聊技术优化与数据：你可以展示RAG检索优化是如何将准确率从65%提升到92%的，让业务方反馈AI终于懂我想问啥了。\n聊体验优化与业务价值：你可以说明多轮对话优化是如何将模糊问题的一次性解决率从50%提升到80%，让用户不用反复解释。\n聊知识沉淀：这是一个团队共同价值，说明你的项目是如何帮助团队经验可复用、不流失的。\n这些具体的改进、数据和场景，正是面试官最想听到的。对话Agent项目将有力地证明你既有技术深度，又有解决实际业务问题的能力。\n总结 对话Agent就像是给你和团队配备了一位永不离职的知识专家：它场景贴合日常、技术逻辑扎实、业务价值看得见摸得着。无论是为了提升日常工作效率，还是为了给自己的简历加上一个出色的项目，它都绝对值得你投入。毕竟，谁不想少回点重复消息，多花时间在真正有创造性的工作上呢？\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E5%AF%B9%E8%AF%9D%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"对话Agent本质上是一个基于AI技术构建的智能交互系统。你可以把它看作是一个能够像真人一样 理解问题、调用知识库并给出精准回答 的中台助手。它最重要的使命就是帮助团队 挡掉高频的重复咨询，加速问题解决，从而提高整体的工作效率 。 简单来说","title":"前置准备：对话的需求、场景、价值分析"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习[前置准备：对话的需求、场景、价值分析](/oncall/智能 OnCall Agent 项目/旧文档备份/前置准备：对话的需求、场景、价值分析/)\n先搞清楚：对话Agent到底是个啥？ 📷 [图片 token=YnqdbbS57oRQuOxvfsgc8JXznwf（未能下载，见飞书原文）]\n想象一下，你们团队里有个\u0026quot;百事通\u0026quot;同事，什么文档都看过，什么问题都能答，而且24小时在线，秒回消息，从不请假。对话Agent，本质上就是这么一个\u0026quot;AI百事通\u0026quot;。\n说得稍微技术一点，它的核心模式其实就是 AI + 知识库：\nAI 负责理解你的问题，搞懂你到底想问啥；\n知识库 负责提供答案的原材料，比如你们团队沉淀下来的文档、手册、历史工单等等。\n把这两者结合起来，就是一个能像真人一样理解问题、自动检索知识、给出精准回答的智能助手。\n你可能会想：\u0026ldquo;这不就是个高级版的搜索框吗？\u0026rdquo;\n还真不一样。普通搜索是你输入关键词，它丢给你一堆链接，你自己慢慢翻。而对话Agent是你用自然语言随便问，它直接把答案告诉你，而且还能跟你多聊几轮，越聊越精准。\n📷 [图片 token=HY5ebgV4tod9mKxecL1ckeddnPe（未能下载，见飞书原文）]\n📷 [图片 token=IlYdbNoBqoR0tUxyhGUcMd0vnUc（未能下载，见飞书原文）]\n为什么需要对话Agent？直接翻文档不行吗？ 这个问题问得好。我猜很多同学的第一反应也是：\u0026ldquo;团队有文档啊，有手册啊，自己翻翻不就行了？\u0026rdquo;\n道理是没错，但现实往往是这样的\n📷 [图片 token=T0EhbRgvOoWvBGx964EcpifdnSh（未能下载，见飞书原文）]\n痛点一：重复问题把人逼疯 你有没有这种体验？作为中台或者基础架构的同学，每天被业务方追着问：\n\u0026ldquo;这个API怎么接入？\u0026rdquo;\n\u0026ldquo;那个接口的参数格式是什么？\u0026rdquo;\n\u0026ldquo;流程是什么？要找谁审批？\u0026rdquo;\n这些问题，每一个单独看都不难，但架不住每天都有人问，而且问的基本是同样的问题。\n结果就是，你可能50%的工作时间都在当\u0026quot;人肉客服\u0026quot;，真正写代码、解决核心问题的时间反而被挤占了。白天回消息，晚上加班写代码——这谁顶得住？\n痛点二：简单问题和复杂问题混在一起 工单系统里，\u0026ldquo;API怎么调用\u0026quot;和\u0026quot;线上数据不一致\u0026quot;这两类问题可能排在一起等你处理。简单的问题占了大头，但你得一个个看，核心的、紧急的任务反而被拖延了。\n这就像你去医院急诊，前面排了一堆感冒发烧的，真正需要急救的反而排不上号。\n痛点三：经验跟着人走 团队里那个\u0026quot;什么都懂\u0026quot;的老员工离职了，带走的不只是人，还有一脑子的经验和解决方案。\n新人来了，又得从头摸索，踩一遍前人踩过的坑。\n对话Agent，就是专门来解决这些痛点的。\n简单总结一下它能带来什么：\n痛点 Agent怎么解决 重复问题耗人力 Agent自动承接80%以上的重复咨询，让你专注有价值的工作 响应慢，得排队等 问题进来秒级响应，平均10秒内给出答案，比人翻文档快太多 简单复杂混在一起 Agent提前拦截简单问题，只把真正需要人处理的流转过来 经验随人流失 所有问答自动沉淀到知识库，团队经验可复用、不流失 对话Agent能用在哪些场景？ 说完了\u0026quot;为什么需要\u0026rdquo;，咱们再来看看\u0026quot;具体怎么用\u0026quot;。\n对话Agent有一个很爽的特点：核心逻辑是通用的。它的底层就是\u0026quot;理解问题 → 检索知识 → 返回答案\u0026quot;，所以它天然能适配很多场景。下面我挑三个最典型的给大家拆解一下：\n场景一：业务方支持 这是最常见的场景了。\n以前是这样的：\n业务方有问题 → 在群里@中台同学 → 中台同学放下手头的活 → 打开文档翻半天 → 组织语言回复 → 业务方追问 → 再翻文档 → 再回复……\n一个简单的问题，可能来回折腾半小时，中台同学的心态也崩了。\n用了Agent之后：\n业务方有问题 → 直接问Agent → Agent秒回答案 → 搞定！\n如果Agent答不了的复杂问题，再自动转给中台同学处理。\n你看，中台同学从\u0026quot;人工客服\u0026quot;变成了只处理\u0026quot;疑难杂症\u0026quot;的专家，工作体验完全不一样了。\n场景二：值班自救 半夜三点，告警来了，你睡眼惺忪地爬起来，对着告警信息一脸懵。\n以前是这样的：\n看到告警 → 打开电脑 → 翻故障处理手册 → 找到对应章节 → 照着操作 → 祈祷别搞错……\n整个过程可能5到10分钟，而且凌晨脑子不清醒，翻手册还容易看错。\n用了Agent之后：\n把告警信息复制给Agent → Agent自动检索《故障处理手册》→ 10秒内推送对应的修复步骤 → 你照着确认执行就行。\n这就好比，以前你得自己翻字典查单词，现在有个翻译官直接告诉你答案。效率完全不在一个量级。\n场景三：工单预处理 研发团队每天可能收到几十上百条工单，里面大量是重复的、简单的问题。\n以前是这样的：\n新工单进来 → 研发一条条看 → 发现大部分是老问题 → 复制粘贴之前的回复 → 真正复杂的问题反而排到后面了。\n用了Agent之后：\n新工单进来 → Agent先检索历史案例库 → 简单问题自动回复 → 复杂问题标记后流转给人工。\n相当于Agent帮你做了一轮\u0026quot;分诊\u0026quot;，你拿到手的全是真正需要动脑子的问题。\n看到没？一套Agent，多个场景复用。这也是为什么说对话Agent是一个非常理想的中台服务项目——投入产出比极高。\n对话Agent背后的核心技术 聊完了场景，你可能好奇：Agent是怎么做到\u0026quot;智能回答\u0026quot;的？它背后到底用了什么技术？\n📷 [图片 token=YTTVbI41YoA41rxztDkcP8Jtnye（未能下载，见飞书原文）]\n核心能力：RAG（检索增强生成） RAG 是 Retrieval-Augmented Generation 的缩写，翻译过来叫\u0026quot;检索增强生成\u0026quot;。\n这名字听着唬人，但原理其实不复杂。咱们拆开来看：\n检索（Retrieval）：用户问了一个问题，Agent先去知识库里\u0026quot;搜\u0026quot;相关的文档片段。\n增强（Augmented）：把搜到的文档片段作为\u0026quot;参考资料\u0026quot;，喂给AI大模型。\n生成（Generation）：AI大模型结合问题和参考资料，生成一个通顺、准确的回答。\n打个比方：你问一个学霸一道题，这个学霸不是纯靠脑子硬想（那容易瞎编），而是先翻了一下课本找到相关知识点，然后结合课本内容给你讲解。RAG就是让AI先\u0026quot;翻课本\u0026quot;再回答，所以答案更靠谱。\n为什么不直接让AI回答，还要先检索？\n因为AI大模型虽然很聪明，但它的知识有\u0026quot;截止日期\u0026quot;，而且不了解你们团队内部的私有知识（比如你们的API文档、业务流程）。通过RAG，我们把团队的知识\u0026quot;喂\u0026quot;给它，它就能回答团队内部的问题了。\n这里面有个关键挑战：怎么搜得准？\n如果用户问\u0026quot;怎么接入支付SDK\u0026quot;，Agent搜出来的是一堆不相关的文档，那回答肯定也是驴唇不对马嘴。所以RAG的检索质量，直接决定了Agent好不好用。\nPrompt工程：让AI\u0026quot;说人话\u0026quot; 你有没有觉得有些AI回答问题像念课文，生硬死板？这就是Prompt（提示词）没写好。\nPrompt工程，简单说就是\u0026quot;怎么跟AI下指令，让它按你想要的方式回答\u0026quot;。\n举个例子，同样是回答\u0026quot;怎么接入支付SDK\u0026quot;：\n没优化Prompt：AI可能回答一大段技术文档的原文，又臭又长，看不懂。\n优化了Prompt：AI会像一个耐心的同事一样，分步骤告诉你：第一步做什么，第二步做什么，遇到问题怎么排查。\n好的Prompt能让Agent从\u0026quot;冷冰冰的机器\u0026quot;变成\u0026quot;靠谱的同事\u0026quot;，这个差距是巨大的。\n多轮对话：记住上下文 我们跟人聊天的时候，不会每句话都把前因后果重复一遍，对吧？比如：\n你：\u0026ldquo;怎么接入支付SDK？\u0026rdquo; Agent：\u0026ldquo;你可以参考这个文档，步骤是……\u0026rdquo; 你：\u0026ldquo;那SDK版本有要求吗？\u0026rdquo;\n注意第二个问题，你说的是\u0026quot;那SDK版本\u0026quot;——如果Agent没有上下文记忆，它根本不知道你问的是哪个SDK。\n所以，多轮对话能力就是让Agent记住你之前聊了什么，能够关联上下文来理解你的新问题。就像你跟同事聊天，不用每次都从头解释一遍背景。\n流式输出：别让用户干等 想象一下，你问了Agent一个问题，然后盯着屏幕等了30秒，突然\u0026quot;唰\u0026quot;一下蹦出一大段文字。这体验很糟糕，因为你不知道它到底在不在处理，会忍不住想\u0026quot;是不是卡了？\u0026quot;\n流式输出（通过SSE技术实现）就是让回答像打字一样，一个字一个字地往外蹦。这样用户能实时看到Agent在\u0026quot;思考\u0026quot;和\u0026quot;回答\u0026quot;，体验感直接拉满。\nSSE 全称是 Server-Sent Events（服务器推送事件），你可以简单理解为：服务器不是等全部算完再一次性返回，而是算一点就推一点给前端，前端实时展示。就像你看直播，画面是实时传过来的，而不是等录完了再放。\n容错处理：不知道就别瞎说 最后一个重要的点：当Agent在知识库里找不到答案时怎么办？\n最差的做法是瞎编一个答案（AI确实有这个\u0026quot;毛病\u0026quot;，行话叫幻觉——Hallucination）。\n正确的做法是：\n诚实告知用户\u0026quot;这个问题我暂时回答不了\u0026quot;；\n引导用户补充更多细节，看看换个问法能不能找到答案；\n如果确实超出能力范围，自动引导转人工服务。\n这就像一个靠谱的同事，不懂就说不懂，而不是瞎忽悠你。\n最后总结一下 咱们今天聊了对话Agent的需求背景、应用场景、核心技术和项目价值，快速回顾一下：\n是什么：对话Agent = AI + 知识库，一个能自动理解问题、检索知识、给出答案的智能助手。\n为什么需要：解决重复咨询耗人力、响应慢、经验易流失等痛点。\n用在哪：业务方支持、值班自救、工单预处理——一次开发，多场景复用。\n怎么做到的：RAG检索增强生成、Prompt工程、多轮对话、流式输出、容错处理。\n简单说，对话Agent就像给你的团队配了一个永不离职、秒级响应的知识专家。它场景贴合日常、技术逻辑清晰、业务价值肉眼可见。\n好了，需求和场景分析到这里就讲完了。下一篇，我们就正式进入技术方案设计环节，聊聊对话Agent到底该怎么一步步搭起来。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E5%AF%B9%E8%AF%9D%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习\u0026lt;mention-doc token=\u0026ldquo;LJpoww6n1i52nakHm3KcjqcAnIe\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 前置准备：对话的需求、场景、价值分析\u0026lt;/men","title":"前置准备：对话的需求、场景、价值分析"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习 [前置准备：知识库的需求、场景、价值分析](/oncall/智能 OnCall Agent 项目/旧文档备份/前置准备：知识库的需求、场景、价值分析/)\n今天咱们来聊一个在AI应用中特别重要、但很多同学一开始容易忽略的东西——知识库。\n先别急，咱们从一个你一定经历过的场景说起。\n为什么我们需要知识库？ 📷 [图片 token=Umakbr9i7oCA9mxTqhUctKQdnDh（未能下载，见飞书原文）]\n你有没有过这样的经历：项目上线前夜，突然报了一个告警，你印象中之前有人写过一份处理手册，但死活找不到在哪。翻了半小时的飞书文档、翻了聊天记录、翻了内部Wiki，最后发现那份文档藏在某个三级目录下的一个不起眼的角落里……\n再比如，新同事入职了，问你\u0026quot;咱们这个API的鉴权流程是什么？\u0026quot;，你说\u0026quot;文档里有\u0026quot;，但他可能花了一下午，都没找到那份写得最全的文档。\n这就是传统文档管理的痛点——信息孤岛。\n什么意思呢？就是说，你的知识明明存在，但它们散落在各个角落，像一座座孤岛，彼此之间没有桥梁相连。你知道答案就在某个地方，但就是够不着。\n所以，知识库的出现，本质上就是要解决一个问题：把\u0026quot;大海捞针\u0026quot;变成\u0026quot;秒级找到\u0026quot;。\n知识库的核心目标是什么？ 说白了，知识库就像你团队里最靠谱的智能知识管家。\n它的核心目标，用一句话概括就是：把团队积累的海量文档，从\u0026quot;沉睡的文件\u0026quot;变成\u0026quot;AI随时能调用的活资产\u0026quot;。\n这句话里有两个关键词，咱们拆开来说：\n1. \u0026ldquo;沉睡的文件\u0026rdquo; 你想想，团队这些年写了多少文档？PDF、Markdown、技术手册、告警处理记录、历史工单……这些东西存在硬盘里、存在云文档里，如果没人主动去翻，它们就永远躺在那里，发挥不了任何价值。这就是\u0026quot;沉睡\u0026quot;。\n2. \u0026ldquo;AI随时能调用的活资产\u0026rdquo; 知识库做的事情，就是把这些沉睡的文档\u0026quot;唤醒\u0026quot;。怎么唤醒？通过一套自动化的流程，把文档内容转化成AI能理解的格式，存到一个专门的数据库里。这样，当任何AI应用需要回答问题的时候，就能像查字典一样，秒级找到最相关的内容。\n所以你看，知识库不是一个简单的\u0026quot;文档管理工具\u0026quot;，它更像是一座桥梁——连接着你的静态文档和动态的AI应用。\n📷 [图片 token=I8M5bBqNYoi87DxqBmtcT4Wun6h（未能下载，见飞书原文）]\n📷 [图片 token=ZOcpbBGHJoGi3Lx0EWdcHT0xnOb（未能下载，见飞书原文）]\n知识库的核心流程：从文档到AI能用，到底经历了什么？ 好，目标讲完了，接下来咱们看看知识库到底是怎么干活的。别担心，流程并不复杂，总共就三步，我会一步步给你拆解。\n📷 [图片 token=MrrvbIKrSoRfvGxY2OjccmTmnte（未能下载，见飞书原文）]\n第一步：文档拆分（Chunking） 你上传了一份100页的PDF技术手册，AI能直接读懂吗？答案是：不能，至少不能一口吞下去。\n这就像你准备考试，你不会把一整本教材从头到尾读一遍就完事了吧？你会按章节、按知识点去拆分，做成一张张笔记卡片，对不对？\n知识库做的也是同样的事情。它会自动把你上传的文档，按照章节、段落、甚至自定义的规则进行拆分。而且，它还会智能适配不同的文档格式——PDF有PDF的拆法，Markdown有Markdown的拆法。\n这一步的关键在于：每个拆出来的片段，都要包含完整的语义信息。\n什么意思？举个例子，假设文档里有一段话：\u0026ldquo;当API返回401错误码时，通常是因为Token过期。解决方案是重新调用/auth/refresh接口获取新Token。\u0026rdquo;\n如果拆分的时候，把\u0026quot;解决方案是重新调用/auth/refresh接口\u0026quot;和前面的\u0026quot;API返回401错误码\u0026quot;拆到了两个不同的片段里，那后续AI检索的时候，就可能只找到问题描述，却找不到解决方案。这就是\u0026quot;语义信息不完整\u0026quot;带来的问题。\n所以，好的拆分，是整个知识库质量的基石。\n第二步：文本向量化（Embedding） 拆分完成后，每个片段还是人类能读懂的文字。但问题是，AI不是用\u0026quot;读文字\u0026quot;的方式来理解内容的，它需要的是数学语言。\n这一步，就是调用一个叫Embedding模型的东西，把每个文本片段转化为一组高维向量。\n等等，\u0026ldquo;高维向量\u0026quot;是什么？别慌，咱们用一个通俗的类比来理解。\n你可以把每个文本片段想象成一个人。每个人都有很多特征：身高、体重、年龄、爱好等等。如果我们用一组数字来描述一个人，比如[175, 70, 25, \u0026hellip;]，那这组数字就是这个人的\u0026quot;向量表示\u0026rdquo;。\n同样的道理，Embedding模型会根据文本的语义，给每个片段生成一组数字（通常是几百到上千维的）。语义越相近的文本，它们的向量在数学空间中就越\u0026quot;靠近\u0026quot;。\n举个例子：\n\u0026ldquo;Token过期了怎么办\u0026rdquo; 和 \u0026ldquo;鉴权失败的处理方法\u0026rdquo;，虽然字面上长得不一样，但语义是相近的，所以它们的向量会很接近。\n\u0026ldquo;Token过期了怎么办\u0026rdquo; 和 \u0026ldquo;今天中午吃什么\u0026rdquo;，语义完全不相关，向量就会离得很远。\n这就是向量化的魔力——它让AI能理解\u0026quot;意思\u0026quot;，而不仅仅是匹配\u0026quot;关键词\u0026quot;。\n这也是为什么知识库比传统的关键词搜索强大得多的原因。传统搜索你搜\u0026quot;鉴权失败\u0026quot;，可能搜不到标题叫\u0026quot;Token过期处理\u0026quot;的文档。但基于向量的检索，就能把它们关联起来。\n第三步：向量库存储 向量生成好了，总得有个地方存起来吧？这就是向量数据库的职责。\n知识库会把生成的向量，连同它的元信息（比如：这段内容来自哪份文档、属于第几章、作者是谁、最后更新时间是什么），一起批量存入向量数据库中。\n为什么要存元信息？两个原因：\n方便溯源：当AI给你一个回答的时候，你可以看到\u0026quot;这个答案来自《故障处理手册》第3章\u0026quot;，这样你就能判断答案靠不靠谱，而不是盲目信任AI。\n支持过滤：比如你只想搜索最近一个月更新的文档，或者只搜索某个特定作者写的内容，有了元信息就能轻松做到。\n到这里，整个流程就跑通了：文档上传 → 自动拆分 → 向量化 → 存入向量库\n整个过程是全自动的，你只需要把文档丢进去，剩下的交给Agent。\n知识库的三大核心价值 流程搞明白了，咱们再来聊聊，知识库到底能给我们带来什么实打实的价值。\n📷 [图片 token=NI27bzK9CoqB2Ux1drncD8zon4d（未能下载，见飞书原文）]\n价值一：支撑RAG精准检索，让AI的回答\u0026quot;言之有物\u0026quot; 先解释一下RAG（Retrieval-Augmented Generation，检索增强生成）。这个词听起来很唬人，但其实不难理解：\n普通的大模型回答问题，是靠它训练时\u0026quot;记住\u0026quot;的知识。但问题是，它可能记错、记混，甚至\u0026quot;编造\u0026quot;一个看起来像模像样但完全错误的答案——这就是大模型的幻觉问题。\nRAG的思路就是：在AI回答之前，先去知识库里搜一搜，找到最相关的内容，然后把这些内容\u0026quot;喂\u0026quot;给大模型，让它基于真实的文档来生成回答。\n打个比方，就像你考试的时候带了一本参考书。你不是凭空编答案，而是翻书找到相关内容，再用自己的话组织出答案。这样写出来的东西，肯定比瞎编靠谱多了，对吧？\n而知识库，就是RAG的数据底座。没有高质量的知识库，RAG就像考试带了一本全是乱码的参考书——根本没用。\n具体来说，当你或者其他AI应用提出一个问题时：\n系统会把你的问题也转化为一个向量。\n然后拿这个向量去向量数据库里做相似性匹配，找出最接近的几个文档片段。\n把这些片段作为\u0026quot;参考资料\u0026quot;交给大模型，大模型基于这些资料生成最终的回答。\n这样一来，AI的回答就有了\u0026quot;出处\u0026quot;，有了\u0026quot;证据\u0026quot;，而不是瞎猜的。\n价值二：知识沉淀与跨场景复用——一次入库，多端受益 这个价值，对团队来说是最有长远意义的。\n我们经常说\u0026quot;铁打的团队，流水的兵\u0026quot;。老员工离职了，他脑子里的经验怎么办？新人入职了，谁来手把手教他那些\u0026quot;只可意会不可言传\u0026quot;的坑？\n知识库解决的就是这个问题。它把散落在各处的经验、文档、处理记录，都统一转化为向量资产，沉淀在知识库里。\n老员工离职？ 没关系，他写过的文档、处理过的工单，都已经被知识库消化吸收了。新人直接问AI就行。\n新人上手慢？ 没关系，有了知识库加持的AI，新人遇到问题直接提问，就能获得基于团队历史经验的精准回答。\n更关键的是，知识库是通用的。你上传一次文档，多个AI应用都能调用。\n比如，你上传了一份《故障处理手册》：\n对话Agent可以用它来回答值班同事的告警咨询。\n运维Agent可以用它来自动排查线上问题。\n一份文档，多个场景，全都受益。这就是\u0026quot;一次入库，多端受益\u0026quot;的威力。\n📷 [图片 token=UxmIbW92uoTzpUxkx3HcaLB0nBg（未能下载，见飞书原文）]\n📷 [图片 token=M3dsby15sonqtpxbwdrcVTTNnCe（未能下载，见飞书原文）]\n价值三：从被动查找到主动赋能，解放研发精力 还记得我们开头说的那个场景吗？找一份文档翻半小时。\n有了知识库之后，这个问题就彻底消失了。\n你上传文档，Agent自动处理一切。处理完成后，你只需要对着AI说一句\u0026quot;XX告警应该怎么处理？\u0026quot;，几秒钟就能得到答案，还附带文档出处。\n这意味着什么？意味着你的研发团队不用再把宝贵的时间浪费在\u0026quot;找东西\u0026quot;上，可以把精力放在真正有创造性的工作上。\n总结 最后咱们来做个简单的回顾。\n📷 [图片 token=NcBLbTcZBoCM6AxFUWkcd92PnCe（未能下载，见飞书原文）]\n知识库的本质，是把团队的文档资产从\u0026quot;沉睡\u0026quot;状态变成\u0026quot;活的\u0026quot;。它通过三步核心流程——文档拆分、向量化、入库存储，实现了全自动化的知识处理。\n它带来的三大核心价值：\n价值 一句话总结 支撑RAG精准检索 让AI回答有据可依，不再瞎编 知识沉淀与复用 一次入库，多个AI应用共享调用 解放研发精力 从翻半小时文档，到秒级获取答案 记住，知识库不是一个简单的文档存储工具，它是连接静态文档和动态AI应用的桥梁，是整个AI应用体系的基础设施。\n如果你正在搭建AI应用，知识库一定是你要最先考虑的事情之一。因为没有好的知识底座，再强的大模型也是巧妇难为无米之炊。\n好了，今天的分享就到这里。如果对你有帮助，别忘了点个赞，咱们下篇见！\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E7%9F%A5%E8%AF%86%E5%BA%93%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习 \u0026lt;mention-doc token=\u0026ldquo;TEeawNReJiel3zkg4hIchy2Jntb\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 前置准备：知识库的需求、场景、价值分析\u0026lt;/m","title":"前置准备：知识库的需求、场景、价值分析"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习[前置准备：运维的需求、场景、价值分析](/oncall/智能 OnCall Agent 项目/旧文档备份/前置准备：运维的需求、场景、价值分析/)\n运维 Agent 到底是个啥？ 如果你是刚入行的同学，可能会觉得\u0026quot;运维\u0026quot;这个词离自己有点远，但实际上，只要你写的代码上了线、跑在服务器上，你就已经和运维打上交道了。\n别急，咱们一步一步来，从最基础的场景讲起。\n先搞清楚一个问题：工程师每天在忙啥？ 📷 [图片 token=WQcAbzAAfoOBqIxzpl5cD72Knad（未能下载，见飞书原文）]\n在聊 Agent 之前，咱们得先理解工程师日常的工作是什么样的，不然你就没法理解为什么需要一个\u0026quot;Agent\u0026quot;来帮忙。\n想象一下这样的场景：\n凌晨三点，你的手机突然疯狂震动，告警群里刷了几十条消息——某个接口的失败率飙升到了 30%，用户开始投诉下单失败。\n你睡眼惺忪地爬起来，打开电脑，先看告警消息里说了什么，再切到日志平台搜关键字，接着打开监控面板看各种指标曲线，然后怀疑是下游服务的问题，又得去问下游团队的同学……\n折腾一个多小时，终于搞清楚是下游某个服务在做发布，导致短暂不可用。你在群里回复了一句\u0026quot;已恢复，继续观察\u0026quot;，然后带着黑眼圈继续睡觉。\n听起来是不是挺惨的？但这就是很多团队真实的日常。\n在现在流行的微服务架构下（简单理解就是：一个大系统被拆成了很多小服务，彼此通过网络调用协作），每个团队可能同时维护着十几甚至几十个服务。告警类型也五花八门：接口超时、数据库连接异常、中间件故障、下游依赖挂了……\n而处理这些告警的流程，说白了就是一套重复的体力活：\n看告警消息，提取关键信息（哪个服务？什么错误？什么时间？）\n去日志平台搜日志\n去监控平台看指标曲线\n根据经验判断根因\n决定处理方案（观察？重启？联系下游？）\n这套流程，不管是白天还是半夜，不管是老手还是新人，每次都得走一遍。\n问题来了：这种重复性的工作，能不能让机器来干？\n答案就是——运维 Agent。\n📷 [图片 token=RZAIbHIgQo2lpwxfrvNckejNn6e（未能下载，见飞书原文）]\n运维 Agent 是什么？用一句话解释 你可以把运维 Agent 理解成一个 7×24 小时在线的、不会累的\u0026quot;虚拟值班工程师\u0026quot;。\n它能自动接收告警，然后像一个有经验的老工程师一样，按照预设的排查步骤去查日志、看监控、分析原因，最后给出一份排查报告和处理建议。\n注意这里有个关键词叫 Agent，这个词在 AI 领域特指\u0026quot;能自主决策和执行任务的智能体\u0026quot;。它和普通的脚本、定时任务不一样——脚本是你写死了\u0026quot;第一步做什么、第二步做什么\u0026quot;，而 Agent 能根据中间的结果动态调整下一步的动作，更像一个\u0026quot;会思考的助手\u0026quot;。\n举个例子：\n普通脚本：告警来了 → 固定查某个日志 → 固定发一条消息。不管查到什么，流程都一样。\n运维 Agent：告警来了 → 先查日志 → 发现是超时错误 → 那就再去查下游服务的状态 → 发现下游正常 → 那就看看是不是自己服务的连接池满了 → 一步步推理下去，最终定位到根因。\n看到区别了吗？Agent 的核心在于它能根据上一步的结果，决定下一步该干什么，这就比写死的脚本灵活太多了。\n为什么我们需要运维 Agent？三大痛点告诉你答案 有同学可能会说：\u0026ldquo;虽然人工处理麻烦了点，但最终也能把问题解决啊，为啥非得搞个 Agent？\u0026rdquo;\n这个问题问得好，我们来看看人工处理到底有哪些硬伤。\n📷 [图片 token=BxCgbfFSKoo59PxfjgTcHBHRnPe（未能下载，见飞书原文）]\n痛点一：经验全装在人脑子里，新人根本接不住 运维排查非常依赖经验。一个干了三年的老工程师，看到某个错误码可能秒懂：\u0026ldquo;哦，这是下游 XX 服务超时的经典表现，不慌，等十分钟就好了。\u0026rdquo;\n但如果换成一个刚入职的新同学呢？告警群里消息刷屏，他可能连应该先看哪个系统都不知道，更别提快速判断根因了。\n更麻烦的是，很多排查经验是靠\u0026quot;口口相传\u0026quot;的——老工程师随口提一句\u0026quot;这种情况你去看看 XX 指标\u0026quot;，但这些知识从来没有被系统地记录下来。一旦关键人员离职或转岗，这些宝贵的经验就跟着一起走了，也就是我们常说的知识断层。\n运维 Agent 怎么解决这个问题？\nAgent 的排查逻辑是被明确写成规则和流程的。比如\u0026quot;遇到错误码 A，先查下游服务状态；遇到错误码 B，先看连接池指标\u0026quot;。这些规则本质上就是把资深工程师脑子里的经验外化成了可执行的知识库。\n新人不需要记住所有的排查技巧，Agent 会按照最优路径自动执行。而且这个知识库是可以持续迭代和维护的，团队里任何人都可以贡献新的规则。\n痛点二：80% 的告警都是\u0026quot;老面孔\u0026quot;，但每次还得重新查一遍 这是最让人抓狂的一点。\n你有没有这种感觉：这周处理的告警，上周也遇到过，上上周也遇到过，每次都是一样的原因、一样的处理方式，但你每次都得走一遍完整的排查流程。\n实际工作中，大约 80% 的告警都是重复出现的常见问题——超时、限流、偶发抖动等等。这些问题的排查步骤几乎是固定的，工程师每次都在做\u0026quot;查日志 → 看监控 → 判断原因 → 得出同样的结论\u0026quot;这种机械性劳动。\n运维 Agent 怎么解决这个问题？\n这恰恰是 Agent 最擅长的事情。对于这些已知的、有固定排查路径的问题，Agent 可以在秒级完成处理，根本不需要人来介入。只有当 Agent 遇到它无法识别的新问题时，才需要升级给人工处理。\n这样一来，工程师就可以把精力放在真正需要人脑去思考的复杂问题上，而不是被淹没在重复劳动里。\n痛点三：各个系统是割裂的，排查得来回切换 这一点可能初学者不太好理解，我多解释下。\n在一个正规的技术团队里，通常会有好几套平台各司其职：\n告警平台：负责告诉你\u0026quot;出事了\u0026quot;\n日志平台：记录了服务运行时的详细日志（你可以理解为服务的\u0026quot;日记\u0026quot;）\n监控平台：用各种图表展示服务的健康指标（比如 CPU 使用率、接口响应时间、错误率等）\n这些平台之间通常是互相独立的。也就是说，你在告警平台看到了\u0026quot;接口 A 失败率飙升\u0026quot;，但要查原因，你得自己手动切到日志平台，输入接口名和时间范围去搜索；搜完日志，你又得切到监控平台去看对应的曲线图。\n这个来回切换的过程特别浪费时间，尤其是在半夜脑子不太清醒的时候，很容易遗漏信息或者搞混时间范围。\n运维 Agent 怎么解决这个问题？\nAgent 可以通过调用各个平台的 API（你可以理解为\u0026quot;程序之间互相通信的接口\u0026quot;）来实现跨系统联动。\n具体来说，当一条告警进来，Agent 可以：\n从告警消息中自动提取关键信息（服务名、接口名、时间范围）\n拿着这些信息去调日志平台的 API，拉回相关日志\n同时调监控平台的 API，拉回对应时间段的指标数据\n把所有信息汇总在一起，生成一份结构化的排查报告\n整个过程一气呵成，不需要人来回切换系统，效率提升不是一点半点。\n运维 Agent 的目标是什么？ 聊完了痛点，我们来明确一下运维 Agent 的目标。一句话总结就是：\n让机器处理重复的事，让人去做有价值的事。\n📷 [图片 token=UmGHbko4GomaZdx4jh0cSF8unKd（未能下载，见飞书原文）]\n具体展开来说，它有这么几个目标：\n1. 降低告警响应时间\n人工响应告警，从看到消息到开始排查，可能需要几分钟甚至十几分钟（尤其是在非工作时间）。而 Agent 可以做到秒级响应，告警一来，立刻启动排查流程。\n2. 减少人工介入频率\n前面说了，80% 的告警都是常见问题。Agent 的目标就是把这 80% 的告警自动消化掉，只把剩下 20% 真正需要人脑判断的复杂问题抛给工程师。\n3. 标准化排查流程\n不同的人处理同一个告警，可能会走不同的路径，甚至可能会遗漏关键步骤。Agent 的目标是把排查流程标准化，确保每个告警都按照最优路径被处理，不会因为人的状态波动（比如半夜犯困）而影响质量。\n4. 沉淀和传承团队经验\n这一点特别重要。Agent 本身就是团队运维经验的载体。每一条排查规则、每一个错误码的处理方案，都是经验的沉淀。新成员加入团队后，不需要花大量时间去\u0026quot;拜师学艺\u0026quot;，只需要了解 Agent 的规则库就能快速上手。\n运维 Agent 有哪些核心能力？ 说了这么多\u0026quot;为什么\u0026quot;，现在来说说\u0026quot;是什么\u0026quot;——运维 Agent 到底能干些啥？\n能力一：实时告警响应与自动排查 这是最核心的能力，也是 Agent 存在的根本意义。\n当一条告警进来，Agent 的处理流程大致是这样的：\n告警触发 ↓ 提取关键信息（服务名、接口名、错误类型、时间范围） ↓ 联动查询（调用日志 API + 监控 API，拉取相关数据） ↓ 智能匹配根因（把查到的错误特征和知识库进行匹配） ↓ 输出排查报告 + 处理建议 举个具体的例子来感受一下：\n场景：告警显示\u0026quot;订单服务接口失败率突增至 25%\u0026quot;\nAgent 的操作：\n自动调用日志 API，查询最近 1 小时包含错误关键词的日志\n发现 90% 的错误日志都是 context canceled（上下文取消，通常意味着请求超时被主动中断了）\n调用监控 API，拉取下游支付服务的响应时间曲线\n发现下游支付服务的 P99 响应时间（即 99% 的请求都在这个时间内完成）从 200ms 飙升到了 3s\n匹配知识库，找到规则：\u0026ldquo;下游响应时间异常导致 context canceled → 联系下游团队确认是否在发布\u0026rdquo;\nAgent 输出： \u0026ldquo;订单服务失败率上升，根因为下游支付服务响应时间异常（P99 从 200ms 升至 3s），导致大量请求超时取消。建议联系支付团队确认当前状态，持续观察 10 分钟。\u0026rdquo;\n看到没，整个过程可能只需要十几秒，而如果是人来做，至少需要十几分钟。\n能力二：智能匹配根因 工程师内部会维护一个错误码/错误特征的知识库。你可以把它想象成一个\u0026quot;病症对照表\u0026quot;：\n错误特征 可能的根因 建议操作 context canceled 占比高 下游服务响应慢，导致请求超时 查看下游服务状态，联系对应团队 connection refused 目标服务实例挂了或端口不通 检查目标服务是否正常运行 数据库连接池耗尽 慢查询过多或连接泄漏 查看慢查询日志，检查连接释放逻辑 当 Agent 查完日志，拿到错误特征后，就会去这个知识库里找匹配项，然后给出对应的处理建议。\n这比人工排查更靠谱的地方在于：它不会遗漏。人在紧张或疲惫的时候可能会跳过某些检查步骤，但 Agent 每次都会严格按照流程走完。\n能力三：经验沉淀的自动化闭环 这是 Agent 最有想象力的一个能力——它会越用越聪明。\n怎么理解呢？我们可以把它看成一个\u0026quot;学习闭环\u0026quot;：\n处理告警 → 总结本次排查过程 → 更新知识库 → 下次遇到类似问题，直接复用 初期，Agent 的知识库可能比较薄弱，需要人工编写排查规则。但随着它处理的告警越来越多，知识库会越来越丰富。以前需要人工判断的问题，慢慢地 Agent 也能自己搞定了。\n这就好比一个实习生刚入职时什么都要问导师，但随着经验积累，他能独立处理的事情越来越多。运维 Agent 也是同样的道理，用得越久，它就越\u0026quot;靠谱\u0026quot;。\n总结一下 最后，我们来做个简单的总结：\n运维 Agent 是什么？ → 一个能自动接收告警、排查问题、分析根因的智能助手。\n它的核心价值是什么？ → 用机器的速度和准确性，解决运维中 80% 的重复劳动，让工程师聚焦在更有价值的事情上。\n它的目标是什么？ → 秒级响应告警、减少人工介入、标准化排查流程、沉淀团队经验。\n它有哪些核心能力？ → 实时告警响应、跨系统联动查询、智能匹配根因、周期性问题汇总、经验自动沉淀。\n有一点需要特别强调：运维 Agent 不是来替代工程师的，而是来当工程师的助手的。它负责处理那些重复的、机械的工作，而真正复杂的系统设计、架构优化、疑难问题攻关，依然需要人的经验和创造力。\n在微服务规模不断扩大、告警量越来越多的今天，拥有一个靠谱的运维 Agent，对于保障服务稳定性来说，已经不是\u0026quot;锦上添花\u0026quot;，而是\u0026quot;刚需\u0026quot;了。\n好了，关于运维 Agent 的作用、价值和核心能力，今天就聊到这里。后面我们会进一步聊聊怎么一步步把这样一个 Agent 给搭建起来，敬请期待~\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E8%BF%90%E7%BB%B4%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习\u0026lt;mention-doc token=\u0026ldquo;YwjcwFAvEijDAHkkBQ1cKNRRnrg\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 前置准备：运维的需求、场景、价值分析\u0026lt;/men","title":"前置准备：运维的需求、场景、价值分析"},{"content":"对话接口 与大模型对话，相同Id的对话带有上下文记忆功能\n请求方法: POST /api/chat\n请求字段:\n字段名 类型 描述 Id string 对话的唯一标识 Question string 用户提问 响应字段:\n字段名 类型 描述 Answer string 系统回答 示例：\n# 示例：快速对话 curl -X POST http://localhost:6872/api/chat \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;Id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;Question\u0026#34;: \u0026#34;什么是人工智能？\u0026#34; }\u0026#39; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;AI 的回答内容...\u0026#34; } } 流式对话接口 与大模型对话，相同Id的对话带有上下文记忆功能，通过SSE实现流式输出回答\n请求方法: POST /api/chat_stream\n请求字段:\n字段名 类型 描述 Id string 对话的唯一标识 Question string 用户提问 响应字段:\n字段名 类型 描述 SSE响应格式：\nevent类型 含义 connected 代表连接建立成功 message 回复的文本片段，会多次发送 error 连接异常，断开连接 done 消息推送完毕，断开连接 示例：\n# 示例：流式对话 curl -X POST http://localhost:6872/api/chat_stream \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;Id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;Question\u0026#34;: \u0026#34;什么是人工智能？\u0026#34; }\u0026#39; # 响应 id: \u0026lt;timestamp\u0026gt; event: connected data: {\u0026#34;status\u0026#34;: \u0026#34;connected\u0026#34;, \u0026#34;client_id\u0026#34;: \u0026#34;session-001\u0026#34;} id: \u0026lt;timestamp\u0026gt; event: message data: 人工智能（AI） id: \u0026lt;timestamp\u0026gt; event: message data: 的发展历史 id: \u0026lt;timestamp\u0026gt; event: message data: 可以追溯到... id: \u0026lt;timestamp\u0026gt; event: done data: Stream completed AI运维接口 AI运维接口，调用后会自动查询现在活跃的告警，并判断根因\n请求方法: POST /api/ai_ops\n请求字段:\n字段名 类型 描述 响应字段:\n字段名 类型 描述 Result string 结果 Detail []string 详细信息列表 示例：\ncurl -X POST http://localhost:6872/api/ai_ops \\ -H \u0026#34;Content-Type: application/json\u0026#34; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;result\u0026#34;: \u0026#34;汇总的分析结果...\u0026#34;, \u0026#34;detail\u0026#34;: [ \u0026#34;执行步骤1...\u0026#34;, \u0026#34;执行步骤2...\u0026#34;, \u0026#34;...\u0026#34; ] } } 文件上传接口 该接口用于上传文档到知识库中，便于后续召回使用\n请求方法：POST /api/upload（multipart/form-data）\nmultipart/form-data ：是 HTTP 请求的一种内容类型（Content-Type），用于在表单中上传文件或二进制数据。\n请求字段：\n字段名 类型 描述 响应字段：\n字段名 类型 描述 fileName string 保存的文件名 filePath string 文件保存路径 fileSize int64 文件大小（字节） 示例：\n# 用curl上传一个 Markdown 文件 # -F 参数会自动设置 multipart/form-data 格式 # @ 符号后面跟文件的绝对路径或相对路径 curl -X POST http://localhost:6872/api/upload \\ -F \u0026#34;file=@README.md\u0026#34; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;fileName\u0026#34;: \u0026#34;README.md\u0026#34;, \u0026#34;filePath\u0026#34;: \u0026#34;/path/to/saved/file/example.txt\u0026#34;, \u0026#34;fileSize\u0026#34;: 1024 } } ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%83%E7%AB%A0%EF%BD%9C%E5%89%8D%E5%90%8E%E7%AB%AF%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0/%E5%90%8E%E7%AB%AF%E6%8E%A5%E5%8F%A3%EF%BC%9A%E4%B8%8E%E5%89%8D%E7%AB%AF%E4%BA%A4%E4%BA%92%E7%9A%84%20API%20%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1/","summary":"对话接口 与大模型对话，相同Id的对话带有上下文记忆功能 请求方法 :  POST /api/chat 请求字段:  字段名   类型   描述   \u0026mdash;   \u0026mdash;   \u0026mdash;   Id   string   对话的唯一标识   Que","title":"后端接口：与前端交互的 API 接口设计"},{"content":"各位林友，你们好！\n很多同学在初次接触 AI Agent 时，总会陷入「懂点零散概念，却串不起完整项目」「背了面试题，却讲不清项目设计逻辑」「想动手实战，却不知道从哪个模块切入」的困境，尤其想靠 AI Agent 项目冲击面试时，更是找不到系统的学习路径。\n别担心！我们完全围绕「面试优先、先懂方案再落地、循序渐进无门槛」的核心逻辑，为你设计了这条全链路学习路径，帮你从零开始，系统吃透智能 OnCall Agent 的全流程设计与实现，最终能在技术面试中自信输出、脱颖而出。\n整个学习路线被分为四大核心阶段，每个阶段都完全匹配专栏目录的章节设计，有明确的学习目标和侧重点，贴合从新手到能面试、能落地的成长节奏：\n认知筑基阶段：扫清所有概念障碍，建立项目全局视野，搞懂「这个项目是什么、要解决什么问题」\n核心模块拆解阶段：分模块深入剖析 Agent 全链路设计，从方案架构到源码逻辑层层递进，吃透面试最核心的项目设计能力\n落地实战演练阶段：把理论转化为实操，完成从环境搭建到项目完整运行的全流程，补全项目落地的闭环细节\n面试求职冲刺阶段：把项目经验转化为求职核心竞争力，搞定简历撰写、项目表达、高频考点全流程\n接下来，我们将逐一拆解每个阶段的学习内容与对应章节，帮你高效掌握智能 OnCall Agent 项目的全部核心内容。\n第一阶段：认知筑基 ——AI Agent 入门与项目全局认知 本阶段对应专栏第一章、第二章，核心目标是为你打下最坚实的认知基础，彻底扫清 AI 领域的概念盲区，建立对整个项目的宏观理解。\n[第一章｜AI 名词大扫盲](/oncall/智能 OnCall Agent 项目/第一章｜AI 名词大扫盲/)\n[第二章 | 项目全局认知与技术选型](/oncall/智能 OnCall Agent 项目/第二章 _ 项目全局认知与技术选型/)\n你将先搞懂 AI Agent、RAG、向量数据库、Tool 等所有高频核心名词，不用再被陌生术语卡住学习节奏；再深入理解这个项目的诞生背景、核心价值、整体架构与技术选型逻辑，搞懂「我们要做一个什么样的项目、这个项目的核心竞争力是什么」。\n这个阶段你不需要钻研任何代码细节，只需要吃透核心概念与项目全貌，就能为后续的深入学习做好 100% 的准备，也是面试中开场介绍项目的核心基础。\n第二阶段：核心模块拆解 ——Agent 全链路方案设计与源码分析 本阶段对应专栏第三章、第四章、第五章、第六章、第七章，是整个项目学习的核心，也是面试中最能拉开差距的重点内容。我们遵循「从基础到高阶、从单模块到全链路」的逻辑，完整拆解智能 OnCall Agent 的所有核心模块。\n[第三章｜知识库 RAG 方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第三章｜知识库 RAG 方案设计与源码分析/)\n[第四章｜对话Agent方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第四章｜对话Agent方案设计与源码分析/)\n[第五章｜运维Agent方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第五章｜运维Agent方案设计与源码分析/)\n[第六章 | Tool 和 MCP 设计思路与源码分析](/oncall/智能 OnCall Agent 项目/第六章｜Tool 和 MCP 设计思路与源码分析/)\n[第七章｜前后端接口设计与前端实现](/oncall/智能 OnCall Agent 项目/第七章｜前后端接口设计与前端实现/)\n我们先从最基础、最核心的知识库 RAG Agent 入手，搞懂 RAG 的全流程设计，再到具备推理能力的对话 Agent，最后攻克高阶复杂场景的运维 Agent，每个模块都遵循「需求场景分析→架构设计拆解→多语言源码分析」的路径，先讲清「为什么这么设计」，再带你看懂「代码怎么实现」，完全贴合面试中讲述项目的逻辑。\n同时，我们会单独拆解 Agent 通用的核心能力 Tool 与 MCP，搞懂大模型的能力扩展逻辑；再完成前后端接口设计与前端实现的全链路闭环，让你彻底搞懂一个完整的 AI Agent 项目，从后端模块到前端交互的全部设计逻辑。\n如果你是初次接触，不用强求一次吃透所有源码细节，先把架构设计、方案逻辑理解到 7 成以上，就可以顺利推进学习；随着学习深入，建议你反复回看本阶段的内容，尤其是架构设计部分，通常需要 3 次以上的精读，才能彻底领悟其中的精髓，在面试中做到流畅输出。\n第三阶段：落地实战演练 —— 从环境搭建到项目完整运行 本阶段对应专栏第八章，核心目标是帮你把前面的理论知识，转化为可落地的实操能力，补全项目落地的所有细节。\n[第八章 | 实战演练与运行项目](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/)\n[项目源码（Go、Java、Python）](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/项目源码（Go、Java、Python）/)\n在这里，你可以获取项目 Go、Java、Python 三版本的完整源码，跟着教程完成开发环境准备，一步步把项目完整运行起来；同时我们也准备了多语言的入门小实战，帮你快速上手对应框架的基础用法，解决动手过程中的所有基础问题。\n如果你的时间充裕，强烈建议你完整通读对应语言的源码，动手运行并测试项目功能，这会极大加深你对项目的理解；如果时间紧张，也可以快速浏览内容，对项目落地全流程有完整认知，再直接进入面试准备环节。\n第四阶段：面试求职冲刺 —— 把项目变成你的 offer 敲门砖 本阶段对应专栏第九章，也是我们整个项目学习的最终目标：帮你顺利通过技术面试，拿到心仪的 offer。\n[第九章 | 面试求职全攻略](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/) 在这一章里，我们会教你如何深度挖掘项目的技术难点与差异化亮点，搞懂这个项目相比其他同类项目，有哪些核心竞争优势；同时手把手教你把项目写进简历，打造一份能抓住面试官眼球的技术简历；还会汇总项目配套的高频面试题、优质面经，帮你做好面试的全流程准备。\n和架构设计章节一样，本章也需要你反复精读 3 遍以上，只有把项目亮点、表达逻辑、高频考点彻底内化，才能确保你在面试中脱颖而出，清晰、流畅地展现你对项目的深度理解。\n学习计划周期 本项目的核心学习周期，我们精心设计为 14 天（2周），用最高效的方式带你从 AI 新手，蜕变为能完整讲清、落地 AI Agent 项目的开发者，完全适配求职冲刺的节奏。\n学习阶段 章节 学习目标 建议周期 认知筑基 - [第一章｜AI 名词大扫盲](/oncall/智能 OnCall Agent 项目/第一章｜AI 名词大扫盲/) [第二章 | 项目全局认知与技术选型](/oncall/智能 OnCall Agent 项目/第二章 _ 项目全局认知与技术选型/) | 扫清概念障碍，建立项目全局认知，搞懂项目核心价值与架构全貌 | 2 天 | | 核心模块拆解 | - [第三章｜知识库 RAG 方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第三章｜知识库 RAG 方案设计与源码分析/) [第四章｜对话Agent方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第四章｜对话Agent方案设计与源码分析/) [第五章｜运维Agent方案设计与源码分析](/oncall/智能 OnCall Agent 项目/第五章｜运维Agent方案设计与源码分析/) [第六章｜Tool 和 MCP 设计思路与源码分析](/oncall/智能 OnCall Agent 项目/第六章｜Tool 和 MCP 设计思路与源码分析/) [第七章｜前后端接口设计与前端实现](/oncall/智能 OnCall Agent 项目/第七章｜前后端接口设计与前端实现/) | 分模块吃透三大 Agent 的方案设计、源码逻辑，掌握 Tool 与 MCP 核心能力，完成全项目链路闭环设计 | 6 天 | | 落地实战演练 | [第八章 | 实战演练与运行项目](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/) | 完成环境搭建，动手运行项目，通过小实战巩固基础，完成理论到落地的闭环 | 4 天 | | 面试求职冲刺 | [第九章 | 面试求职全攻略](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/) | 提炼项目亮点，优化简历，吃透高频面试题，完成面试全流程准备 | 持续反复学习，面试前重点冲刺 | 在 12 天的集中学习期内，你就能系统掌握从理论基础、架构设计到 Agent 项目全链路实现的核心内容；而面试准备环节，将作为你求职冲刺的重中之重，建议你在面试前反复精读打磨，确保把项目经验彻底内化于心，在面试中做到游刃有余。\n核心目标：面试 对于目标是面试的同学来说，学会将有限的时间和精力投入到最具产出的环节至关重要。\n面试必要环节 理解项目背景和需求： 永远把这个项目是做什么的，它解决了什么实际问题放在第一位。\n系统设计与细节设计： 仔细阅读整体设计文档，并对关键的设计点（比如 RAG、ReAct、Plan-Execute-Replan 等）进行深入理解和展开。\n面试题准备： 了解面试官可能会问什么，这反过来也是检验你学习成果、查漏补缺的最佳方式。在这个过程中，记住一个关键点：每道题目都需要自己完整地、口述地讲述五遍以上。直到你自己满意为止，而不是仅仅停留在理解了的阶段。\n有效输出： 理解是远远不够的，最具效果的学习方式是能够清晰地回答、用文字讨论、以及用语音讲述。因为面试时都是口头讲述，你需要在短短两分钟内讲清楚一个话题。一个题目练习五遍，至少能帮你回答好百分之六十以上的问题。没有经过反复的练习，怎么能指望自己在面试时能侃侃而谈呢？\n非必要环节 深入相关技术知识： 如果你在看代码时有很多疑问，往往是因为你对代码所依赖的相关技术（比如 Java/Go 语言、Spring AI / Eino 框架的使用等）了解太少。你需要先学习这些基础知识，再看代码效果才会好。\n代码实现： 你可以大致了解代码的实现思路，但如果时间紧张，不必强求自己手动实现所有代码。数据显示，在我们训练营中成功拿到大厂 Offer 的数百位同学里，有百分之九十以上都没有自己完整地编写代码。\n部署： 部署的细节可以按照题库中的描述来讲述，不需要把时间花费在实际的部署操作上。\n面试简历项目打造 在学习完项目之后，我们就需要把项目写到简历上。一个好的项目写法，需要分为3点：\n项目介绍：用一句话讲清楚你这个项目有什么功能，解决了什么痛点。\n个人职责：分点叙述项目的功能，这里最好能介绍这个功能用了什么技术，或者解决了什么问题\n项目亮点/技术难点：分点叙述亮点/难点，并对相关关键技术名词进行加粗。\n那么在你学完项目，搞懂项目之后，你的简历就可以这样写项目：[面试简历的写法](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/面试简历的写法/)\nGo版本： Java版本： Python版本： ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E5%BF%85%E8%AF%BB%EF%BD%9C%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E6%94%BB%E7%95%A5/","summary":"各位林友，你们好！ 很多同学在初次接触 AI Agent 时，总会陷入「懂点零散概念，却串不起完整项目」「背了面试题，却讲不清项目设计逻辑」「想动手实战，却不知道从哪个模块切入」的困境，尤其想靠 AI Agent 项目冲击面试时，更是找不到","title":"必读｜智能 OnCall Agent 攻略"},{"content":"Agent 需要哪些 Tool ？ 要明确需要哪些工具，需先锚定每个Agent的核心目标与场景痛点，再对应工具的价值：\n对话Agent 核心需求：智能问答交互\n所需工具query_internal_docs：该工具基于RAG召回技术，能从向量数据库中快速匹配与问题相关的知识片段。支撑知识检索，回答 API鉴权失败怎么办 等问题时，直接定位文档关键片段\n运维Agent 核心需求：自动告警响应、日志/监控联动排查、故障根因分析。\n所需工具：\nquery_prometheus_alerts：实时获取告警详情\nquery_log：通过腾讯云CLS MCP实现自然语言日志检索（如 服务下线前5分钟错误日志 ）\nget_current_time：实时获取当前时间\nquery_internal_docs：补充运维经验（如故障处理步骤），形成完整排查闭环\n总结：四个工具覆盖所有Agent需求，其中query_internal_docs是通用支撑工具，其他工具聚焦运维Agent的故障排查场景。\n核心 Tool 设计 query_prometheus_alerts：告警数据精准提取 功能定位：实时获取 Prometheus 告警信息，为大模型提供故障诊断的第一手数据。\n实现细节：\nAPI 对接：调用 Prometheus 官方告警查询接口 GET /api/v1/alerts。\n数据提取逻辑：从 JSON 响应中解析关键字段，结构化输出给大模型：\nalertname：告警名称（如 服务下线 ），来自 labels.alertname； description：告警详情（如 广告微服务下线 ），来自 annotations.description； activeAt：告警触发时间（如 2025-10-29T08:48:42Z ），直接提取 activeAt 字段。 核心优势：实时性强，能快速获取故障诊断的起点数据。\nquery_internal_docs：知识库精准召回 功能定位：检索内部文档（如故障处理手册），为所有Agent提供精准的知识片段检索服务。\n实现细节：\n文档存储：采用向量数据库存储文档，支持语义相似度检索；\n调用触发：当大模型判断需要背景知识时（如 服务下线的处理步骤 ），自动调用该工具，传入关键词（如 服务下线 处理步骤 ），返回Top N相关文档片段。\n核心优势：避免大模型 幻觉 ，答案严格基于检索到的事实性内容。\nget_current_time：时间感知能力补充 功能定位：解决大模型 时间失忆 问题，为Agent提供实时时间信息，辅助时间维度的计算（如告警持续时长计算）。\n实现细节：\n极简接口：极简接口返回多格式时间（秒/毫秒级时间戳、YYYY-MM-DD HH:MM:SS字符串），Agent自动前置调用需时间参数的场景。\n调用时机：当工具需要时间参数时（如 查询过去1小时的告警 ），Agent 自动前置调用该工具。\n核心优势：轻量高效，支撑运维Agent的时间校准需求。\nMCP 集成 query_log ：自然语言驱动的日志检索 功能定位：让运维/业务人员用日常语言查询日志，降低技术门槛\n实现细节：\nMCP 对接：集成腾讯云CLS MCP，由MCP自动将自然语言转为查询语句，统一返回结构化日志结果\n调用逻辑：Agent 将用户问题（如 查询服务下线前 5 分钟的错误日志 ）直接转发给 CLS MCP，MCP 自动生成日志查询语句并返回结果，Agent 再将结果整理后提交给大模型。\n核心优势\n低门槛：让业务人员用日常语言查日志（如 服务下线前5分钟的错误日志 ），无需学Lucene/SQL语法\n动态适配：MCP 自动处理不同日志源的语法差异，统一输出格式。\n典型场景举例：服务下线告警处理流程 以 服务下线 告警为例，展示 Agent 如何协同 Tool 与 MCP 完成任务：\n数据采集：调用query_prometheus_alerts获取告警详情（名称、触发时间）；\n时间校准：通过get_current_time计算告警持续时长（如 已持续11分钟 ）；\n日志回溯：调用query_log传入 广告微服务 下线前5分钟 错误日志 ，CLS MCP返回关键日志片段；\n知识补充：大模型触发query_internal_docs，检索 服务下线 处理步骤 文档，获取标准化操作指南；\n决策输出：整合上述数据，生成含日志摘要、处理步骤、历史案例的故障报告。\n总结 通过标准化Tool接口（实时告警数据、知识检索、时间感知）与灵活MCP集成（自然语言查询日志），让大模型从 被动对话 升级为 主动诊断 ，高效衔接监控数据、内部知识与业务系统，真正实现AI在运维场景的落地价值\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AD%E7%AB%A0%EF%BD%9CTool%20%E5%92%8C%20MCP%20%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%EF%BC%9ATool%20%E5%92%8C%20MCP%20%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF/","summary":"Agent 需要哪些 Tool ？ 要明确需要哪些工具，需先锚定每个Agent的核心目标与场景痛点，再对应工具的价值：     对话Agent 核心需求 ：智能问答交互 所需工具 query_internal_docs ：该工具基于RAG召","title":"方案设计：Tool 和 MCP 设计思路"},{"content":"编辑器安装 Go编辑器Goland安装 https://www.jetbrains.com/go/download/\n进行官网按照你电脑的对应版本即可\nGoland Eino-Dev插件安装 进入设置-\u0026gt;插件，搜索eino，安装这个Eino Dev\n📷 [图片 token=FVKDb9NwHoWNEHxVqmLcIfranvb（未能下载，见飞书原文）]\nGo 环境 Go安装：https://goframe.org/docs/install-go/index\nGo module配置：https://goframe.org/docs/install-go/go-module\nJava编辑器IDEA安装 https://www.jetbrains.com/idea/download/\n进行官网按照你电脑的对应版本即可\nPython编辑器PyCharm安装 https://www.jetbrains.com/pycharm/download/\n进行官网按照你电脑的对应版本即可\ndocker安装 https://www.runoob.com/docker/windows-docker-install.html\n根据你的电脑类型选择windows or mac安装即可\n📷 [图片 token=R89vbIf4MoqCQIx8eicc94Kxnod（未能下载，见飞书原文）]\n修改镜像源\n{ \u0026#34;builder\u0026#34;: { \u0026#34;gc\u0026#34;: { \u0026#34;defaultKeepStorage\u0026#34;: \u0026#34;20GB\u0026#34;, \u0026#34;enabled\u0026#34;: true } }, \u0026#34;experimental\u0026#34;: false, \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://docker.hpcloud.cloud\u0026#34;, \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://docker.unsee.tech\u0026#34;, \u0026#34;https://docker.1panel.live\u0026#34;, \u0026#34;http://mirrors.ustc.edu.cn\u0026#34;, \u0026#34;https://docker.chenby.cn\u0026#34;, \u0026#34;http://mirror.azure.cn\u0026#34;, \u0026#34;https://dockerpull.org\u0026#34;, \u0026#34;https://dockerhub.icu\u0026#34;, \u0026#34;https://hub.rat.dev\u0026#34;, \u0026#34;https://proxy.1panel.live\u0026#34;, \u0026#34;https://docker.1panel.top\u0026#34;, \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://docker.1ms.run\u0026#34;, \u0026#34;https://docker.ketches.cn\u0026#34; ] } 大模型开通 Go版本（默认用字节的大模型） 字节跳动的火山云，新注册送50w token：https://console.volcengine.com/home\n注册好后，创建api key。这个api key需要你记住，等会要放到配置文件里面的：https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey\n📷 [图片 token=R5zBbG4nZos3EOx5NUbcudENn4c（未能下载，见飞书原文）]\n开通2个模型：https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement 语言模型 -\u0026gt; DeepSeek-V3.1 开通 📷 [图片 token=CEG0bHhSpom1VXxNhY7cHlejnJb（未能下载，见飞书原文）]\n向量模型 -\u0026gt; Doubao-embedding 开通 📷 [图片 token=XAMTb3JaRoKN2WxNoflcQzYOnOb（未能下载，见飞书原文）]\nJava版本（默认用阿里的大模型） 先登录阿里云，新注册也免费送token：https://bailian.console.aliyun.com/?tab=model#/model-market\n创建api key：https://bailian.console.aliyun.com/?tab=model#/api-key\n阿里云的模型不需要开启，可以直接使用，记住上面创建的密钥即可\nPython版本（默认用阿里的大模型） 先登录阿里云，新注册也免费送token：https://bailian.console.aliyun.com/?tab=model#/model-market\n创建api key：https://bailian.console.aliyun.com/?tab=model#/api-key\n阿里云的模型不需要开启，可以直接使用，记住上面创建的密钥即可\nCLS MCP配置 登陆腾讯云：https://console.cloud.tencent.com/\n创建密钥，secret id/key保存下来，后面要用：https://console.cloud.tencent.com/cam/capi\n📷 [图片 token=M0uCbwzPtoHxP3xaeWrcSKDQnfh（未能下载，见飞书原文）]\n任意找一个目录，创建.env文件，里面填入： TRANSPORT=sse TENCENTCLOUD_SECRET_ID=\u0026lt;上面你保存的SECRET_ID\u0026gt; TENCENTCLOUD_SECRET_KEY=\u0026lt;上面你保存的SECRET_KEY\u0026gt; PORT=3000 TZ=Asia/Shanghai 启动 SSE 服务，在.env目录下执行： # 需要提前装好node js：https://nodejs.org/en/download npx -y cls-mcp-server@latest # 出现这个日志代表启动成功 Started cls-mcp-server in sse transport on port 3000. 启动后，地址为： http://localhost:3000/sse Go版本配置修改处：\n📷 [图片 token=K1cqbGSwgoxLJBxwISgcWJGZnWf（未能下载，见飞书原文）]\nJava版本配置修改处：\n📷 [图片 token=BBLvbMMVEoHYfjxiHNrcqjWznsc（未能下载，见飞书原文）]\nPython版本配置修改处：\n📷 [图片 token=QoiubNsaFoSbJYxfZ8occbCrnJf（未能下载，见飞书原文）]\n验证MCP是否连接成功，请在页面提问：你有哪些工具\n📷 [图片 token=QM2kb3JzPoQdR1xBZ3lcRrIIn2b（未能下载，见飞书原文）]\n📷 [图片 token=UHtrbXDhNodoDOxwbA5cZAmQnRd（未能下载，见飞书原文）]\n项目配置 Go版本 替换：\napi_key：如果你也用火山云，按照上述开通两个模型后，只需要替换下面的 api_key 即可（所有模型共用同一个api key，不需要申请多个！）\nfile_dir：用于存储用户上传的文档目录，自行选择一个目录即可\ncls_mcp_url：mcp的地址，替换成上一步骤的url即可\n路径：SuperBizAgent/manifest/config/config.yaml\n📷 [图片 token=NbiHbnROnovxSixWBM0cQAxEnhd（未能下载，见飞书原文）]\nJava版本 替换：\n把红框里面替换成前面注册的阿里的api key\nsse-endpoint替换成上面注册的mcp地址\n路径：SuperBizAgent/src/main/resources/application.yml\n📷 [图片 token=DSSgbPuc9oP0Wyx7jQ0cixqznWh（未能下载，见飞书原文）]\nPython版本 配置文件路径：super_biz_agent_py/.env\nPython版本只需要修改 DASHSCOPE_API_KEY 即可跑起来\n在启动项目之前，请完整的看完README.md，里面写的非常详细，一键启动脚本也帮你写好了\n📷 [图片 token=BvJbbsL5mo4Qgvx34acc3kPTnad（未能下载，见飞书原文）]\n📷 [图片 token=FbfRbo02YoVSrZxacr2cFXvFnME（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87%E6%95%99%E7%A8%8B/","summary":"编辑器安装  Go编辑器Goland安装 \u003ca href=\"https://www.jetbrains.com/go/download/\"\u003ehttps://www.jetbrains.com/go/download/\u003c/a\u003e 进行官网按照你电脑的对应版本即可  Goland Eino-Dev插件安装 进入设置-\\ 插件，搜索eino，安装这个E","title":"环境准备教程"},{"content":"从这篇文章开始，我们将正式进入智能 OnCall Agent 系统的学习准备阶段。在接下来的篇幅里，我们会为你讲清楚这个系统到底长什么样，它能实现哪些功能。请跟我一起扫清这些基础认知障碍，后续的学习就会更加轻松高效！\n项目背景：为什么要做智能OnCall Agent？ 在日常工作中，OnCall 值班是一个无法避免的杂活。相信你一定经历过这些场景：\n深夜被告警电话惊醒，迷迷糊糊爬起来查看日志、翻阅监控。\n日志、监控、告警通知群等系统是割裂的，需要不断切换多个系统才能定位问题。 重复执行搜error日志-\u0026gt;看指标-\u0026gt;修数据的流程。\n这些场景不仅高频，而且让人痛苦，却长期以来都依赖人工处理。\n**智能 OnCall Agent **正是为了解决这些痛点而诞生的。它能够像一位资深工程师一样工作：自动接收告警、按照标准化流程进行排查、分析根因并给出处理建议，甚至还能自动执行修复操作，彻底将我们从重复的劳动中解放出来。\n智能OnCall Agent是什么？ 简单来说，OnCall Agent 是一个基于 AI 的运维自动化助手。它的核心目标就是取代人工，完成那些高频、标准化的告警处理工作。我们可以从以下几个维度来理解它的定义：\n自动响应： 能够实时接收监控系统发出的告警通知，彻底摆脱人工值守。\n智能排查： 按照预设的逻辑，自动调用日志、监控等工具，聚合多方数据来定位问题。\n根因分析： 通过匹配历史处理案例和故障处理手册，给出精准的根因解释。\n闭环沉淀： 自动总结每一次处理过程，并更新知识库，让整个系统能够越用越聪明。\n请注意，它不是要完全替代工程师，而是要成为你的得力助手。处理百分之八十的重复性、简单问题，让工程师可以将精力聚焦于百分之二十的复杂故障。\n智能OnCall Agent能做什么？ 结合值班场景的实际痛点，系统的核心功能可以概括为三个主要方面：\n**文件上传知识库（知识库 Agent）：**这个功能支持团队将各种文档（比如故障处理手册、服务接入说明等）上传到系统。它为大模型提供了可靠的知识来源，实现了团队经验的持续沉淀和高效复用。\n**对话交互支持（对话 Agent）：**值班人员可以通过自然语言与系统进行交互，快速查询故障处理手册。例如，你可以直接问：错误码 120000001 的原因是什么？系统会立即从《XX系统错误码手册》中返回精准的错误原因。\n**AI Ops 运维诊断（运维 Agent）：**系统接收到告警通知后（例如接口失败率过高），会按照预设的步骤自动开始排查。它可能会调用日志 API 查询最近一小时的 error 日志，调取监控面板查看失败率的趋势，然后聚合数据并返回关键信息，比如：近三十分钟内 context cancel 错误占比百分之九十，疑似为下游系统异常。\n📷 [图片 token=TwLXbq6ksoTSnNxpUo1cuUJ6n3d（未能下载，见飞书原文）]\n智能OnCall Agent 系统架构长啥样 了解了功能之后，我们再来看一下系统的架构图。总的来说，OnCall Agent 系统包含四个接口、三个核心 Agent 以及多个辅助组件。你现在看到这些架构图和功能描述可能看不懂，可能对整个系统的协作方式还不太清楚，这没关系，先留个初步的印象即可，后面的章节会有层层递进的详细分析。\n这里先介绍一下贯穿整个项目的三个最核心的 Agent：\n知识库Agent ： 当你需要让 AI 回答特定领域问题时，如果直接将长文本丢给大模型，会受限于模型的上下文窗口大小，导致成本高、速度慢、准确率低。知识库 Agent通过采用先检索相关内容，再生成答案的策略，完美地解决了这些问题。\n对话 Agent： 这是一个智能交互系统，它能够像真人一样理解你的问题、调用知识库，并给出精准的回答。它尤其适合用来处理高频重复的咨询类场景。\n运维 Agent： 它可以像资深工程师一样，自动接收告警、按预设步骤排查问题、分析根因并给出处理建议，甚至能够自动执行标准化操作，将工程师从重复劳动中彻底解放出来。\n📷 [图片 token=MJzLbm1gRoLNxxxjNgtcxBoPnLe（未能下载，见飞书原文）]\n项目拿去面试的亮点是什么？ 我们这个 Agent 项目的核心是解决了团队的真实痛点，但在面试时，你需要将这些痛点转化为具有技术深度和业务价值的亮点。结合面试官的高频关注点，我们可以从以下方向拆解项目的亮点：\n架构设计上的亮点：\n**RAG知识库：**将散落的技术文档、告警手册、历史工单等转化为统一的数据资产\n对话 Agent： 将重复咨询-\u0026gt;文档检索-\u0026gt;问题匹配的逻辑抽象成一个中台知识引擎，支持多场景的无缝复用。\n运维 Agent： 实现了跨系统联动架构，打破了日志、监控、告警群、文档之间的信息孤岛。\n技术实现上的亮点：\n**RAG知识库：**无缝对接对话Agent、运维Agent等多个上层应用。一次开发、多场景受益。\n对话 Agent： 动态分解复杂任务、减少幻觉，并通过实时环境反馈自主调整行动。\n运维 Agent： 采用了结构化任务执行模式，确保告警排查流程能够标准化、准确地执行。\n项目的亮点非常多，不会在这里具体展开。关于项目亮点和详细的解释，请查阅《面试攻略：面试亮点打造》。\n项目功能演示 对话功能演示 快速对话：最常见的对话功能\n流式对话：通过SSE技术让前端界面流式输出回答\n文档上传知识库后的提问效果：\n第一次询问错误码的时候，大模型由于没有《告警处理手册》，所以无法分析错误码的原因。\n第二次询问错误码之前，我们上传了对应的文档。这一次，大模型成功告诉我们错误码的原因是什么。\n🎬 视频「基于知识库对话.mp4」（飞书视频，无法在博客播放）\n多轮对话与工具调用演示 与大模型进行多轮对话，大模型能记得历史对话。\n大模型根据提问，自动查询日志。让大模型拥有调用工具的能力。\n🎬 视频「对话功能多轮对话与辅助查询日志.mp4」（飞书视频，无法在博客播放）\nAI Ops智能诊断 点击右上角的AI Ops，进行根源诊断。\n大模型返回了一份告警分析报告，并指出现在有一个服务下线的告警，原因是runtime panic。\n这份告警分析报告里还给出了技术分析和处理方案，在最上面还可以看到大模型执行的详细步骤。\n自动根据告警、日志、监控排查分析，协助值班人员快速定位与处理告警，提升值班效率。\n🎬 视频「运维分析告警.mp4」（飞书视频，无法在博客播放）\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%8C%E7%AB%A0%20_%20%E9%A1%B9%E7%9B%AE%E5%85%A8%E5%B1%80%E8%AE%A4%E7%9F%A5%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/%E8%83%8C%E6%99%AF%EF%BC%9A%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89%E6%99%BA%E8%83%BD%20OnCall%20Agent%E9%A1%B9%E7%9B%AE%EF%BC%9F/","summary":"从这篇文章开始，我们将正式进入智能 OnCall Agent 系统的学习准备阶段。在接下来的篇幅里，我们会为你讲清楚这个系统到底长什么样，它能实现哪些功能。请跟我一起扫清这些基础认知障碍，后续的学习就会更加轻松高效！  项目背景：为什么要做","title":"背景：为什么要有智能 OnCall Agent项目？"},{"content":"前言 注意，看到这篇文章的简历写法，不要直接复制到你的简历上。一定要魔改一下，修改修改。避免大家的简历都太相似了，除非你是第前1000个吃螃蟹拿去面试的人。（最早来学习这个项目并拿去面试的同学有福了）\n下面给几种写法思路，你理解了之后可以复制给AI，让AI帮你改改项目描述。\n[!WARNING] 简历PDF制作可以用小林的网站：https://jianli.xiaolinnote.com/（免费+无水印）\nGo版本 写法1-分点叙述 智能OnCall Agent系统 项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。 技术栈：Goframe、Eino、RAG、ReAct、Plan-Executor、Multi-Agent、MCP 个人职责：\n负责 AI Agent 架构设计：基于 Eino 框架设计并实现了多个AI Agent，包括 Knowledge Index Agent、Chat ReAct Agent和 Plan-Execute-Replan Agent 负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。 **负责对话功能开发：基于 ReAct 模式实现了对话Agent 。**实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求。 负责AIOps功能开发：基于** Plan-Execute 模式实现了智能运维Agent，解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。** 项目亮点： **AI Agent 架构设计：**通过图编排实现模块化的 Agent 工作流。设计并实现了多通用工具集，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。 RAG 知识库系统设计：在 RAG 系统中，需要处理文档分块大小和检索TopK参数的选择。分块过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+ 对话功能：支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。 AIOps功能：实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议的完整业务流程。能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。 写法2-稍微魔改 Auto OnCall 系统\n项目介绍：Auto OnCall 系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n技术栈：Goframe、Eino、RAG、ReAct、Plan-Executor、Multi-Agent、MCP（Go语言选手）\n技术栈：SpringBoot、SpringAI-Alibaba、RAG、ReAct、Plan-Executor、Multi-Agent、MCP（Java语言选手）\n个人职责：\n负责企业知识库检索问答系统实现，采用向量数据库存储文档向量，基于检索增强生成（RAG）技术实现文档智能问答和知识检索\n基于Eino实现业务工具的智能编排能力，将知识库召回、日志查询、监控查询等功能封装为标准化工具。实现 LLM 根据用户意图自主选择工具、解析参数、组合调用的完整链路\n负责 AI 对话的实时交互系统设计与实现，基于SSE实现流式对话推送，支持多用户并发对话场景。实现对话历史的持久化存储和上下文自动加载，保证对话连贯性\n负责 AI Ops的设计与实现，基于** Plan-Execute 模式实现了智能运维Agent，实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议**的完整业务流程。\n项目亮点：\n对话上下文窗口超限导致 LLM 调用失败。设计对话摘要机制，保留最近50轮完整对话和历史摘要，在保证上下文连贯性的前提下，上下文 Token 使用率降低 60%\n多用户并发对话存在上下文混淆问题。采用用户 ID 作为会话隔离标识，为每个会话分配独立的对话记忆实例，实现万级并发用户的对话隔离和状态管理\nAI 响应速度慢影响用户体验。通过流式输出机制实现 LLM 响应的逐字推送，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发**。**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。\n写法3-完全合并 智能OnCall Agent系统\n项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n技术栈：Goframe、Eino、RAG、ReAct、Plan-Executor、Multi-Agent、MCP（Go语言选手）\n技术栈：SpringBoot、SpringAI-Alibaba、RAG、ReAct、Plan-Executor、Multi-Agent、MCP（Java语言选手）\n个人职责：\n负责 AI Agent 架构设计：基于 Eino 框架设计并实现了多个AI Agent，包括 **Knowledge Index Agent、Chat ReAct Agent 和 Plan-Execute-Replan Agent，**通过图编排实现模块化的 Agent 工作流。设计并实现了多工具集成方案，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。\n负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。在 RAG 系统中，需要处理文档分割大小和检索TopK参数的选择。分割过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+\n负责对话功能开发：基于 ReAct 模式实现了对话Agent 。实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求，支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发：基于** Plan-Execute-Replan 模式实现了智能运维Agent**。实现了** 根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议 **的完整业务流程，**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。**能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。\nJava版本 写法1-分点叙述 智能OnCall Agent系统 项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。 技术栈：SpringBoot、SpringAI-Alibaba、RAG、ReAct、Plan-Executor、Multi-Agent、MCP 个人职责：\n负责 AI Agent 架构设计：基于 SpringAI-Alibaba 框架设计并实现了多个AI Agent，包括 Knowledge Index Agent、Chat ReAct Agent和 Plan-Execute-Replan Agent 负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。 **负责对话功能开发：基于 ReAct 模式实现了对话Agent 。**实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求。 负责AIOps功能开发：基于** Plan-Execute 模式实现了智能运维Agent，解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。** 项目亮点： **AI Agent 架构设计：**通过图编排实现模块化的 Agent 工作流。设计并实现了多通用工具集，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。 RAG 知识库系统设计：在 RAG 系统中，需要处理文档分块大小和检索TopK参数的选择。分块过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+ 对话功能：支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。 AIOps功能：实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议的完整业务流程。能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。 写法2-稍微魔改 Auto OnCall 系统\n项目介绍：Auto OnCall 系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n技术栈：SpringBoot、SpringAI-Alibaba、RAG、ReAct、Plan-Executor、Multi-Agent、MCP\n个人职责：\n负责企业知识库检索问答系统实现，采用向量数据库存储文档向量，基于检索增强生成（RAG）技术实现文档智能问答和知识检索\n基于SpringAI-Alibaba实现业务工具的智能编排能力，将知识库召回、日志查询、监控查询等功能封装为标准化工具。实现 LLM 根据用户意图自主选择工具、解析参数、组合调用的完整链路\n负责 AI 对话的实时交互系统设计与实现，基于SSE实现流式对话推送，支持多用户并发对话场景。实现对话历史的持久化存储和上下文自动加载，保证对话连贯性\n负责 AI Ops的设计与实现，基于** Plan-Execute 模式实现了智能运维Agent，实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议**的完整业务流程。\n项目亮点：\n对话上下文窗口超限导致 LLM 调用失败。设计对话摘要机制，保留最近50轮完整对话和历史摘要，在保证上下文连贯性的前提下，上下文 Token 使用率降低 60%\n多用户并发对话存在上下文混淆问题。采用用户 ID 作为会话隔离标识，为每个会话分配独立的对话记忆实例，实现万级并发用户的对话隔离和状态管理\nAI 响应速度慢影响用户体验。通过流式输出机制实现 LLM 响应的逐字推送，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发**。**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。\n写法3-完全合并 智能OnCall Agent系统\n项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n技术栈：SpringBoot、SpringAI-Alibaba、RAG、ReAct、Plan-Executor、Multi-Agent、MCP\n个人职责：\n负责 AI Agent 架构设计：基于 SpringAI-Alibaba 框架设计并实现了多个AI Agent，包括 **Knowledge Index Agent、Chat ReAct Agent 和 Plan-Execute-Replan Agent，**通过图编排实现模块化的 Agent 工作流。设计并实现了多工具集成方案，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。\n负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。在 RAG 系统中，需要处理文档分割大小和检索TopK参数的选择。分割过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+\n负责对话功能开发：基于 ReAct 模式实现了对话Agent 。实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求，支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发：基于** Plan-Execute-Replan 模式实现了智能运维Agent**。实现了** 根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议 **的完整业务流程，**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。**能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。\nPython版本 写法1-分点叙述 智能OnCall Agent系统 项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。 技术栈：LangChain、LangGraph、RAG、ReAct、Plan-Executor、Multi-Agent、MCP 个人职责：\n负责 AI Agent 架构设计：基于 LangChain 框架设计并实现了多个AI Agent，包括 Knowledge Index Agent、Chat ReAct Agent和 Plan-Execute-Replan Agent 负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。 **负责对话功能开发：基于 ReAct 模式实现了对话Agent 。**实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求。 负责AIOps功能开发：基于** Plan-Execute 模式实现了智能运维Agent，解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。** 项目亮点： **AI Agent 架构设计：**通过图编排实现模块化的 Agent 工作流。设计并实现了多通用工具集，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。 RAG 知识库系统设计：在 RAG 系统中，需要处理文档分块大小和检索TopK参数的选择。分块过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+ 对话功能：支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。 AIOps功能：实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议的完整业务流程。能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。 写法2-稍微魔改 Auto OnCall 系统\n项目介绍：Auto OnCall 系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n**技术栈：**LangChain、LangGraph、RAG、ReAct、Plan-Executor、Multi-Agent、MCP\n个人职责：\n负责企业知识库检索问答系统实现，采用向量数据库存储文档向量，基于检索增强生成（RAG）技术实现文档智能问答和知识检索\n基于 LangChain 实现业务工具的智能编排能力，将知识库召回、日志查询、监控查询等功能封装为标准化工具。实现 LLM 根据用户意图自主选择工具、解析参数、组合调用的完整链路\n负责 AI 对话的实时交互系统设计与实现，基于SSE实现流式对话推送，支持多用户并发对话场景。实现对话历史的持久化存储和上下文自动加载，保证对话连贯性\n负责 AI Ops的设计与实现，基于** Plan-Execute 模式实现了智能运维Agent，实现了根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议**的完整业务流程。\n项目亮点：\n对话上下文窗口超限导致 LLM 调用失败。设计对话摘要机制，保留最近50轮完整对话和历史摘要，在保证上下文连贯性的前提下，上下文 Token 使用率降低 60%\n多用户并发对话存在上下文混淆问题。采用用户 ID 作为会话隔离标识，为每个会话分配独立的对话记忆实例，实现万级并发用户的对话隔离和状态管理\nAI 响应速度慢影响用户体验。通过流式输出机制实现 LLM 响应的逐字推送，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发**。**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。\n写法3-完全合并 智能OnCall Agent系统\n项目介绍：智能OnCall系统通过AI Agent解决团队真实痛点，整合知识库、对话、运维三大核心能力，实现问题自动应答和故障智能排查的一体化服务。致力于降低团队OnCall的人力成本，提升团队效率。\n**技术栈：**LangChain、LangGraph、RAG、ReAct、Plan-Executor、Multi-Agent、MCP\n个人职责：\n负责 AI Agent 架构设计：基于 LangChain 框架设计并实现了多个AI Agent，包括 **Knowledge Index Agent、Chat ReAct Agent 和 Plan-Execute-Replan Agent，**通过图编排实现模块化的 Agent 工作流。设计并实现了多工具集成方案，包括通过 MCP 协议集成日志查询工具、Prometheus 告警查询工具、MySQL 数据操作工具、联网查询等，使 AI Agent 能够灵活调用外部工具完成复杂任务。\n负责 RAG 知识库系统设计：设计了完整的文档向量化存储和检索方案，支持内部文档的智能检索增强生成。使 AI 能够基于内部知识库提供准确的业务咨询和技术支持。在 RAG 系统中，需要处理文档分割大小和检索TopK参数的选择。分割过小会丢失上下文，过大会降低检索精度；TopK 过大会影响性能，过小可能遗漏关键信息。经过测试验证，确定了最优参数组合，知识检索准确率达到 85%+\n负责对话功能开发：基于 ReAct 模式实现了对话Agent 。实现业务咨询、告警自救、工单预处理等场景无缝切换，一次开发覆盖研发、运维、业务方多角色需求，支持多轮对话上下文记忆，并通过容错处理优化体验。同时基于SSE技术实现AI对话流式输出，解决大模型响应延迟问题，前端呈现实时对话效果。\n负责AIOps功能开发：基于** Plan-Execute-Replan 模式实现了智能运维Agent**。实现了** 根据告警信息 → 检索知识库 → 规划执行步骤 → 调用工具查询 → 分析结果 → 生成建议 **的完整业务流程，**解决了传统运维需要人工排障的低效问题，将运维响应时间从小时级降低到分钟级。**能够自动根据内部文档查询监控信息和日志信息，通过工具调用实现自动化监控告警查询和日志分析，并结合历史工单生成运维建议方案。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/%E9%9D%A2%E8%AF%95%E7%AE%80%E5%8E%86%E7%9A%84%E5%86%99%E6%B3%95/","summary":"前言 注意，看到这篇文章的简历写法，不要直接复制到你的简历上。一定要魔改一下，修改修改。避免大家的简历都太相似了，除非你是第前1000个吃螃蟹拿去面试的人。（最早来学习这个项目并拿去面试的同学有福了） 下面给几种写法思路，你理解了之后可以复","title":"面试简历的写法"},{"content":"这篇文档是「AI Native全链路开发实战」知识库的统一入口。你不需要第一次就把所有文章从头读到尾，更合适的做法是先确定目标：是想尽快用上 Codex，还是想掌握 OpenSpec 的规范驱动流程，或者准备深入阅读 OncallAgent 的真实源码。\n📷 [图片 token=VbjQbNHr4oA5DVxIC5JcwKsDnSc（未能下载，见飞书原文）]\n整套内容围绕两个问题展开：怎样让 AI Coding 从“能生成代码”变成可规划、可验证、可维护的工程协作；怎样在一个真实项目中理解知识检索、Agent、MCP、AIOps 诊断与证据报告之间的关系。\n📷 [图片 token=BBxHb3eXioPK4axcBzocMolKnjc（未能下载，见飞书原文）]\n[!SUCCESS] **第一次阅读建议：**先完成「AI Coding 基础与工程约束」，再进入 OpenSpec 实践，最后阅读 OncallAgent 源码系列。已经有相关经验的读者，可以直接按下方目标表跳转。\n这套知识库包含什么 02. AI Coding 基础与工程约束负责解决“怎样与 Codex 协作”的问题。从工具安装、需求表达，到 SDD 与 AGENTS.md，它帮助你建立最基础的 AI 编程工作习惯。\n03. OpenSpec 工程化实践：从变更建模到知识沉淀负责解决“怎样把需求变成可追踪工程资产”的问题。这里会讲 proposal、delta specs、design、tasks、main specs，以及实现、验证、同步、归档和知识站沉淀的完整过程。\n04. OncallAgent 功能与源码解析负责解决“这些工程方法怎样落到真实项目”的问题。OncallAgent 是一个本地优先的 AIOps Agent 工作台，前端使用 Vue 3、Vite 和 TypeScript，后端使用 FastAPI，并实现了知识检索、流式 Agent、用户级 MCP、LangGraph 诊断与证据报告等链路。\n📷 [图片 token=T8vhbCQ3DosGACxzFMncG5eonEh（未能下载，见飞书原文）]\n按学习目标选择入口 你的目标 建议先读 读完应该获得什么 第一次接触 AI Coding Codex 与 OpenSpec 快速安装使用 完成环境准备，并走通一次最小 OpenSpec 流程 经常遇到 AI 跑偏或返工 规范驱动开发 SDD、AGENTS.md 详解 理解需求规范与仓库规则分别约束什么 想系统掌握 OpenSpec OpenSpec 全流程实操介绍 建立从变更提出到归档沉淀的完整心智模型 想理解 RAG 与流式 Agent 07. 知识文档上传、切分、索引与页面状态、09. LangChain 流式 Chat Agent 与前端 SSE 看清知识进入系统、被检索并参与回答的链路 想理解真实 AIOps 诊断 11. 用户级 MCP 连接与真实工具调用、12. 告警到证据报告的 LangGraph 诊断闭环 理解从告警、SOP、工具取证到证据报告的闭环 准备简历或技术面试 OpenSpec 高频追问与回答、怎么把 OpenSpec 融入简历描述 把真实做过的工程工作转化为准确表达 推荐主线：从会用工具到读懂项目 **第一阶段：建立 AI Coding 的基本动作。**先读项目开发经历，理解 AI 提升速度的前提并不是“自动完成一切”，而是人先做出边界与验收判断；然后完成 Codex 和 OpenSpec 的安装，再学习 SDD 与 AGENTS.md。\n📷 [图片 token=WCCTbvnyGoCKurxZZ45c0j2znrr（未能下载，见飞书原文）]\n一个人用 AI 写一个 Agent 项目需要多久？\nCodex 与 OpenSpec 快速安装使用\n规范驱动开发 SDD：让 AI 永远在轨道上\nAGENTS.md 详解：让 Codex 真正理解并遵守仓库规则\n**第二阶段：把一次需求沉淀为可检查的变更。**进入 OpenSpec 专题后，不要只记命令。重点是看清每份产物回答的问题、它们怎样相互约束，以及代码和测试怎样成为验收证据。\n**第三阶段：用 OncallAgent 验证这些方法。**先建立架构全景，再按照共享契约、数据边界、知识链路、Agent 运行时、真实工具和诊断闭环逐步下钻。阅读源码时同时对照主规格与测试，不要仅凭文件名猜测系统行为。\n**第四阶段：完成一次自己的纵向练习。**选择一个范围足够小的需求，从 proposal、spec、design 和 tasks 开始，经过实现、测试、verify、sync 与 archive。练习的目标不是生成更多代码，而是形成一条别人可以复查的需求—实现—证据链。\n📷 [图片 token=UnxmbIJe4ob4WlxKXAPcchD2n3d（未能下载，见飞书原文）]\nOpenSpec 专项阅读路线 如果你的主要目标是掌握 OpenSpec，可以按照“先看完整流程，再理解单个产物，随后学习追踪、归档和案例”的顺序阅读。\n📷 [图片 token=SB9xbpEWZoe8WsxFHVAcLzkbnTd（未能下载，见飞书原文）]\nOpenSpec 开发闭环：先建立从提出变更到完成归档的全局视角。\nOpenSpec 核心产物：逐篇理解元数据、proposal、delta spec、design、tasks 与 main spec 的职责边界。\nOpenSpec 追踪链路介绍：学习怎样把需求、场景、设计、任务、代码和测试连成证据链。\nOpenSpec 归档与知识沉淀：理解 archive、wiki-sync 与仓库知识站之间的分工。\nVitePress：启动与浏览 OncallAgent 项目知识站：在本地查看归档变更形成的项目知识页面。\nOpenSpec 真实案例：通过 Qwen Embedding 批量限制和检索阶段排名可见化理解不同规模的变更。\nOncallAgent 源码阅读路线 OncallAgent 目录中的 15 篇文章保持平铺编号。下面的分段只是阅读建议，不会改变实际目录结构。第一次阅读建议按编号前进；已经熟悉某一部分时，可以直接从对应阶段切入。\n📷 [图片 token=HIGsbjDENohYLLxFW6Ncgb6lnec（未能下载，见飞书原文）]\n**01～05：先建立系统边界。**这一段依次解释组合根、共享契约、认证与 tenant 隔离、模型配置，以及 Milvus 等本地基础设施。\n📷 [图片 token=NXXfb6VUOoHaOXxCEYocuCpenZf（未能下载，见飞书原文）]\n01. OncallAgent 架构全景与源码阅读地图\n02. HTTP、错误、OpenAPI 与 SSE 共享契约\n03. 用户认证与 tenant 数据隔离\n04. Qwen 模型接入与本地安全配置\n05. 本地基础设施与 Milvus 向量边界\n**06～10：理解知识链路与对话运行时。**这一段从持久化后台任务开始，串起知识文档摄取、混合检索、LangChain 流式聊天，以及 Prompt、Skill 和记忆配置。\n📷 [图片 token=XKibb2xUQoF5ekxLhXgcMPKonOq（未能下载，见飞书原文）]\n06. SQLite Durable Background Job 运行机制\n07. 知识文档上传、切分、索引与页面状态\n08. Milvus、BM25L、RRF 与 rerank 混合检索\n09. LangChain 流式 Chat Agent 与前端 SSE\n10. Prompt、渐进式 Skill 与会话记忆\n**11～15：进入真实工具与 AIOps 闭环。**这一段关注用户级 MCP、Alertmanager 告警入口、LangGraph 诊断、案例沉淀、工具审计、用户反馈和本地运行状态。\n📷 [图片 token=NS9VbuSyHoPB8bx1HExcRUkgnje（未能下载，见飞书原文）]\n11. 用户级 MCP 连接与真实工具调用\n12. 告警到证据报告的 LangGraph 诊断闭环\n13. 诊断案例自动沉淀与知识复用\n14. 工具调用审计与结构化用户反馈\n15. Readiness、可观测性与本地运行\n阅读时始终保留三条工程判断 **先确认边界，再让 AI 执行。**需求、非目标、接口契约和验收方式如果没有明确，模型生成得越快，返工可能越多。\n**让规格、实现和测试互相印证。**OpenSpec 产物不是为了增加文档数量，而是让每个重要判断都有位置，让代码变更可以被追踪和验证。\n**证据不足就明确说明。**OncallAgent 的诊断结论必须建立在授权知识、真实 MCP 工具结果和持久化证据之上。没有日志、指标或工具返回时，不能编造根因、执行动作或成功状态。这既是项目约束，也是学习 AI Native 工程时最重要的可信性原则。\n📷 [图片 token=WwXsbRmj1o9XYaxLFKocg00SnQb（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/01%EF%BD%9C%E5%BC%80%E5%A7%8B%E8%BF%99%E9%87%8C%EF%BC%9A%E7%9F%A5%E8%AF%86%E5%BA%93%E5%AF%BC%E8%88%AA%E4%B8%8E%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/","summary":"这篇文档是「AI Native全链路开发实战」知识库的统一入口。你不需要第一次就把所有文章从头读到尾，更合适的做法是先确定目标：是想尽快用上 Codex，还是想掌握 OpenSpec 的规范驱动流程，或者准备深入阅读 OncallAgent","title":"01｜开始这里：知识库导航与学习路线"},{"content":"在 OncallAgent 中，HTTP 与 SSE 不是两套互不相干的传输实现，而是前端、后端和 Agent 生命周期之间的公共语言。共享契约位于 packages/api-contracts，覆盖响应 envelope、错误码、认证与领域 DTO、OpenAPI 路径以及流事件。这个包的价值不只是让 TypeScript 编译通过，更重要的是把“成功、失败、流式进度和恢复信息”变成可审查、可测试的产品行为。\n📷 [图片 token=QGrkbz8DHoFPVGxV0N2cpeGjnpc（未能下载，见飞书原文）]\nAI Native 应用比普通 CRUD 更需要稳定契约。一次聊天可能先输出内容增量，再触发工具调用、产生知识引用，最后完成或失败；一次 AIOps 诊断还会包含任务状态与报告。若每个页面或服务临时设计 payload，前端很快会依赖隐含顺序，错误也容易泄露上游异常。OncallAgent 用带判别字段的联合类型、统一错误消息和受保护 OpenAPI 表面来降低这种漂移。\n📷 [图片 token=HkQPb5CLsozo2vxnKnDcWpVOnhh（未能下载，见飞书原文）]\n共享契约并不自动保证 Python 实现正确。后端仍需在 apps/backend/src/super_ai/api/responses.py、apps/backend/src/super_ai/error_catalog.py 和 SSE 事件构造中手工对齐 TypeScript 形状。因此理解契约需要同时看定义、消费端、后端序列化与跨层测试，而不能只读一个类型文件。\n📷 [图片 token=ThOHbDDYcofTltxBX1lcUjQHnPW（未能下载，见飞书原文）]\n学习目标 掌握统一 HTTP 成功与错误 envelope 的判别方式及 request ID 作用。\n理解错误目录如何统一 category、HTTP 状态与安全默认消息。\n能区分 OpenAPI 的路径级安全声明与运行时认证、授权检查。\n能追踪聊天或诊断 SSE 从后端事件到前端异步迭代器的完整链路。\n识别流开始前的 HTTP 错误与流开始后的 error 事件这两种失败表面。\n📷 [图片 token=WMlzbrUwyoKLuBxk86wcFAuknOh（未能下载，见飞书原文）]\n功能入口与完整调用链 普通 HTTP 请求从 apps/frontend/src/api/apiClient.ts 的 createApiClient 开始。buildTransportHeaders 设置 Accept，在非 FormData 请求中补 Content-Type，并从回调读取 token 后加入 Authorization。响应交给 readResponseEnvelope：只有包含 ok 判别字段且形状可识别时才继续；ok: false 被包装为 ApiClientError，调用页面可以统一读取 code、category、httpStatus 和 message。\n📷 [图片 token=LO0ObktiyovPHixihvyczeW4nZc（未能下载，见飞书原文）]\n后端由 apps/backend/src/super_ai/api/app.py 的请求中间件生成或接收 x-request-id，并在响应头写回 X-Request-ID。成功路由调用 success_response，业务或权限失败抛出 ApiErrorException；全局异常处理器再调用 error_response。Pydantic 的 RequestValidationError 也被归一成 VALIDATION_INVALID_ARGUMENT，客户端无需理解 FastAPI 默认验证 payload。\n📷 [图片 token=GxDHbqhn0op3MMxOJAJcLqCXnBd（未能下载，见飞书原文）]\n流式请求由 apps/frontend/src/api/sseClient.ts 的 createSseClient 发起。若 HTTP 状态不是成功，仍按统一 HTTP 错误解析；只有拿到响应 body 后，才逐块读取字节、用 TextDecoder 拼接缓冲区，以空行拆分 SSE frame，并解析每个 data: JSON。后端聊天路由返回 StreamingResponse，apps/backend/src/super_ai/chat/streaming.py 的 encode_sse 把共享事件写成帧。浏览器得到的不是任意 JSON，而是至少含 id、type、channel 和 timestamp 的事件。\n📷 [图片 token=UiV2bObYvoKBxBxuFxDcLIEhnze（未能下载，见飞书原文）]\n请求前：共享 DTO → JSON 或 FormData → bearer HTTP 普通响应：success_response / error_response → ApiResponse → ApiClient 流响应：领域事件 → 共享 SseEvent → encode_sse → SSE frame → SseClient 流开始前失败：HTTP ApiErrorResponse 流开始后失败：type 为 error 的结构化 SseEvent 📷 [图片 token=FV5HbP8aKoQ64BxoLNGc5Uu5ngh（未能下载，见飞书原文）]\n以聊天为例，POST /chat/sessions/{sessionId}/messages:stream 在 OpenAPI 中声明 bearer 认证、请求体和 text/event-stream 响应。运行时先完成认证和 owner-scoped 会话查询，所以缺少 token 或跨 tenant 会话会在建流前返回 401 或 403。建流后，ChatStreamingService.stream_message 可发出 content.delta、reasoning.delta、tool.call、reference.source、complete 或 error。这一区分很重要：前端既要处理 fetch 失败，也要处理合法流中的错误事件。\n📷 [图片 token=ENQ2bscLyop4uRxnfVNcU5ZFnJg（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 packages/api-contracts/src/responses.ts ApiResponse、buildSuccessResponse、buildErrorResponse 定义成功/错误联合类型、metadata 与构造函数。 packages/api-contracts/src/errors.ts API_ERROR_CODES、ApiErrorCode 维护稳定错误码、类别、HTTP 状态和默认消息。 packages/api-contracts/src/sse.ts SSE_EVENT_TYPES、SseEvent 定义聊天与 AIOps 共用的带判别字段事件联合。 packages/api-contracts/src/openapi.ts OPENAPI_CONTRACT、protectedErrorResponses 描述路径、请求/响应 schema、bearer 安全方案与 401/403。 apps/backend/src/super_ai/error_catalog.py ERROR_DEFINITIONS 后端错误码到 category、status、message 的运行时映射。 apps/backend/src/super_ai/api/responses.py success_response、error_response、ApiErrorException 生成与共享契约对齐的 JSON envelope。 apps/backend/src/super_ai/chat/streaming.py encode_sse、_sse_event、_error_event 构造聊天事件并编码 SSE frame。 apps/frontend/src/api/apiClient.ts readApiError、readResponseEnvelope 严格解析 HTTP envelope并提供安全 fallback。 apps/frontend/src/api/sseClient.ts createSseClient、parseSseFrames 处理流式 fetch、增量缓冲、事件解析与坏帧错误。 packages/api-contracts/tests/api-contracts.test.ts HTTP response contracts、SSE event contracts、OpenAPI contract 锁定跨层契约的结构、覆盖面和安全声明。 📷 [图片 token=HkwBbLj9coSzgzxyYGicztZNn8c（未能下载，见飞书原文）]\n📷 [图片 token=FSXubgxlEoQjaQxVI3BcyuSOnJb（未能下载，见飞书原文）]\n📷 [图片 token=L19ybw7wDosIdtxEhm3cCETUn0g（未能下载，见飞书原文）]\n📷 [图片 token=Jqu5bcTWMoykoVxkGafcd1TSnne（未能下载，见飞书原文）]\n代码调用流程图 契约并不是单个类型文件，而是一条从前端传输层、FastAPI 路由到普通 HTTP 或 SSE 消费端的双分支链路。\n关键实现拆解 HTTP envelope 与 request metadata ApiSuccessResponse 固定为 ok: true、类型化 data 和 meta；ApiErrorResponse 固定为 ok: false、error 与同样的 meta。这比依赖 HTTP status 猜测 body 更稳定：202 的索引任务仍是成功 envelope，503 的 readiness 降级也可以携带完整组件诊断数据；业务失败则同时有传输状态和机器可读错误码。\n📷 [图片 token=LRQ2bG6kOoSRo3x47cscAKUunod（未能下载，见飞书原文）]\n后端 _request_id 优先使用中间件写入 request state 的 ID，其次读取请求头，最后生成新 ID。request ID 不代表分布式追踪已经完整实现，但它让浏览器错误、服务日志和 API 响应有稳定的关联键。共享类型还允许可选 traceId，当前 Python helper 只写 requestId，因此不应把可选字段描述为已在每个响应中产生。\n📷 [图片 token=NuMKbk3spoORW5xy7VscKkg9nTc（未能下载，见飞书原文）]\n看什么：共享类型把 ok 设为字面量判别字段，成功与失败都强制携带同一种 metadata。\n// 1. ok 为 true 时，消费者可以安全收窄到 data。 export interface ApiSuccessResponse\u0026lt;TData\u0026gt; { readonly ok: true; readonly data: TData; readonly meta: ApiResponseMeta; } // 2. ok 为 false 时，机器错误在 error，关联信息仍在 meta。 export interface ApiErrorResponse { readonly ok: false; readonly error: ApiErrorMessage; readonly meta: ApiResponseMeta; } export type ApiResponse\u0026lt;TData\u0026gt; = ApiSuccessResponse\u0026lt;TData\u0026gt; | ApiErrorResponse; 📷 [图片 token=HWT1bsMVbohfyIxX1GjcmDsnnMb（未能下载，见飞书原文）]\n代码证明客户端不需要通过“有没有 data 字段”猜测分支；202、503 等状态码也不改变 envelope 形状。边界在于 TypeScript 只约束编译期消费者，Python 返回值和线上异常仍需运行时构造与测试对齐，且可选 traceId 当前并非每次都生成。\n📷 [图片 token=CY4zbPDVmoXwhWxGuwhcosIzn5e（未能下载，见飞书原文）]\n看什么：request ID 从进入中间件到响应 meta 的关联路径，可解释浏览器报错如何回到一条服务端请求记录。\n图中的关联键方便排障，但不等同于完整分布式 trace；外部 Qwen、Milvus 或 MCP 是否传播同一 ID 不能由此推断。若处理器在统一 helper 之外崩溃，还要依赖框架错误路径和中间件日志，不能假设一定得到相同 envelope。\n📷 [图片 token=LNaubNFwYo4l8IxlPbhcRlyRnMf（未能下载，见飞书原文）]\n错误目录与安全消息 API_ERROR_CODES 当前覆盖认证、业务冲突或未找到、验证、系统不可用与内部错误。认证中特别区分 AUTH_UNAUTHENTICATED 的 401 和 AUTH_FORBIDDEN 的 403；登录失败统一为 AUTH_INVALID_CREDENTIALS，不告诉调用者是邮箱不存在还是密码错误。ErrorSseEvent 直接复用 ApiErrorMessage，避免流式失败另造一套含原始异常的 payload。\n📷 [图片 token=IZsnbCckfozRNox1vC8cZuTZnFb（未能下载，见飞书原文）]\n当前 error_response 支持 code 和可选 message，但没有把 FastAPI 参数级错误细节写入 details。共享契约允许 details，主规格也要求参数级验证信息；阅读当前实现时应如实区分“类型预留”与“运行时已经填充”。同理，后端全局只注册了业务错误和请求验证异常处理器；未预期异常仍由框架形成 500，日志中间件只记录异常类别。对外部 provider 的错误脱敏主要在 provider 与具体服务边界完成。\n📷 [图片 token=Vv92bOs9golDEwx7rqjcw1S2nNe（未能下载，见飞书原文）]\n看什么：Python helper 不复制 HTTP status 规则，而是从后端错误目录取得 category、status 和默认消息，再写成与共享类型相同的字段。\n📷 [图片 token=MMSrb5v6koiox3xiXwscx1Zwnxb（未能下载，见飞书原文）]\n# 1. code 决定 category、HTTP status 和默认安全消息。 def error_response(request: Request, code: str, *, message: str | None = None) -\u0026gt; JSONResponse: category, http_status, default_message = ERROR_DEFINITIONS[code] return JSONResponse( status_code=http_status, content={ \u0026#34;ok\u0026#34;: False, \u0026#34;error\u0026#34;: { \u0026#34;code\u0026#34;: code, \u0026#34;category\u0026#34;: category, \u0026#34;httpStatus\u0026#34;: http_status, \u0026#34;message\u0026#34;: message or default_message, }, # 2. requestId 关联客户端错误与服务端请求日志。 \u0026#34;meta\u0026#34;: {\u0026#34;requestId\u0026#34;: _request_id(request)}, }, ) 📷 [图片 token=VnUibHpdFoFjmcx5Bt8cGmu3nZb（未能下载，见飞书原文）]\n片段证明错误码同时控制传输与业务语义，并且原始异常不会自动进入响应。边界也很明确：调用者传入的自定义 message 必须先在领域边界完成脱敏；这个 helper 当前没有输出 details，因此不能把类型中的可选验证明细描述成已普遍实现。\n📷 [图片 token=ID8Fbe4WIoxDPyxm4dPctv5knch（未能下载，见飞书原文）]\n判别联合与事件生命周期 SSE_EVENT_TYPES 包含八种类型。content.delta 与 reasoning.delta 携带顺序号；tool.call 的状态是 started、delta、completed 或 failed；reference.source 可携带 chunk、文档、知识库、来源、向量/BM25/RRF/rerank 排名与分数；task.status 和 report 服务于长任务；complete 携带最终结果；error 携带统一错误。\n📷 [图片 token=B0JsbsC6uoNCAIxlPEzcWwAknSh（未能下载，见飞书原文）]\n聊天实现不会要求每条流都出现全部类型。没有工具调用就没有 tool.call，没有知识命中就没有引用。最终内容当前按单字符 content.delta 发出，非内容事件保持原粒度。工具调用 started 会先创建 owner-scoped 审计，terminal 状态再完成对应审计；审计持久化不应吞掉最终聊天输出。前端消费应根据 type 分派，而不是依赖事件位置或假定固定数量。\n📷 [图片 token=F71gbo5s0oSvKyxUEzLcsrqxn7c（未能下载，见飞书原文）]\n看什么：事件联合不仅区分内容与任务，还让终止事件和 HTTP 共享同一个 ApiErrorMessage。\n// 1. complete 与 error 都是显式事件类型。 export interface CompleteSseEvent extends SseEventBase\u0026lt;\u0026#34;complete\u0026#34;\u0026gt; { readonly result?: unknown; } export interface ErrorSseEvent extends SseEventBase\u0026lt;\u0026#34;error\u0026#34;\u0026gt; { readonly error: ApiErrorMessage; } // 2. 消费者必须按 type 分派，不能依赖固定事件顺序。 export type SseEvent = | ContentDeltaSseEvent | ReasoningDeltaSseEvent | ToolCallSseEvent | ReferenceSourceSseEvent | TaskStatusSseEvent | ReportSseEvent | CompleteSseEvent | ErrorSseEvent; 📷 [图片 token=VdHbblIYwoM57fxgztucKRnYnle（未能下载，见飞书原文）]\n这段联合证明内容、推理、工具、引用、任务和终态共享同一判别入口；没有某类事件仍是合法流。失败边界是：error 代表业务已知终止，而 TCP 断开、浏览器取消或坏 frame 只是客户端无法确认终态，需要回读持久状态。\n看什么：工具调用有自己的局部生命周期，而整条流只能走向完成、业务错误或连接未知三类终局。\n图中工具失败不必然等于整条流失败，最终由 Agent 与服务边界决定是否还能回答；而连接未知不能伪装成 error。工具审计使用 toolCall ID 配对，流终态使用事件 type，二者不能互换。\n📷 [图片 token=FxYEb4njOofxCqxzUThc1uSHn9W（未能下载，见飞书原文）]\nOpenAPI 是安全表面的机器可读索引 OPENAPI_CONTRACT 使用 OpenAPI 3.1，集中定义 health、认证、聊天、知识文档、索引、后台任务、反馈、MCP 与 AIOps 路径。bearerSecurity 和 protectedErrorResponses 被复用于受保护操作，使 401、403、验证、冲突和系统错误在文档层保持一致。文档上传策略来自 DOCUMENT_UPLOAD_POLICY，减少前端提示、OpenAPI 与后端校验之间的魔法数字。\n📷 [图片 token=VcUzbqjALod0UZxMek7ciJmFnIc（未能下载，见飞书原文）]\n但 OpenAPI 的 security 只是声明，不执行认证。真正的校验在 FastAPI Depends(_current_user)、_bearer_token 和 owner-scoped Repository 查询。反过来，路由已实现也不代表共享 OpenAPI 必然覆盖；契约测试中的 covers required backend surfaces 和 marks protected paths with bearer auth and unified 401/403 errors 正是用来防止两边分离。\n📷 [图片 token=EpagbNQ0coUwVvx79WYcTI4inxf（未能下载，见飞书原文）]\n看什么：受保护响应和 bearer security 被定义为可复用常量，路径声明不必重复手写 401/403 结构。\nconst unauthenticatedResponse = { description: API_ERROR_CODES.AUTH_UNAUTHENTICATED.message, content: jsonContent(\u0026#34;#/components/schemas/ApiErrorResponse\u0026#34;) } as const; const forbiddenResponse = { description: API_ERROR_CODES.AUTH_FORBIDDEN.message, content: jsonContent(\u0026#34;#/components/schemas/ApiErrorResponse\u0026#34;) } as const; // 1. 每个受保护操作复用统一 401、403 和通用错误。 const protectedErrorResponses = { \u0026#34;401\u0026#34;: unauthenticatedResponse, \u0026#34;403\u0026#34;: forbiddenResponse, ...errorResponses } as const; // 2. 安全声明引用同一个 bearerAuth scheme。 const bearerSecurity = [{ bearerAuth: [] }] as const; 📷 [图片 token=KqStbztF8oC7aWxMGN4cicsinjd（未能下载，见飞书原文）]\n📷 [图片 token=C5hcbmJD2oZEm7xDhlWcaMn5nHf（未能下载，见飞书原文）]\n代码证明 OpenAPI 能稳定表达安全表面，并让契约测试检查路径是否同时声明 bearer、401 和 403。它不执行身份验证，也不验证 Repository 是否真的按 owner 查询；因此 API 测试仍要用两个用户实际访问同一资源 ID，确认运行时返回统一 403。\n📷 [图片 token=E8ekbYPYroMMSdxFAsZcDRCInfc（未能下载，见飞书原文）]\n客户端恢复策略来自契约语义 契约不仅决定数据怎样解析，也决定失败后该做什么。401 表示当前认证不可继续，认证 store 可以移除 token、清空受保护数据并引导重新登录；403 表示调用者身份有效但目标资源不属于其范围，界面不应通过反复登录掩盖授权问题；409 通常提示资源状态冲突，例如重复文档需要用户明确覆盖；400 或 422 要把注意力放在输入；503 表示依赖暂不可用，可以保留用户输入并允许稍后重试。恢复策略应依据稳定 code 与 category，而不是对 message 做字符串匹配。\n📷 [图片 token=OiZmblmtlohJOOxaSXic0sYYnde（未能下载，见飞书原文）]\ncreateApiClient 对格式错误的成功响应也会抛系统错误，因为无法证明其中的 data 满足协议。createSseClient 对没有 body、坏 JSON 或缺少基础事件字段采取相同保守策略。这样的“严格消费”能尽早暴露后端漂移；如果客户端默默忽略未知结构，页面可能看似运行却丢失完成、引用或错误状态。\n📷 [图片 token=OElvbO9YyoImrZxqTTacfHQHn6b（未能下载，见飞书原文）]\n看什么：通用客户端先解析 envelope，再使用 ok 分支；格式错误不会被当作成功数据继续传播。\nreturn { async request\u0026lt;TData\u0026gt;(path: string, init: RequestInit = {}): Promise\u0026lt;TData\u0026gt; { const response = await fetchImpl(`${baseUrl}${path}`, { ...init, headers: buildTransportHeaders({ accept: \u0026#34;application/json\u0026#34;, body: init.body, headers: init.headers, token: options.getAccessToken() }) }); // 1. JSON 与 envelope 形状都在统一入口检查。 const payload = await readResponseEnvelope\u0026lt;TData\u0026gt;(response); if (!payload.ok) { // 2. 恢复策略读取稳定 code/status，而非 message 文本。 throw new ApiClientError(payload.error, response.status); } return payload.data; } }; 📷 [图片 token=KwcZbNl0eoEm2tx5jiEcA7FInih（未能下载，见飞书原文）]\n片段证明 bearer header、envelope 校验和类型化错误集中在同一传输层，页面无需复制解析逻辑。边界是 isApiResponse 只做基础结构检查，并不会完整验证每个领域 DTO；高风险字段仍要由领域测试和组件状态处理覆盖。\n终止事件与幂等观察 流式界面需要明确区分草稿和事实。content.delta 只是当前连接看到的增量，complete 才表示后端完成最终持久化并可给出结果。error 表示流在业务层终止；连接在两者之前断开则是第三种“不知道最终状态”的情况。聊天前端应重新读取会话历史协调屏幕草稿，AIOps 前端应依赖持久任务和事件恢复，而不是把最后收到的进度当作最终报告。\n📷 [图片 token=QoGabX5pFok2sHx6lftcPezVncd（未能下载，见飞书原文）]\n事件 id、任务 ID、消息 ID 和工具调用 ID 各自服务不同关联关系。工具调用的 started 与 completed/failed 通过稳定 toolCall ID 配对，引用用 chunk 或来源 ID 标识，任务状态用 task ID。不能把 SSE frame 的 id 当成业务对象主键，也不能仅凭 sequence 跨连接去重所有事件。当前契约为这些标识留出了明确位置，具体恢复仍由聊天历史、后台任务 repository 和诊断事件记录完成。\n📷 [图片 token=DHvlbon8Wotz3nxT0dkcHGUkn2b（未能下载，见飞书原文）]\n看什么：SSE 客户端保留不完整 frame 的 remainder，只有完整分隔后才解析和产出共享事件。\nconst reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = \u0026#34;\u0026#34;; for (;;) { const { done, value } = await reader.read(); if (done) { break; } // 1. 网络 chunk 不等于 SSE frame，先累计再解析。 buffer += decoder.decode(value, { stream: true }); const parsed = parseSseFrames(buffer); buffer = parsed.remainder; yield* parsed.events; } // 2. 流结束后刷新 decoder，但不会凭空补 complete。 buffer += decoder.decode(); const parsed = parseSseFrames(buffer); yield* parsed.events; 📷 [图片 token=TU2kbtgfJo7BGRxGagKcxJ6unIb（未能下载，见飞书原文）]\n代码证明客户端不会因为 TCP 分块而截断 JSON，也不会在连接结束时自动生成终止事件。若最后没有 complete 或 error，调用者必须把结果视为未知并回读会话或后台任务；仅重播屏幕上的 delta 不能证明服务端最终持久化成功。\n📷 [图片 token=V0IvbalKIolpfvxwKsXcCDzfn0g（未能下载，见飞书原文）]\n看什么：把“所见增量”和“可确认事实”分开，明确每种终止后应该读取哪个持久来源。\n这条恢复链只保证客户端不把未知状态误报为成功；幂等与断点读取仍依赖具体领域。聊天历史、后台 job event sequence 和诊断任务各有自己的业务 ID，不能用一个通用 SSE event ID 替代。\n契约演进的检查方法 增加字段时，优先把非必需展示信息设计为可选，并确保旧消费者仍能根据判别字段工作；改变字段含义、错误 code 或事件 type 则是更高风险变更，需要同步 OpenAPI、后端构造、前端分派和测试。删除一个 type 前还要检查持久化历史中是否可能存在该事件。共享包的类型检查只能发现编译期消费者，Python 字典和数据库中保存的 JSON 还必须由运行时测试覆盖。\n📷 [图片 token=ZsuybSfnfolJjkxjsg5cc6r6nFf（未能下载，见飞书原文）]\n尤其不要在某个页面为临时需求复制一份局部接口。局部复制会让上传策略、索引状态或引用分数字段出现两种解释。正确入口是修改 packages/api-contracts 的相应领域模块，从 packages/api-contracts/src/index.ts 导出，再让前端 client 和后端 serializer 同时对齐。这样代码审查能看到协议影响范围，而不是在页面 diff 中猜测后端行为。\n📷 [图片 token=ArOTbbMZwoY5XOxEdBBcjnD3nke（未能下载，见飞书原文）]\n看什么：下面的检查图把一次字段或事件变更拆成必须同步的五个落点，任何一处缺失都会产生可观察漂移。\n图中的共享包是入口而不是全部证明：TypeScript 无法检查 Python 字典或数据库历史 JSON。高风险变更还应覆盖旧事件、终止顺序、权限响应和安全消息；若删除字段或 type，必须先确认持久化数据与旧客户端的兼容策略。\n数据、契约与状态 错误 category 只有 auth、business、validation 和 system。category 适合界面选择呈现与恢复策略，code 适合精确业务分支，httpStatus 仍用于传输语义。例如 token 失效可以清理认证状态，409 冲突可以提示覆盖或刷新，503 可以提示稍后重试；不能只比较英文 message。\n📷 [图片 token=GXvSbB6idoApRYx2CgkcQaGKngf（未能下载，见飞书原文）]\nOpenAPI schema 之外，领域模块继续提供静态 DTO。packages/api-contracts/src/auth.ts 定义注册、登录、用户和 token；packages/api-contracts/src/documents.ts 定义上传策略、文档状态与 chunk 预览；packages/api-contracts/src/indexing.ts 定义索引任务状态；packages/api-contracts/src/chat.ts 与 packages/api-contracts/src/retrieval.ts 描述消息、完成结果和知识命中。这些类型从 packages/api-contracts/src/index.ts 统一导出，前端 client 直接导入，而不是在页面内复制接口。\n📷 [图片 token=BIftbnXCboxjdUxXZCucF4SknBd（未能下载，见飞书原文）]\nSSE 的 channel 是 chat 或 aiops，同一个 type 因而可以在不同业务流中复用。id 和 timestamp 支持事件标识与展示；sequence 只在增量事件内表达顺序。当前前端 parser 验证的是基础字段，不会逐类型深度校验所有 payload，因此后端事件构造与契约测试仍是防止字段漂移的主要保证。\n📷 [图片 token=AQOqb1ymtoY2iKxuh0jcQmCpnMb（未能下载，见飞书原文）]\n权限、安全与失败边界 认证与授权错误必须在建流之前尽早发生。stream_chat_message 先解析 bearer session，再按 owner_user_id 获取会话；跨 tenant ID 返回 AUTH_FORBIDDEN，Agent runner 未被调用。知识文档、索引任务和诊断路径同样先做 owner-scoped 查询。OpenAPI 中所有这类路径同时声明 bearer、401 与 403，避免调用者把“没登录”和“无权访问该对象”混为一谈。\n📷 [图片 token=NlUsbCBGlokj4nxlskVcx8s5ndB（未能下载，见飞书原文）]\n错误 payload 不应包含密码、token、API key、Authorization header、连接串、堆栈、用户消息、工具参数值或文档正文。apps/backend/src/super_ai/observability.py 的 _redact 清理敏感键，LLM readiness 对错误中的 API key 做替换，/ready 与 /config/check 将组件失败归一为安全消息。前端遇到非 JSON、非法 envelope、空流或坏 SSE 帧时也生成通用系统错误，不把解析细节直接显示给用户。\n📷 [图片 token=DzFabqHlRoLTXXxgbIhcAaCFn7g（未能下载，见飞书原文）]\n网络中断还有一个容易忽略的边界：HTTP 成功且已经开始读流后，连接可能在 complete 前断开。聊天服务的规格要求失败时不持久化部分 assistant 消息；前端则应保留可见失败状态并重新读取后端历史，而不是把屏幕上的草稿视为已完成事实。AIOps 长任务另有 durable job 和持久化事件恢复路径，不能仅依赖某一条浏览器连接。\n📷 [图片 token=CATybokKsoTqETxkew7cDiF1nXf（未能下载，见飞书原文）]\n阅读顺序与小结 先读 packages/api-contracts/src/responses.ts 与 packages/api-contracts/src/errors.ts，掌握 HTTP 判别字段和错误语义。\n再读 packages/api-contracts/src/sse.ts，按事件 type 建立流式状态机，而不是记页面代码。\n浏览 packages/api-contracts/src/openapi.ts 的路径、安全与 schema 复用方式。\n对照 apps/backend/src/super_ai/api/responses.py、apps/backend/src/super_ai/error_catalog.py 和 apps/backend/src/super_ai/chat/streaming.py 检查 Python 序列化。\n最后追踪 apps/frontend/src/api/apiClient.ts 与 apps/frontend/src/api/sseClient.ts，确认失败如何从服务端到达用户界面。\n这套共享契约的核心不是“多写一份类型”，而是让普通请求、长连接、工具生命周期、引用和错误都具有可预测形状。开发新 API 或事件时，应先更新共享定义与 OpenAPI，再同步后端序列化、前端消费和双方测试；安全上则始终区分 HTTP 建流失败、流内错误和连接中断。这样 Agent 的非确定执行才能被包在确定的工程协议里。\n📷 [图片 token=HzpAbUWQEomLmpxXwN4c7jPlnqg（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/02.%20HTTP%E3%80%81%E9%94%99%E8%AF%AF%E3%80%81OpenAPI%20%E4%B8%8E%20SSE%20%E5%85%B1%E4%BA%AB%E5%A5%91%E7%BA%A6/","summary":"在 OncallAgent 中，HTTP 与 SSE 不是两套互不相干的传输实现，而是前端、后端和 Agent 生命周期之间的公共语言。共享契约位于  packages/api-contracts ，覆盖响应 envelope、错误码、认证","title":"02. HTTP、错误、OpenAPI 与 SSE 共享契约"},{"content":"openspec/changes/archive/ 保存已经完成的 change。归档不是删除，也不是把几个 Markdown 压缩起来；它把一次变化的完整上下文从活动工作区移到带日期的历史目录，同时要求当前主规格已经反映最终行为。\n📷 [图片 token=AGwBbQ3p0oPgOTx2bXpc1k1Yn2f（未能下载，见飞书原文）]\n归档前要检查什么 OncallAgent 的本地 archive Skill 依次检查 artifact 状态、tasks 完成度和 delta spec 同步状态。\n**Artifact 完整性。**proposal、specs、design、tasks 是否都处于 done。缺产物时会警告，不能假装 change 已经完整。\n**任务完成度。**统计 - [ ] 与 - [x]。存在未完成项时必须显式确认风险。\n**规格同步评估。**逐个比较 delta spec 与 openspec/specs/\u0026lt;capability\u0026gt;/spec.md，说明哪些 requirement 需要新增、修改、删除或重命名。推荐先 sync，再归档。\n**实现验证。**archive Skill 自身的文件检查不能替代 openspec-verify-change、测试、类型检查和构建。归档是生命周期动作，不是自动质量认证。\n📷 [图片 token=QXknbVmKsoiBTmxxm2ccYX76nPd（未能下载，见飞书原文）]\n归档后的目录结构 openspec/changes/archive/ └── 2026-07-11-limit-qwen-embedding-batch-size/ ├── .openspec.yaml ├── proposal.md ├── design.md ├── tasks.md └── specs/ └── qwen-openai-provider/ └── spec.md 日期前缀让目录天然表达归档时间，原 change 名保留语义。所有产物一起移动，因此以后既能看到最终需求和设计，也能看到当时的任务状态与 delta。.openspec.yaml 继续保存 schema 和创建时间。\n📷 [图片 token=VUIIbCT4gohdOuxbnBpc3ySVncb（未能下载，见飞书原文）]\n主规格和 archive 分别回答什么 主规格回答“系统当前应该怎样表现”。archive 回答“某次变化为什么发生、当时考虑了什么、怎样实施和验收”。两者一起才构成可追溯工程记录。\n以主案例为例，当前是否要求 Embedding 每批最多 10 条，应查看 main spec；为什么把限制放在 Provider 层、为什么不修改索引业务、当时接受了怎样的吞吐代价，应查看 archive 中的 proposal 和 design。\n📷 [图片 token=UKD3bFtUdoTo2kxrvdncZindnio（未能下载，见飞书原文）]\n为什么归档目录不能继续开发 直接修改旧 archive 会改写历史，使当时的 proposal、tasks 与实际提交不再对应，也可能让 VitePress WIKI 在不知情的情况下展示被篡改的决策。\n如果后续厂商把上限提高到 20，应创建新的 change，例如 raise-qwen-embedding-batch-size，用新的 proposal 解释动机，用 MODIFIED delta 更新 requirement，再记录新的设计和测试。这样演进链完整，而不是把旧的“10”悄悄改成“20”。\n📷 [图片 token=MEFmbU3kwo8kKqxTUj3cluA6nwb（未能下载，见飞书原文）]\n归档不等于 Git 回滚 OpenSpec 保存意图与规格演进，Git 保存代码和文件版本。需要撤销实现时，应使用经过审查的 Git revert，或创建一个反向 OpenSpec change 来表达新行为；OpenSpec archive 本身不会自动还原代码。\n同样，归档目录被保留并不代表可以删除相关测试。测试仍然保护当前行为，除非新的 change 明确改变该行为。\n📷 [图片 token=VOesbBzdtoaF5sxdH5scLwI7nfh（未能下载，见飞书原文）]\n本地 Skill 与参考站的差异 参考站点的 v2.x 文档把 archive 概括为 CLI 合并 delta 并移动目录。OncallAgent 当前本地 archive Skill 采用 Agent 驱动的 sync 评估，必要时调用同步流程，然后用文件移动把 changeRoot 放入日期目录。教学和面试应以仓库本地 Skill 为准，不应把外部示例说成已经在本机执行的实现。\n另一个边界是：通用 archive Skill 在没有 delta specs 时可以继续，但 OncallAgent 的 wiki-sync 要求每个归档都有 delta spec，缺失时默认阻断。仓库级约束比通用流程更严格，这是为了保证每个 WIKI 历史页都能建立规格追踪。\n📷 [图片 token=TlCVbCrrnoCo1Hxxkvdc6wxcnmb（未能下载，见飞书原文）]\n归档后的下一步 OpenSpec 目录移动完成后，还需要运行仓库级 wiki-sync，并执行 VitePress 构建：\npython3 .codex/skills/wiki-sync/scripts/sync_wiki.py archive \\ limit-qwen-embedding-batch-size npm run docs:build 这两步不会改变产品行为，而是保证归档内容可以在仓库 WIKI 中浏览，索引和 Sidebar 与文件系统一致。OpenSpec 结构本身还应运行 openspec validate --all；若本机没有 CLI，必须明确报告未执行。\n📷 [图片 token=YTZmbYjGAoMlyDxcg4VcYbfanCg（未能下载，见飞书原文）]\n面试表达 归档不是把需求删掉，而是把已完成 change 从活动区移动到带日期的审计区。归档前检查产物、任务、实现和 delta 同步；归档后主规格保存当前事实，archive 保存决策历史。后续变化新建 change，不能改旧 archive 重写历史。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E5%BD%92%E6%A1%A3%E4%B8%8E%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/archive%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"openspec/changes/archive/  保存已经完成的 change。归档不是删除，也不是把几个 Markdown 压缩起来；它把一次变化的完整上下文从活动工作区移到带日期的历史目录，同时要求当前主规格已经反映最终行为。 \u0026lt;i","title":"archive文件作用介绍"},{"content":" 面向第一次接触 AI Coding 的同学：从环境准备，到完成一次可检查、可追踪的代码修改。\n前言：为什么 AI 会“听不懂”你的需求 你可能已经在抖音、B 站、小红书或各种技术社区里刷到过很多 AI 内容：几句话生成网页、自动修复 Bug、编写脚本，甚至一个人完成过去一个团队的工作。\n但真正开始使用 AI Coding 后，你可能马上遇到这些问题：第一句话不知道怎么说；不知道需求要写多细；不确定应该先规划还是直接写代码；明明觉得自己说清楚了，AI 却朝错误方向写了一大段。代码没有完成，Token、时间和耐心却消耗了不少。\n📷 [图片 token=EIIPbysDpo3xT6xtHZMcCXmkn9b（未能下载，见飞书原文）]\n这通常不只是模型能力的问题。无论你使用订阅工具、API 服务，还是豆包、千问、Kimi、ChatGPT 等产品，只要目标、背景、限制和验收标准不清楚，AI 就很难稳定地交付你真正需要的结果。\n因此，学习 AI Coding 不能只学“怎样让 AI 写代码”，还要学会：\n怎样把一个模糊想法变成清楚的需求；\n怎样让 AI 先理解项目，再开始修改；\n怎样用任务和测试检查结果；\n怎样把需求、设计和实现记录在仓库里，方便以后继续。\n本文用三个工具组成一套容易理解的学习流程：Codex 负责在项目中工作，OpenSpec 负责记录和管理变更，Superpowers 可选地提供头脑风暴、TDD 和调试等工程方法。\n📷 [图片 token=JqpRbCaRrohk6VxojVQcJ2YRnUg（未能下载，见飞书原文）]\n1. 先认识三个工具 1.1 Codex：真正执行开发任务的 AI 编码 Agent Codex 是 OpenAI 的编码 Agent。打开一个项目后，你可以用自然语言让它阅读代码、解释实现、修改文件、运行命令、执行测试和检查结果。它不只是聊天窗口，而是能够围绕代码仓库完成任务的协作工具。\n例如，不要只说：\n帮我加一个深色模式。\n可以改成：\n请先阅读这个 Vue 项目的主题和布局实现，为设置页增加深色模式。刷新后要保留用户选择，不改动登录流程，并补充相关组件测试。先说明影响范围，再开始修改。\n后一句提供了项目背景、目标、限制和验收要求，AI 更不容易跑偏。\n📷 [图片 token=K4dyb96wWobxg5xYtN2cxct4ndf（未能下载，见飞书原文）]\nCodex 的界面、基本操作和完整演示可以参考：Codex 使用教程（B 站）。安装和账号要求可能随版本变化，请同时以 Codex 应用内提示和 OpenAI 官方文档为准。\n如果要开通ChatGPT Plus，可以参考这个文章：2026年最新ChatGPT充值教程，支持国内支付宝和微信充值 GPT 5.6、Plus、Pro、Codex等\n如果你希望了解 Codex 接入国产模型的思路，可以参考：Codex 接入国产模型（B 站）。需要注意，第三方或 OpenAI-compatible 模型并不一定完整支持 Codex 所需的工具调用、结构化输出和长任务能力；API Key 也不要写进 Git 仓库、截图或聊天记录。\n1.2 OpenSpec：把需求变成仓库里的开发说明书 OpenSpec 可以理解为 AI Coding 中的“规范驱动开发层”。它不会代替 Codex 写代码，而是帮助 Codex 和开发者先把一次变更整理清楚。\n一个常见的 OpenSpec change 会包含：\nproposal.md：为什么要做、准备改变什么；\ndesign.md：准备怎样实现、有哪些技术取舍；\ntasks.md：可以逐项完成和勾选的任务；\nspecs/.../spec.md：新增或修改的行为以及验收场景。\n这样，“做什么、为什么做、怎样做、怎样才算完成”就不只存在于聊天记录里，而会成为仓库的一部分。以后换一个会话，或者由同学继续开发，也能根据这些文件恢复上下文。\n在本仓库中，已经生效的规范位于 openspec/specs/，正在进行的变更位于 openspec/changes/\u0026lt;change-name\u0026gt;/，完成的历史变更位于 openspec/changes/archive/。归档目录只用于追溯，不应继续在里面开发。\n📷 [图片 token=SkH3bzLLboTijexonExctSaXngg（未能下载，见飞书原文）]\n1.3 Superpowers：可选的工程方法工具箱 Superpowers 可以理解为一套给 AI Agent 使用的工程技能和工作流。它常见的思路包括：先通过 brainstorming 澄清问题，再设计方案；用 test-driven-development 按“红—绿—重构”推进；遇到失败时进行 systematic-debugging；交付前执行 verification-before-completion。\n它解决的是“AI 应该怎样做事”，OpenSpec 解决的是“需求和变更怎样沉淀”。二者并不冲突：OpenSpec 像项目说明书，Superpowers 像工程纪律。\n📷 [图片 token=FvsebJh4LoWIyHx6HUXcxaY9nMG（未能下载，见飞书原文）]\nSuperpowers 是可选项，而且插件名称、安装入口和可用 Skill 会随 Codex 版本、平台或组织策略不同。没有安装它也能完成本文练习：直接要求 Codex“先澄清、按 TDD 实现、完成前验证”即可。\n2. 环境准备清单 开始前请准备：\n一台 Windows、macOS 或 Linux 电脑；\nGit，用来获取项目和查看代码变化；\nNode.js 与 npm，用来安装 OpenSpec；\nCodex 应用或 Codex CLI（如果要开通ChatGPT Plus，可以参考这个文章：2026年最新ChatGPT充值教程，支持国内支付宝和微信充值 GPT 5.6、Plus、Pro、Codex等）\n一个可以练习的 Git 项目，建议先使用测试项目，不要直接修改重要作业或生产代码。\n每安装一个工具都要马上检查版本。这样出现问题时，你能判断究竟是“没有安装”，还是“已经安装但配置不正确”。\n📷 [图片 token=C6D8bLdcloImAwxaEqqcWFQcnye（未能下载，见飞书原文）]\n3. 安装 Codex 从 OpenAI 官方渠道下载适合自己系统的 Codex，或按照官方文档安装 Codex CLI。\n如果想要开通 chatgpt plus 套餐的，教程看这个：2026年最新ChatGPT充值教程，支持国内支付宝和微信充值 GPT 5.6、Plus、Pro、Codex等\n完成登录或所需的 API 配置。\n在 Codex 中打开你的项目文件夹。\n先提出一个只读问题，例如：“请阅读这个仓库的 README，告诉我如何启动项目，暂时不要修改文件。”\n如果无法登录或加载模型，请依次检查网络是否能访问所使用的官方服务、账号是否有相应权限、系统时间是否正确，以及学校或公司的网络策略是否拦截连接。不要使用来源不明的安装包、代理脚本或共享 API Key。\n📷 [图片 token=IgoRb7qfKoaY0lx30RzcJjFBnqo（未能下载，见飞书原文）]\n4. 安装 OpenSpec 4.1 安装 Node.js 前往 Node.js 官网 安装仍在维护的 LTS 版本。安装完成后，重新打开终端并运行：\nnode --version npm --version 两条命令都能输出版本号，才能继续。\n📷 [图片 token=JcS1bCcP0oars7xGMbYc9WEAnkc（未能下载，见飞书原文）]\n4.2 全局安装 OpenSpec 在 macOS、Linux 或普通命令行中运行：\nnpm install -g @fission-ai/openspec@latest 在 Windows PowerShell 中，如果 npm 被执行策略拦截，可以运行：\nnpm.cmd install -g @fission-ai/openspec@latest 然后验证：\nopenspec --version openspec --help 📷 [图片 token=WRlSbw4aXo0sKJx5ewucpqtKnch（未能下载，见飞书原文）]\n如果系统提示找不到 openspec，先关闭并重新打开终端，再检查 npm 的全局可执行目录是否已经加入 PATH。不要反复执行不同来源的安装脚本，否则会让环境更难排查。\n4.3 在项目中初始化 进入你的项目根目录，先查看当前版本支持的命令：\nopenspec --help 对于还没有 openspec/ 的项目，按照当前版本帮助执行初始化命令（常见命令为 openspec init）。初始化后，确认仓库中出现 OpenSpec 目录和相关配置，再让 Codex读取其中的说明。\n本仓库已经完成初始化，不要重复初始化。你应该能看到：\nopenspec/ ├── specs/ # 已生效的主规格 └── changes/ ├── \u0026lt;change-name\u0026gt;/ # 正在进行的变更 └── archive/ # 已完成的历史变更 📷 [图片 token=IMQRbJm90ooTPgxQDricYhXbnKf（未能下载，见飞书原文）]\n5. 可选：安装 Superpowers 如果你使用的 Codex 版本提供插件市场，可以打开“设置 → 插件”，搜索 Superpowers，阅读插件来源、权限和说明后再安装。安装完成后重新打开任务，并让 Codex列出当前可用的相关 Skill，确认是否真的包含 brainstorming、TDD、debugging 和 verification 等能力。\n📷 [图片 token=Rbncb9qT7oOIVbxku5icGHDhnse（未能下载，见飞书原文）]\n如果搜索不到，说明当前平台、账号或插件源没有提供该插件。此时不需要卡在安装步骤，可以直接使用这样的提示词：\n先通过提问澄清需求，再给出实现方案；实现时先补失败测试，再写最小代码让测试通过；完成前运行相关测试并检查 Git diff。\n📷 [图片 token=UIzjbB3b3oB2PqxniFwcyWrtnVd（未能下载，见飞书原文）]\n6. OpenSpec 的完整使用流程 下面以“增加深色模式”为例。变更名称使用小写英文和连字符：add-dark-mode。\n📷 [图片 token=YzYYbf3kwoLm8SxseLncTbAYngd（未能下载，见飞书原文）]\n第一步：需求不清楚时，先讨论 你可以对 Codex 说：\n先用 brainstorming 的方式帮我梳理深色模式需求。请确认适用页面、颜色来源、是否跟随系统、是否保存用户选择，以及验收标准。先不要写代码。\n即使没有 Superpowers，Codex 也可以按这段自然语言完成澄清。\n📷 [图片 token=AhiMbhJepoygGMxYMvqcnrxdngc（未能下载，见飞书原文）]\n第二步：创建 OpenSpec 变更 在本仓库中，直接说：\n使用 openspec-propose 创建 add-dark-mode 变更。先阅读现有主规格、实现和测试，生成 proposal、design、tasks 和需要的 delta specs，内容使用简体中文。\n如果你的安装环境提供 OpenSpec 斜杠命令，也可以使用：\n/opsx:propose add-dark-mode 生成后不要急着实现。先阅读 proposal.md、design.md 和 tasks.md，重点检查功能边界、风险与验收步骤。规划错了，后面的代码通常也会错。\n📷 [图片 token=Wuu8bsY9noNdNYxtydhcSBW4nsd（未能下载，见飞书原文）]\n第三步：按任务实现 确认方案后，对 Codex 说：\n使用 openspec-apply-change 实现 add-dark-mode，按 tasks.md 顺序完成。遵守仓库 AGENTS.md，先写或更新测试，再修改实现；每完成一项就勾选任务。\n有斜杠命令的环境也可以使用：\n/opsx:apply add-dark-mode 如果实现过程中出现 Bug，可以说：\n使用 systematic-debugging 的方式排查这个失败：先复现并收集证据，说明根因，再提出最小修复。不要先猜答案。\n📷 [图片 token=BprEbINySoyjyqxvaaNcARcyn6g（未能下载，见飞书原文）]\n第四步：验证实现 在本仓库中，可以要求：\n使用 openspec-verify-change 验证 add-dark-mode。对照 proposal、design、tasks、delta specs 和实际实现，运行与改动范围匹配的测试，并列出没有执行的检查及原因。\n还应运行 OpenSpec 校验：\nopenspec validate --all 修改前端时，至少从相关测试开始；如果改动涉及 API 或 SSE，还要同步检查共享契约、后端和前端。不要用“测试应该能过”代替真实的命令结果。\n📷 [图片 token=R7nsbkqcRoQ5mwx0ABDcYacCnwf（未能下载，见飞书原文）]\n第五步：归档变更 只有在任务完成、验证通过，并将 delta specs 同步到主规格后，才归档：\n使用 openspec-archive-change 归档 add-dark-mode，并按仓库规则同步 WIKI、索引和侧栏，最后验证文档构建。\n支持斜杠命令时，也可以使用：\n/opsx:archive add-dark-mode 本仓库规定：创建或归档 change 后，还要使用 wiki-sync Skill 同步 docs/changes/，并运行 npm run docs:build。这一步是本仓库特有的交付要求，不是所有 OpenSpec 项目都一样。\n📷 [图片 token=Ustpb7Fc8oZ8WsxlULucNzx3nUg（未能下载，见飞书原文）]\n7. 一段可以直接使用的完整提示词 把项目文件夹在 Codex 中打开，然后发送：\n请使用 OpenSpec 流程处理下面的需求。 1. 先阅读 AGENTS.md、openspec/specs/、相关实现和测试。 2. 先通过提问澄清目标、边界和验收标准，不要立刻写代码。 3. 使用 openspec-propose 创建一个聚焦的 change。 4. 等我确认方案后，再使用 openspec-apply-change 实现。 5. 实现时先补测试，再写最小代码，并逐项更新 tasks.md。 6. 完成后使用 openspec-verify-change 验证，报告实际运行的命令和结果。 7. 验证通过后提醒我归档；未经确认不要提前归档。 需求：为前端设置页增加深色模式。用户选择需要保留，刷新页面后仍然生效，不影响登录和聊天功能。 这比只写“做一个深色模式”多花一分钟，却能明显减少误解、返工和无效 Token 消耗。\n📷 [图片 token=VWG1bl2jQoI2f6xPKnKc6LN0n7g（未能下载，见飞书原文）]\n8. 在本仓库中练习启动与验证 Agent Py 是一个已经有完整实现的本地优先 AIOps 工作台，不是空白脚手架。开始任何练习前，先执行：\ngit status --short 如果看到已有修改，不要删除、覆盖或顺手格式化这些文件。阅读根目录 AGENTS.md，再查看与你的需求相关的 openspec/specs/、实现和测试。\n📷 [图片 token=BsfubaxrDoVpanxAy2YcMqqKnAc（未能下载，见飞书原文）]\n安装项目依赖后，macOS/Linux 的正式本机启动入口是：\n./scripts/start-local.sh Windows 使用：\nscripts\\start-local.bat 启动器会安装依赖、执行数据库迁移并启动有状态服务，因此不要把它当成无副作用的检查命令。只想验证文档时，应运行：\nnpm run docs:build 仓库的完整检查还包括前端、共享契约、后端和 OpenSpec 校验，应根据实际改动范围选择，不能为了“看起来通过”而跳过类型检查、删除测试或弱化断言。\n📷 [图片 token=XDm2bVlezou3HTxZOnbcMsWhnpc（未能下载，见飞书原文）]\n9. 常见问题 Codex 需要开通 chatgpt 会员吗？ codex有免费的额度，但是要开发项目的话，还是得开通chatgpt会员才行，如果你还没有开通，可以看这个教程开通：2026年最新ChatGPT充值教程，支持国内支付宝和微信充值 GPT 5.6、Plus、Pro、Codex等\nCodex 一上来就开始改代码怎么办？ 明确说“先阅读和说明影响范围，暂时不要修改文件”。需要较大改动时，先完成 OpenSpec proposal，再批准实现。\nOpenSpec 安装成功，但 /opsx:propose 不可用怎么办？ CLI 与 Codex Skill/斜杠命令是不同层。先确认 openspec --version 可用，再检查项目是否安装了 OpenSpec Skills。没有斜杠命令时，直接说“使用 openspec-propose Skill 创建变更”即可。\n一定要安装 Superpowers 吗？ 不需要。它能帮助规范流程，但真正重要的是把澄清、规划、测试、调试和验证这些动作落实。自然语言也能明确要求 Codex 这样工作。\n怎样减少 Token 浪费？ 一次说明目标、背景、限制、验收标准和允许修改的范围；让 Codex 先读取仓库事实；把长期约束写入 AGENTS.md，把一次变更写入 OpenSpec；发现方向错误时立即停止并回到方案，不要在错误实现上连续打补丁。\n📷 [图片 token=LEQzbK5g6oDqp4x0WLRcemKQnLd（未能下载，见飞书原文）]\n结语 OpenSpec 和 Superpowers 的目的都不是让 AI 看起来更“炫”，而是让 AI Coding 更清晰、更稳定、更可控。\nOpenSpec 解决“需求如何沉淀”：它把聊天里的想法整理成提案、设计、任务、规格和验收标准。Superpowers 解决“AI 如何执行”：先理解、再规划、再测试和实现，最后验证。Codex 则把这些信息真正落实到代码、命令和检查结果中。\n一个负责执行，一个负责记录，一个可选地约束工程方法。把三者正确组合起来，AI 才不只是会生成代码的工具，而会成为一个能够参与工程协作的开发伙伴。\n📷 [图片 token=HUaab7K5qousK7xxGbLcbtEWn8c（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/02%EF%BD%9CAI%20Coding%20%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A6%E6%9D%9F/Codex%20%E4%B8%8E%20OpenSpec%20%E5%BF%AB%E9%80%9F%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8/","summary":"面向第一次接触 AI Coding 的同学：从环境准备，到完成一次可检查、可追踪的代码修改。  前言：为什么 AI 会“听不懂”你的需求 你可能已经在抖音、B 站、小红书或各种技术社区里刷到过很多 AI 内容：几句话生成网页、自动修复 Bu","title":"Codex 与 OpenSpec 快速安装使用"},{"content":"design.md 负责回答“怎样实现，以及为什么选择这种实现”。它连接行为契约与代码结构：spec 只要求系统表现正确，design 则说明约束放在哪一层、哪些抽象保持不变、如何测试、接受什么代价。\n面试中真正能体现工程判断的内容，往往不在“我改了某个参数”，而在“为什么参数应该由 Provider 层持有，而不是让每个业务调用方重复理解厂商限制”。这正是 design 应保存的知识。\n📷 [图片 token=UXJgbvOWkohnTcx2QPpcy7hlnGc（未能下载，见飞书原文）]\n推荐组织结构 ## Context ## Goals / Non-Goals ## Decisions ## Risks / Trade-offs ## Migration Plan ## Open Questions 不是每个 change 都必须机械填满所有章节。小型修复可以没有 Migration Plan 和 Open Questions，但 Context、目标边界、关键决策和风险通常不可缺少。\n📷 [图片 token=IITCbCzGyoPYyPxrkaZcQppTn5c（未能下载，见飞书原文）]\nContext：给技术决策补足现场 Context 应解释当前架构、数据流、约束和失败位置。主案例的现场是：文档索引服务把拆分后的完整 chunk 列表一次交给 EmbeddingModel.aembed_documents；默认 OpenAIEmbeddings 的批量值超过厂商单次 10 条限制，因此 11 个以上 chunk 会收到 HTTP 400。\n这段 Context 同时说明业务抽象和外部约束。没有它，读者只看到 chunk_size=10，会误以为这是随意调优；有了它，才能判断问题属于 Provider 适配，而不是文档切分或 Milvus 写入。\n📷 [图片 token=K8vfbjmUJolATPxEcvicIaRYn5j（未能下载，见飞书原文）]\nGoals / Non-Goals：约束设计空间 Goals 描述设计必须达到的技术目标。主案例要求每个真实请求不超过 10 条，索引服务仍可一次提交完整列表，客户端透明分批并保持向量顺序，同时用两层测试覆盖。\nNon-Goals 明确本次不解决什么：不改 chunking 策略、不改 Milvus 写入格式、不改索引任务 API 和前端，也不为所有 Embedding Provider 建动态限流系统。\nNon-Goals 不是“偷懒声明”，而是控制 change 的原子性。如果把动态限流、配置中心和所有供应商适配都塞进一个小修复，设计成本和回归面会突然扩大，反而延迟解决已经可复现的故障。\n📷 [图片 token=JQlBbpeibojDyGxXKTrccjjunbA（未能下载，见飞书原文）]\nDecisions：记录选项、理由和边界 主案例有三项互相关联的决定：\n**在 Provider 构造处设置 chunk_size=10。**批量上限来自模型提供商，属于 Provider 适配责任。放在这里，文档索引、知识检索和未来调用方都自动获得同一兼容行为。\n**保留业务层一次调用完整列表。**索引服务依赖稳定的 EmbeddingModel 抽象，不知道百炼的请求上限。LangChain 客户端负责分批和重组，业务代码不会被厂商细节污染。\n**使用分层测试。**Provider 测试直接验证 11 条输入变成 10+1 请求并保持顺序；文档索引测试验证业务层一次提交 11 个 chunk，最终全部写入。前者锁定适配细节，后者锁定端到端业务结果。\n📷 [图片 token=A1JDbpHGDoo3s6x1j8bcDSDsnhb（未能下载，见飞书原文）]\n好的 Decision 不只是宣布结论，还要说明责任归属和为什么不选择另一层。这样未来重构时，维护者知道哪些边界是有意设计，哪些只是当时实现。\n📷 [图片 token=Pgr7bzJWRosQebxQtzocApcOnBf（未能下载，见飞书原文）]\nRisks / Trade-offs：承认方案代价 任何技术方案都有成本。主案例承认大文档会产生更多 HTTP 请求，索引耗时可能增加；但索引本来就是后台任务，正确性和兼容性优先。它也承认厂商未来放宽上限时固定 10 会牺牲吞吐，后续可以配置化。\n这种写法比“方案无风险”更可信。Trade-off 的意义不是制造恐慌，而是告诉评审者：我们知道牺牲了什么，为什么目前可以接受，以及未来什么条件会触发重新设计。\n📷 [图片 token=VXefbMoPOoCn53xQhn1cSTU0nxf（未能下载，见飞书原文）]\nMigration Plan 什么时候需要 涉及数据库 schema、持久化格式、共享 API/SSE 契约、配置结构或外部服务切换时，应写迁移步骤、兼容窗口、回滚方案和数据修复。OncallAgent 的数据库变化必须增加 Alembic migration，不能直接修改已发布 migration 伪装当前状态。\n主案例只调整 Provider 客户端构造，不改变存量数据和协议，所以不需要复杂迁移。这也是设计判断：没有迁移需求时不要为了模板完整虚构一套。\n📷 [图片 token=RU7MbQQLRozMCXxVnIvcMW59n8g（未能下载，见飞书原文）]\nOpen Questions 怎样使用 Open Questions 保存尚未决定、但会影响方案的问题。它们应在进入相关实现前关闭，或者明确标注不阻塞当前范围。不能把关键安全、权限和数据一致性问题长期留在“以后再说”。\n📷 [图片 token=NByGbKx9doCTFoxnaTpcyQJon2g（未能下载，见飞书原文）]\n怎样审查 design **是否与 spec 分工清楚。**design 可以写 LangChain、Provider、常量和测试位置；spec 仍只写可观察行为。\n**决策是否符合仓库边界。**OncallAgent 要求 FastAPI 路由保持薄层、业务逻辑进入服务或 repository、项目配置只来自本地 JSON、用户资源显式 owner/tenant scope、MCP 真实调用并审计。设计不能绕过这些不变量。\n**是否说明失败路径。**外部模型、Milvus、MCP、CLS 和 Alertmanager 都可能失败。设计必须返回真实失败并脱敏，不能编造成功状态。\n**测试是否验证了正确层次。**配置断言、适配行为、业务结果和端到端交互各有边界，不要只写一个宽泛测试名称。\n📷 [图片 token=LhFBb2jKzoNKxtxRShacpp72nBb（未能下载，见飞书原文）]\n面试表达 design 不是代码清单，而是决策记录。Embedding 案例里，我把厂商 10 条限制收敛到 Provider 层，保留业务层的完整列表抽象，并用 Provider 10+1 拆批测试和索引 11 chunk 落库测试分别锁定适配行为与业务结果。这样不仅修了 HTTP 400，也避免厂商约束扩散到业务代码。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/design.md%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"design.md  负责回答“怎样实现，以及为什么选择这种实现”。它连接行为契约与代码结构：spec 只要求系统表现正确，design 则说明约束放在哪一层、哪些抽象保持不变、如何测试、接受什么代价。 面试中真正能体现工程判断的内容，往往","title":"design.md文件作用介绍"},{"content":"OpenSpec 最重要的工程价值，不是生成 Markdown，而是让一次变化能够从“为什么做”一路追到“哪段代码兑现、哪组测试证明、当前规格怎样更新、历史记录在哪里”。如果任何一层断开，文档数量再多也只是形式完整。\n📷 [图片 token=FrvobgHLsoxDK7xKK0Dc591Rnod（未能下载，见飞书原文）]\n追踪链到底在追什么 问题与边界 → 可观察行为 → 技术决策 → 实施任务 → 生产代码 → 测试与运行证据 → 当前主规格 → 归档决策历史 → 可浏览 WIKI 这里追踪的是语义，不是简单的文件链接。proposal 中的一条目标可能对应多个 Scenario；一个 Scenario 可能需要契约测试、服务测试和前端测试共同证明；一项 design 决策也可能被多个模块复用。因此追踪关系通常是一对多或多对多，不应强行要求“一条需求等于一个函数”。\n📷 [图片 token=Ckseblw7qokwVKxyvanc0MWsnye（未能下载，见飞书原文）]\n用 Embedding 批量限制案例建立矩阵 案例目录是 openspec/changes/archive/2026-07-11-limit-qwen-embedding-batch-size/。它的语义链可以整理为：\n层次 核心问题 仓库证据 Proposal 为什么要改，边界在哪里 proposal.md：百炼单次最多 10 条，范围限定为 Provider 与测试 Delta spec 系统必须表现成什么样 specs/qwen-openai-provider/spec.md：1 条 Requirement、3 个 Scenario Design 为什么由这一层实现 design.md：在 Provider 设置 chunk_size=10，业务层保持完整列表调用 Tasks 交付要产生哪些证据 tasks.md：实现、客户端测试、索引回归和质量门禁 实现 技术决策落在哪里 apps/backend/src/super_ai/llm/provider.py 边界测试 Provider 是否真的按 10 条分批并保序 apps/backend/tests/test_llm_provider.py 业务回归 大文档是否仍完整索引 apps/backend/tests/test_document_indexing.py Main spec 当前系统正式承诺什么 openspec/specs/qwen-openai-provider/spec.md Archive 这次为什么这样决策 带日期的完整归档 change WIKI 如何让历史便于浏览 docs/changes/archive/.../index.md 通过 @include 引用原文件 第一段：从 proposal 锁定问题与范围 proposal 说明：较大文档会产生 11 个以上 chunk，默认 Embedding 客户端把它们作为一个请求发送，超过百炼 text-embedding-v4 的 10 条上限并得到 HTTP 400。它同时把影响范围限定为 Qwen Provider 构造、Provider 测试和文档索引回归。\n📷 [图片 token=CjMObidddoEe8Mx6GeqcGHsJneb（未能下载，见飞书原文）]\n这个边界很重要。没有 proposal，开发者可能去修改 chunking 策略、给索引服务加入厂商判断、调整 Milvus schema，甚至改变前端错误提示。proposal 中的非影响范围明确排除了 HTTP、SSE、Milvus schema 和前端，从一开始就减少无关改动。\n追踪审查的第一个问题是：后续 spec、design 和代码有没有超出这一范围。如果出现数据库迁移或前端改动，就必须解释它为何必要，并回到 proposal 更新影响分析。\n📷 [图片 token=IoLvbqn1PoA9KMxEJMlcf6uCnvc（未能下载，见飞书原文）]\n第二段：把目标改写为可验收 Scenario delta spec 没有写“修复 Embedding 报错”这种不可验收描述，而是拆成三个场景：\nScenario 需要证明的行为 主要证据 输入不超过 10 条 单个请求仍符合上限 Provider 客户端配置与调用测试 输入超过 10 条 拆成每批最多 10 条，完整返回且顺序一致 11 条输入形成 10+1 请求，输出仍按原序 文档超过 10 个 chunk 索引任务成功并写入全部 chunk 索引服务回归测试 📷 [图片 token=KYR2bQsUroEPpZxqF0lcn5xznud（未能下载，见飞书原文）]\n这里能看到“一个测试无法证明全部设计”。Provider 测试负责证明真实客户端配置、10+1 分批和顺序保持；索引回归使用 FakeEmbeddingModel 与 FakeVectorStore，证明业务层仍提交 11 个 chunk 并完整写入。后者没有真正执行 10+1 请求，前者也没有覆盖整个索引编排，两项证据组合后才与三个 Scenario 对齐。\n📷 [图片 token=I8CIbW0kiof2hVxIwUlcnQ2Rnwb（未能下载，见飞书原文）]\n第三段：让 design 解释责任归属 design 的关键决策是把提供商上限放在 OpenAIEmbeddings Provider 配置，而不是文档索引服务。其理由可以从三个方向追踪：\n**抽象边界。**批量上限来自模型提供商，应由 Provider 隐藏；业务服务只关心“为这组文本生成向量”。\n**复用范围。**未来其他调用方使用同一 Embedding Provider 时，无须重复实现拆批。\n**变化成本。**提供商未来调整上限时，只修改 Provider 配置或配置项，而不是修改所有业务调用路径。\n📷 [图片 token=MbNwbnyA3olsGdxEq8gcCadrnkh（未能下载，见飞书原文）]\n代码中的 QWEN_EMBEDDING_BATCH_SIZE = 10 和 OpenAIEmbeddings(chunk_size=...) 正是这项决策的落点。如果代码改成索引服务手写分批循环，即使测试通过，也会违反 design 的责任边界，verify 应把它识别为一致性问题。\n📷 [图片 token=QMxqbIQCJoLnkhxFhaxcfja5nyb（未能下载，见飞书原文）]\n第四段：tasks 把行为与证据排成执行顺序 本案例的 tasks 没有按“后端开发、测试、结束”粗略分组，而是依次要求：设置默认 Qwen Embedding 的单批上限；验证真实客户端配置与超过上限的行为；验证 11 个以上 chunk 的索引回归；运行 Ruff、Pyright、Pytest 与 OpenSpec 校验。\n📷 [图片 token=QDCNbE2bgowRH3x3cskc2l5OnSf（未能下载，见飞书原文）]\n这种拆法让每个 checkbox 都有对象和证据。apply 可以按顺序推进，verify 也能反查“任务是否只勾选但没有对应实现或输出”。不过复选框仍不是证据本身，历史 tasks 中的 [x] 不能代替当前会话的测试结果。\n📷 [图片 token=ZMRebzxUQoOslQxH4cHcFX7Nndb（未能下载，见飞书原文）]\n第五段：从代码反向追踪到需求 正向追踪用于交付，反向追踪用于维护。假设未来 test_default_embedding_model_preserves_raw_qwen_inputs_and_batches_by_ten 失败，可以沿下面路径反查：\n失败测试 → provider.py 的 chunk_size 配置 → design 中的 Provider 责任归属 → delta spec 的大批量拆分 Scenario → proposal 中的百炼 10 条限制与范围 📷 [图片 token=AE7nbg7w7ou9E7xYDt3cqXNwncc（未能下载，见飞书原文）]\n这样维护者不只知道“哪个断言坏了”，还能知道这个断言保护的是哪项系统承诺，以及为什么不能随意删除。反向链也能判断需求是否已经变化：如果提供商官方上限改变，应创建新 change 更新当前规格，而不是简单删掉测试。\nMain spec、archive 与 WIKI 各自保存什么 Main spec 保存当前事实。openspec/specs/qwen-openai-provider/spec.md 已包含批量兼容 Requirement 和三个 Scenario。日常开发需要确认系统现在承诺什么，应先看这里。\n**Archive 保存变化过程。**归档目录保留当时的 proposal、design、tasks、delta spec 和元数据。需要回答“为什么选择 Provider 层”“当时排除了哪些范围”，应查 archive。\nWIKI 提供浏览入口。docs/changes/archive/2026-07-11-limit-qwen-embedding-batch-size/index.md 不复制正文，而是通过 @include 引用归档文件。它改善导航，但不成为第二套事实来源。\n📷 [图片 token=JLHDb5Bm3ovVaax6XVzcNCIinng（未能下载，见飞书原文）]\nOpenSpec 追踪与 Git 追踪并不重复 该案例的 archive、实现、测试和 main spec 可以在 Git 历史中找到，但相关提交同时包含很多其他文件。Git 能精确显示文本差异，却不天然提供一个聚焦于本变更的语义边界。OpenSpec change 把这次需求的意图、行为和取舍单独组织起来，二者互补。\n也不能反过来夸大 OpenSpec：当前仓库没有把 Requirement ID 自动写进代码和测试，也没有自动生成形式化 trace matrix。现有追踪依赖稳定的 capability/Requirement 名称、文件路径、测试名称，以及开发者或 Agent 的语义核对。它是可审查的工程追踪，不是数学证明。\n📷 [图片 token=XeWNbqPevoYayDxjS0RcEK4cnbb（未能下载，见飞书原文）]\n审查一条追踪链的实用方法 从 proposal 选一条目标，确认 delta spec 中有对应 Requirement 或 Scenario。\n确认 design 解释了实现责任、关键取舍和非目标。\n从 Scenario 反推出 tasks，检查实现、测试和验证是否都被安排。\n在生产代码中找到 design 决策的落点，而不只搜索相同关键词。\n检查测试是否覆盖条件、结果和重要失败边界，并说明 fake 与真实集成的差别。\n确认 delta 已正确进入 main spec，未覆盖无关 Requirement。\n确认 archive 保留完整产物，WIKI include 指向归档后的真实路径。\n📷 [图片 token=AGbObyD0YoKHNXx2WaHcqp1bnef（未能下载，见飞书原文）]\n一段可直接使用的技术表达 我不会把 OpenSpec 理解成模板生成器，而是把它当作需求到证据的追踪链。在 Embedding 批量限制案例里，proposal 先锁定百炼 10 条上限和 Provider 层范围；delta spec 把小批量、大批量保序和完整索引写成三个 Scenario；design 决定由 Provider 配置 chunk_size；tasks 安排实现、边界测试、业务回归和质量门禁；代码与两层测试分别证明拆批和索引完整性；最后 delta 合入 main spec，完整 change 归档，WIKI 通过 include 提供浏览入口。这样任何人都能从当前行为反查当时的决策，也能从失败测试定位它保护的业务承诺。\n📷 [图片 token=DmHfboHlwoVRv6xbpv7c2eJ3ndm（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E5%BC%80%E5%8F%91%E9%97%AD%E7%8E%AF/OpenSpec%E8%BF%BD%E8%B8%AA%E9%93%BE%E8%B7%AF%E4%BB%8B%E7%BB%8D/","summary":"OpenSpec 最重要的工程价值，不是生成 Markdown，而是让一次变化能够从“为什么做”一路追到“哪段代码兑现、哪组测试证明、当前规格怎样更新、历史记录在哪里”。如果任何一层断开，文档数量再多也只是形式完整。 \u0026lt;image toke","title":"OpenSpec追踪链路介绍"},{"content":"2026-07-11-show-retrieval-stage-ranks 是一个适合解释纵向交付的归档 change。它没有新建一套检索算法，而是把后端已经掌握的向量、BM25 和 rerank 三阶段排名，稳定地穿过共享契约、聊天 SSE、持久化引用、AIOps、Vue 摘要与详情，并用多层测试锁定语义。\n这个案例的价值在于目标单一、边界清楚，却必须协调多个工程层。它能说明 OpenSpec 为什么不只适合“大功能”，也适合控制一次会跨越协议和界面的可观察行为变化。\n📷 [图片 token=DhXybFeS8o7v5NxT9sxcE3cSnVd（未能下载，见飞书原文）]\n问题不是没有分数，而是信息在链路中丢失 变更前，混合检索内部已经知道向量召回和 BM25 召回的源列表名次，rerank 结果也有最终顺序，后端还保存向量、BM25、RRF 和 rerank 分数。但聊天引用摘要只展示类似“精排 99%”的结果。\n这会造成两个问题。第一，使用者无法知道一条引用是同时被语义召回和关键词召回命中，还是只来自其中一路。第二，排查检索质量时只能看到最终精排分，无法观察某条文档在三个阶段的位置变化。\n因此真正需求不是“前端加三个标签”，而是建立一条端到端的检索阶段轨迹：后端生成正确语义，共享契约允许表达空值和兼容旧消息，SSE 与持久化不丢字段，前端统一展示。\n📷 [图片 token=LbyybrRfYoLgWCx9Pyfc9jYan3g（未能下载，见飞书原文）]\nproposal 如何控制改动范围 proposal 将变化限定为三项：\n检索命中和引用增加 vectorRank、bm25Rank、rerankRank。\n聊天 SSE、持久化引用和 AIOps 引用保持相同三阶段语义。\n前端摘要和详情展示名次、分数以及“未召回”状态。\nproposal 同时明确不修改数据库 schema、Milvus collection、召回/RRF/rerank 算法和模型配置。这些 Non-Goals 防止一个展示可观测性需求演变成检索算法重构，也避免为了新增三个字段引入不必要的数据迁移。\n📷 [图片 token=X7YVbORntoE4rUxJcI2cdEdTnsD（未能下载，见飞书原文）]\n为什么需要三份 delta spec 一次纵向变化会同时改变多个长期能力，因此该 change 修改三个 capability：\nCapability 负责的行为边界 关键要求 knowledge-retrieval-tool 检索领域语义 保留三阶段一基排名；单路缺失为空；rerankRank 表示最终输出位置 api-and-sse-contracts 跨进程传输语义 HTTP/SSE 引用支持排名字段，并允许未召回的粗排字段为空 knowledge-answer-citation-view 可见交互语义 摘要和详情展示阶段轨迹；未命中显示“未召回”；窄视口不得水平溢出 如果只写前端 spec，就无法约束后端字段来源；只写后端 spec，又无法约束 SSE 兼容和用户可见状态。三个 capability 不是重复描述，而是分别保护领域、协议和界面边界。\n📷 [图片 token=BsYebQMKYouTPrx8jTHc99M7npe（未能下载，见飞书原文）]\n三个关键 design 决策 **排名从 1 开始。**向量、BM25 和 rerank 使用一基序号，与界面中的“第 1 名”一致，契约还通过 minimum: 1 排除 0 和负数。\n**未召回必须为空，不能伪造。**关键词独占候选的 vectorRank/vectorScore 为 null，向量独占候选的 bm25Rank/bm25Score 为 null。0 分或第 0 名会把“没有进入该召回列表”误写成“进入了但表现很差”。\n📷 [图片 token=IPcebEZhvoHE2JxhI75chlmcn4e（未能下载，见飞书原文）]\n**最终 rerank 名次按输出位置产生。**实现遍历 rerank 返回列表时使用 enumerate(..., start=1)，不把 provider 的输入 index 当作最终名次。因为 rerank 的目的正是改变候选顺序，输入位置不能代表输出排名。\n📷 [图片 token=Dc1cb0wfMo8m2ux1rbPcnJujnx2（未能下载，见飞书原文）]\n展示规则也在 design 中统一：向量相似度与 rerank 相关度显示百分比，BM25 保留三位小数，详情继续展示 RRF 融合分。旧引用缺少新增字段，因此 SSE/历史引用字段保持兼容，而不是强迫已有消息补数据。\n📷 [图片 token=Q84BbSK5coCDs7xpFelcOJ8cnxf（未能下载，见飞书原文）]\n共享契约先定义跨层共同语言 OncallAgent 规定 packages/api-contracts 是 HTTP、OpenAPI 和 SSE 的唯一事实来源。这个 change 没有在 Vue 和 Python 中各自临时发明字段，而是先把语义固化在共享契约：\npackages/api-contracts/src/retrieval.ts：检索 Hit/Citation 的 vectorRank 与 bm25Rank 是必有键但允许 null，rerankRank 必填。\npackages/api-contracts/src/sse.ts：reference.source 中三类 rank 保持可选，以兼容旧的流式引用和历史消息。\npackages/api-contracts/src/openapi.ts：同步字段、nullability 和一基排名的最小值约束。\n📷 [图片 token=JGJFbqhTgopANLxYKd5cwe7pnxg（未能下载，见飞书原文）]\n“retrieval 对象允许 null”与“SSE 字段可选”是两种不同语义。前者表示当前对象明确知道某一路没有命中；后者允许旧数据根本没有这些新字段。把两者都笼统说成“可选”会丢失兼容设计。\n📷 [图片 token=V9Ddb27GJoVmBGxCfNLcY3VUnQg（未能下载，见飞书原文）]\n后端怎样传递而不改排序算法 apps/backend/src/super_ai/retrieval/hybrid.py 已经能够从 RRF 候选中得到 vector_rank 和 bm25_rank。这一基础不是本 change 新发明的，也没有在该变更中修改检索算法。\n变更的主要工作从 apps/backend/src/super_ai/retrieval/tool.py 开始：把源排名保留到 fused candidate、最终 hit 和 citation；遍历 rerank 输出时生成 rerank_rank；对外 payload 使用共享契约约定的 camelCase 字段。\n📷 [图片 token=MueFb9oYtorkGmxSjb1cRui2nBh（未能下载，见飞书原文）]\napps/backend/src/super_ai/chat/streaming.py 从工具 citation 读取排名，并把它们继续放入引用 payload 与 SSE。apps/backend/src/super_ai/aiops/diagnostics.py 对 SOP hit、citation 和 AIOps reference.source 做同样传递。这样聊天与 AIOps 不会出现两套引用语义。\n📷 [图片 token=C203bHShNouqRAxCoV2cbnKYnLe（未能下载，见飞书原文）]\n已有 vector/BM25 源排名 → fused candidate → rerank 最终位置 → KnowledgeRetrievalHit / Citation → chat reference / AIOps reference → SSE 与持久化 metadata 前端怎样保证摘要与详情一致 apps/frontend/src/chat/retrievalPresentation.ts 集中处理百分比、BM25 三位小数和“未召回”文案。格式规则没有散落在多个组件里。\napps/frontend/src/components/RetrievalStageTrace.vue 封装三阶段展示，并提供 ARIA 描述、自动换行和窄屏样式。ChatTranscript.vue 在引用摘要中复用它，ChatCitationDetail.vue 在详情中复用同一组件并保留 RRF 融合分。\n📷 [图片 token=R6Z5b16nJoTvaKx7uWEcZgCPnvG（未能下载，见飞书原文）]\n这种设计避免摘要说“精排”，详情却使用另一套术语，也减少后续格式调整时的重复修改。实现中存在 flex-wrap 与窄屏媒体查询，但当前组件自动化测试没有设置真实 viewport 或浏览器截图，所以不能把它描述成已经完成视觉验收。\n📷 [图片 token=JMxcblbmCoXKKkx0gfzc2TLDnMh（未能下载，见飞书原文）]\n测试如何按风险分层 测试位置 主要证明什么 test_knowledge_retrieval_tool.py 双路命中、单路缺失为 null、rerank 改序后的最终名次和 payload test_hybrid_retrieval.py RRF 候选保留向量与 BM25 源排名；这是既有基础测试 api-contracts.test.ts 共享检索对象与引用包含三类排名字段 chatComponents.test.ts 摘要和详情格式，以及“向量未召回”等用户可见状态 📷 [图片 token=PnytbOnopoTrH8xNscwcEcAAnCb（未能下载，见飞书原文）]\n本次为案例核对，在当前工作树定向运行了四组测试：\ncd apps/backend uv run python -m pytest tests/test_knowledge_retrieval_tool.py -q # 14 passed uv run python -m pytest tests/test_hybrid_retrieval.py -q # 6 passed cd ../.. npm --workspace packages/api-contracts test -- --run tests/api-contracts.test.ts # 24 passed npm --workspace apps/frontend test -- --run tests/chatComponents.test.ts # 6 passed 四组共 50 项通过。这是定向证据，不代表 Ruff、Pyright、前后端全量测试、OpenSpec validate、docs build、浏览器窄视口验收或外部 Milvus/LLM 真链路都已经在本轮验证。AIOps 排名传播能在实现中找到，但该 change 没有新增独立的 AIOps 专项断言，也不应夸大。\n📷 [图片 token=M2k6bDdWHoaNuIxalrMcIhL8nzf（未能下载，见飞书原文）]\n从 delta 到 main specs，再到 archive 与 WIKI 三条 Requirement 已分别出现在当前主规格：\nopenspec/specs/knowledge-retrieval-tool/spec.md\nopenspec/specs/api-and-sse-contracts/spec.md\nopenspec/specs/knowledge-answer-citation-view/spec.md\n完整 change 位于 openspec/changes/archive/2026-07-11-show-retrieval-stage-ranks/。仓库 WIKI 页面 docs/changes/archive/2026-07-11-show-retrieval-stage-ranks/index.md 标记为 archived，并通过 @include 引用 proposal、design、tasks 和三份 delta spec。\n📷 [图片 token=GFrEbEWdbo5PL2xy15wcsPxundf（未能下载，见飞书原文）]\n这个终态可以证明主规格、归档与 WIKI 当前存在并互相对应，但不能反推归档当天具体执行过哪些 CLI 命令，也不能只凭历史 tasks 的 [x] 宣称当时的全量检查和真实前端检索已经被本轮复核。\n📷 [图片 token=XLkCbu3l3oafWlxrvEMcKaLCndc（未能下载，见飞书原文）]\n这个案例怎样讲得专业 我做过一次三阶段检索排名可见化的纵向变更。问题不是后端没有分数，而是向量、BM25 和 rerank 的排名没有穿过引用链路。先用三个 delta capability 分别约束检索领域、HTTP/SSE 契约和 Vue 展示；设计上一基排名，未召回用 null，最终 rerankRank 按输出位置生成，并保留旧消息兼容。实施时先更新共享契约，再把字段穿过检索工具、聊天和 AIOps，前端用统一组件展示摘要与详情。测试覆盖双路命中、单路缺失、rerank 改序、契约对象和用户可见文案，最后同步三份主规格、归档 change 并生成 WIKI。整个过程没有修改排序算法、数据库或 Milvus schema，范围始终受 proposal 和 Non-Goals 约束。\n这段表达包含问题、边界、规格拆分、关键设计、实施顺序、测试证据和非目标。它比“我给页面加了三个字段”更能说明端到端交付能力，也避免把现有 RRF 能力误说成这次变更新增。\n📷 [图片 token=ZfbAbWwhFoQj3CxKONNckYIKn0e（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E7%9C%9F%E5%AE%9E%E6%A1%88%E4%BE%8B/show-retrieval-stage-ranks%E8%B7%A8%E5%B1%82%E6%A1%88%E4%BE%8B%E4%BB%8B%E7%BB%8D/","summary":"2026-07-11-show-retrieval-stage-ranks  是一个适合解释纵向交付的归档 change。它没有新建一套检索算法，而是把后端已经掌握的向量、BM25 和 rerank 三阶段排名，稳定地穿过共享契约、聊天 S","title":"show-retrieval-stage-ranks跨层案例介绍"},{"content":"上一章你已经知道了，一个工具 = 函数本体 + 名称 + 描述 + 参数定义，大模型靠工具描述来判断要不要调这个工具。\n但这里有个关键问题还没解决：大模型做出「要调这个工具」的决策之后，它怎么告诉 Agent 去执行？\n大模型只会\u0026quot;说话\u0026quot;，它的输出永远是文字。但 Agent 要调用工具，必须知道：调哪个工具？传什么参数？格式是什么？这中间的信息交换，靠什么来保证准确？\n这就是 Function Calling 要解决的核心问题。在讲它是什么之前，我们先来看看没有它的时候，开发者有多惨。\n没有 Function Calling 之前：靠自然语言\u0026quot;猜\u0026quot; 在 Function Calling 出现之前，开发者只能把工具的描述和调用规范，全部用**自然语言写进 System Prompt（系统提示词）**里。\n比如你想让 AI 能调用一个查天气的工具，System Prompt 里就得这样写：\n你是一个智能助手，你可以使用以下工具： 工具名：check_weather 功能：查询某个城市的天气 参数： - city：城市名，中文，比如\u0026#34;上海\u0026#34; - date：日期，格式必须是 YYYY-MM-DD，比如\u0026#34;2026-03-19\u0026#34; - date 是可选的，不填默认查今天 当用户需要查天气时，你要调用这个工具，告诉我工具名和参数。 当用户需要查天气时，你要调用这个工具，告诉我工具名和参数。\n听起来还好？但问题是，大模型是个自由发挥的选手，你约束不住它。你让它查「上海明天的天气」，它可能给你返回：\n散文式回复：\u0026ldquo;好的，我来帮你查询上海明天的天气，需要使用 check_weather 工具，城市是上海，时间是明天～\u0026rdquo;\n参数顺序颠倒：{\u0026quot;date\u0026quot;: \u0026quot;明天\u0026quot;, \u0026quot;city\u0026quot;: \u0026quot;上海\u0026quot;}（\u0026ldquo;明天\u0026quot;完全不是 YYYY-MM-DD 格式，直接传进工具就报错）\n直接编造天气：不调工具，根据训练数据给你生成一个\u0026quot;明天上海天气预报\u0026rdquo;\nJSON 里混自然语言：调用工具: check_weather, 参数 city=上海, date=2026-03-20\n完全无视要求：给你一段介绍上海气候特点的科普文章\n这就是传统 System Prompt 模式的本质问题，全靠 AI 猜：猜要不要调工具、猜参数格式、猜回复结构。猜对了正常运行，猜错了整个流程就崩了。\n而开发者呢？就得写一大堆正则表达式和解析代码，尝试从 AI 五花八门的回复里\u0026quot;扒\u0026quot;出工具名和参数。工具越多，解析代码比业务代码还长。而且换个模型，还得重写一遍。这种开发体验，完全是噩梦级别的。\nFunction Calling 的出现：告别猜谜，统一规范 这就是 Function Calling 诞生的背景。\nFunction Calling 的本质，是一套大模型和 Agent 之间的标准化工具调用协议，我们不再用自然语言告诉 AI「你可以用这个工具」，而是用一套固定的 JSON 格式把工具定义清楚；大模型也不再自由发挥，而是严格按固定的 JSON 结构返回调用指令。\n📷 [图片 token=XLjoboYz6ouOuKxAFtpcno6fnlf（未能下载，见飞书原文）]\n整个过程变成了这样：你给我标准格式的工具说明书，我给你标准格式的调用指令，双向约定，零歧义，零猜测。\n这里很多同学会有个疑问：Tool 和 Function Call 有什么关系？简单来说，Tool 是「能力实体」，Function Call 是「调用机制」，大模型通过 Function Call 这个协议，去调用 Tool 这个具体的功能。解决的是完全不同层面的问题：\n工具（Tool）= 能力层，解决「有什么」：一个工具需要哪些要素才算完整、才能让大模型认识和使用它，函数本体是真正干活的，名称让大模型能精准识别，描述告诉大模型什么时候该用它，参数定义告诉大模型怎么构造调用参数。这是在定义一个能力本身。\nFunction Calling = 协议层，解决「怎么传」：工具写好之后，大模型和 Agent 之间要用什么格式来交换信息，大模型怎么告诉 Agent 要调哪个工具、传什么参数；Agent 执行完工具后怎么把结果回传给大模型。这是一套标准化通信规范。\n打个比方：工具是外卖骑手（有姓名、有技能、能送餐），Function Calling 是美团的派单系统（规定了订单怎么下达、骑手怎么接单、送达后怎么确认）。骑手和派单系统缺一不可，但它们解决的是完全不同层面的两件事，骑手是「能力」，派单系统是「通信规范」。\n所以Tool章讲的「工具四要素」，是从概念层告诉你一个工具应该有什么；这章讲的 Function Calling，是从协议层告诉你这些要素以什么标准化格式在大模型和 Agent 之间流转。\n两者的协作流程，以「查询北京明天天气」为例：\n用户提问：「北京明天天气怎么样？」\n大模型分析：需要调用天气 Tool，生成 Function Call 格式的指令。\n系统解析指令：调用 get_weather(\u0026quot;北京\u0026quot;, \u0026quot;2026-03-21\u0026quot;) 这个 Tool。\nTool 执行并返回结果：「北京明天晴，10~20℃」。\n大模型把结果整理成自然语言，回复给用户。\nFunction Calling 三部分核心结构 接下来，我们用天气查询的场景，把 Function Calling 完整的三部分结构拆解清楚，每一行都讲明白作用。\n📷 [图片 token=RqQMbIK21oq4b5x2IxlckTz3nlb（未能下载，见飞书原文）]\n第一部分：工具定义（Tool Definition），给大模型的「工具说明书」 这一步是我们开发者要写的，相当于给大模型一份严格规范的工具说明书，大模型会完全按照这份说明书，判断要不要调用工具、怎么调用工具。\n{ \u0026#34;name\u0026#34;: \u0026#34;check_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;获取指定城市指定日期的天气情况，包括温度区间、晴雨状况、风力风向\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;需要查询天气的城市中文名称，示例：北京、上海、广州\u0026#34; }, \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;YYYY-MM-DD\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;需要查询的日期，格式为年-月-日，示例：2026-03-19，不填默认查询当天\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;] } } 这里每个字段都有明确作用：\nname：工具的唯一标识，大模型调用时必须和这个名字完全一致\ndescription：工具功能说明，是大模型判断「要不要调这个工具」的核心依据，写得越精准，大模型做对决策的概率越高\nparameters：告诉大模型这个工具需要哪些参数、什么类型、什么格式，大模型会严格按照这里的约束来生成参数值\nrequired：必填参数清单，如果用户没提供必填信息，大模型会主动去问用户，而不是瞎猜一个\n对比之前 System Prompt 里用自然语言描述的\u0026quot;松散约定\u0026quot;，这里的 JSON 格式是严格的机器可读规范，大模型在服务端就会做格式校验，不合规的参数直接拒绝，从源头杜绝了参数乱传的问题。\n第二部分：AI 调用格式（Function Call），大模型给 Agent 的「标准化执行指令」 当大模型判断需要调用工具时，会严格按照固定格式返回 JSON，不再有任何自由发挥的自然语言。我们的 Agent 直接解析这个结构，拿到工具名和参数，不用做任何额外的猜解处理。\n{ \u0026#34;function_call\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;check_weather\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;, \u0026#34;date\u0026#34;: \u0026#34;2026-03-19\u0026#34; } } } 注意 function_call 这个字段，它是一个固定标识，告诉 Agent：这不是给用户看的回复，这是一条工具调用指令，你去执行它。Agent 看到这个字段，就知道要去工具注册表里找 check_weather 这个函数，把 city 和 date 作为参数传进去。\n你会发现，这里的 date 字段大模型自动把「明天」转换成了 2026-03-19，完全符合我们在工具定义里约定的 YYYY-MM-DD 格式。这就是标准化规范的威力，大模型知道格式要求，会自己做转换，不再把模糊的「明天」直接丢给工具。\n第三部分：工具返回结果（Tool Response），Agent 把结果回传给大模型 Agent 拿到大模型的调用指令后，去执行对应的工具，然后把工具的执行结果，按规范格式回传给大模型。大模型会基于这个结果，决定下一步是继续调用其他工具，还是直接给用户返回最终答案。\n{ \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;, \u0026#34;date\u0026#34;: \u0026#34;2026-03-19\u0026#34;, \u0026#34;temperature\u0026#34;: \u0026#34;16-22℃\u0026#34;, \u0026#34;condition\u0026#34;: \u0026#34;多云转晴\u0026#34;, \u0026#34;wind\u0026#34;: \u0026#34;3级西北风\u0026#34; } 这一步是整个循环的闭环，大模型不是一次性就完成任务的，它需要看到工具的执行结果之后，才能做出下一步决策。比如天气查到了，它会把这个结果整理成自然语言回复用户；如果工具报错了，它会决定换个方式或者告知用户。\n对应 Agent 执行流程 把这三部分放回之前讲过的 Agent 执行循环，对应关系就清晰了。\n先回忆一下 tools 文章里的那 7 步执行流程：\n第 1 步：Agent 把「用户需求 + 工具清单」打包，发给大模型 第 2 步：大模型做决策，返回调用指令 第 3 步：Agent 执行指令，调用工具函数 第 4 步：Agent 把工具执行结果回传给大模型 第 5 步：大模型根据结果，决定下一步 第 6 步：循环执行，直到任务完成 第 7 步：Agent 把最终结果反馈给用户 Function Calling 规范的是其中有「数据交换」的三步：\n第 1 步 → 工具定义（Tool Definition）\nAgent 要把工具清单传给大模型，这份清单就是用 Function Calling 规定的 JSON 格式写的，每个工具都有标准化的 name、description、parameters 字段。大模型收到的不是一段随意的文字描述，而是严格规范的结构化数据。\n第 2 步 → AI 调用格式（Function Call）\n大模型做出「调用工具」的决策后，它返回给 Agent 的不是一句自然语言（比如「你去帮我查一下天气」），而是严格的 JSON 格式指令，function_call.name 告诉 Agent 调哪个工具，function_call.parameters 告诉 Agent 传什么参数。Agent 直接解析，不用猜。\n第 4 步 → 工具返回结果（Tool Response）\nAgent 执行完工具（第 3 步），拿到结果，按规范格式打包回传给大模型。大模型收到这个结果，才能做出下一步决策，是继续调工具，还是直接回答用户。\n第 3 步不在 Function Calling 的管辖范围内，那一步是 Agent 实际执行函数，是真正「干活」的部分，不涉及大模型和 Agent 之间的通信格式。\n用一张图把对应关系标出来：\n📷 [图片 token=UrvUbRkpkoUg3cxN9ZIci5rznYb（未能下载，见飞书原文）]\nFunction Calling 把第 1、2、4 步的数据格式全部标准化了，这三步恰好是大模型和 Agent 之间所有的「信息交换点」。有了这套规范，双方说的是同一种语言，不再靠猜。\n解析工具名：从 function_call.name 拿到 \u0026quot;check_weather\u0026quot;\n查找对应函数：在工具注册表里，找到 check_weather 对应的实际函数\n传入参数执行：把 function_call.parameters 里的 city 和 date 原样传进函数\n拿到结果回传：函数执行完毕，把返回的天气数据回传给大模型\n整个过程干净利落，不需要任何猜测和解析，因为 Function Calling 已经保证了调用指令的格式是固定的、可预期的。Agent 只需要按格式拆开、对号入座、执行、回传，就完成了自己在这一轮循环中的全部工作。\n一张表看懂：System Prompt 传统模式 vs Function Calling 标准化模式 lbcwsV 对比维度 System Prompt 传统模式 Function Calling 标准化模式 工具描述方式 自然语言随意写，无统一规范 固定 JSON 格式，强制规范 name/description/parameters 必填项 调用返回格式 完全靠 AI 自由发挥，五花八门，无固定结构 严格固定 JSON 结构，字段统一，可直接解析 参数错误率 极高，经常出现参数顺序错、格式错、模糊值 极低，大模型严格按参数规范生成，格式和必填项有服务端校验 开发成本 极高，需要写大量解析、纠错、重试逻辑 极低，直接解析固定格式，无需处理额外兼容问题 模型兼容性 极差，换个模型就要重写一整套提示词 极好，主流大模型通用，一套工具定义可跨模型使用 总结 整理一下这一章的核心认知：\nFunction Calling 是什么：大模型和 Agent 之间的标准化工具调用协议，解决的是「大模型做出调用决策后，如何精准传递指令」的问题，是整个 Agent 开发的底层基础。\n为什么需要它：没有 Function Calling 之前，只能用自然语言在 System Prompt 里描述工具，大模型的回复格式完全不可控，开发者要写大量解析和纠错代码；有了 Function Calling，双方都遵循固定的 JSON 规范，彻底告别猜谜。\n三部分结构：工具定义（我能用什么）→ AI 调用格式（我要调哪个）→ 工具返回结果（调完得到什么），对应 Agent 执行循环的第 1、2、4 步。\n后续章节呼应：\nRAG：一种特殊工具，专门解决「大模型看不到你私有数据」的问题，重要到单独一章\nMCP：工具的标准化协议，不用每次都自己写工具定义，直接接入现成生态\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%20Function%20Call%EF%BC%9F/","summary":"上一章你已经知道了，一个工具 = 函数本体 + 名称 + 描述 + 参数定义，大模型靠工具描述来判断要不要调这个工具。  但这里有个关键问题还没解决： 大模型做出「要调这个工具」的决策之后，它怎么告诉 Agent 去执行？ 大模型只会\u0026quot;说话","title":"什么是 Function Call？"},{"content":"goframe框架 GoFrame 是一款模块化、高性能的Go 语言开发框架。 如果您想使用 Golang 开发一个业务型项目，无论是小型还是中大型项目，GoFrame 是您的不二之选。如果您想开发一个 Golang 组件库，GoFrame 提供开箱即用、丰富强大的基础组件库也能助您的工作事半功倍。https://goframe.org/\n安装框架 https://goframe.org/quick/scaffold-index\n使用脚手架快速启动一个http服务\ngf init demo -u cd demo \u0026amp;\u0026amp; gf run main.go 配置Goland，使得能自动生成代码\n新增chat接口 首先，我们在api目录下依葫芦画瓢创建chat/v1目录，并编写Chat接口 📷 [图片 token=QQ1UbNIquoFdrrx8I6lcddNanHd（未能下载，见飞书原文）]\ntype ChatReq struct { g.Meta `path:\u0026#34;/chat\u0026#34; method:\u0026#34;get\u0026#34; summary:\u0026#34;对话\u0026#34;` Id string Question string } type ChatRes struct { Answer string `json:\u0026#34;answer\u0026#34;` } Ctrl+S保存，此时框架会自动帮我们生成internal/controller/chat控制层的代码。我们返回一个chat demo字符串回去 📷 [图片 token=WEDobzrNGo9yNMxdAQpc2Uj9ncc（未能下载，见飞书原文）]\nfunc (c *ControllerV1) Chat(ctx context.Context, req *v1.ChatReq) (res *v1.ChatRes, err error) { return \u0026amp;v1.ChatRes{Answer: \u0026#34;chat demo\u0026#34;}, nil } 将我们刚才写的chat控制器绑定上去 📷 [图片 token=MNiabHIrUoFZJoxhbB6cGLpbnUg（未能下载，见飞书原文）]\n最后，我们需要将collector层的返回值返回出去 📷 [图片 token=MuqZbODrvogywix0U77cWLuwnvc（未能下载，见飞书原文）]\nfunc main() { s := g.Server() s.Group(\u0026#34;/api\u0026#34;, func(group *ghttp.RouterGroup) { group.Middleware(ResponseMiddleware) group.Bind(chat.NewV1()) }) s.SetPort(6872) s.Run() } func ResponseMiddleware(r *ghttp.Request) { r.Middleware.Next() var ( msg string res = r.GetHandlerResponse() err = r.GetError() ) if err != nil { msg = err.Error() } else { msg = \u0026#34;OK\u0026#34; } r.Response.WriteJson(Response{ Message: msg, Data: res, }) } type Response struct { Message string `json:\u0026#34;message\u0026#34; dc:\u0026#34;消息提示\u0026#34;` Data interface{} `json:\u0026#34;data\u0026#34; dc:\u0026#34;执行结果\u0026#34;` } 运行 go run main.go 看到route里面有刚才写的接口就说明成功了，赶快试一试吧～\n📷 [图片 token=OggPb9ghToWnumxhQtycXrFAngc（未能下载，见飞书原文）]\n📷 [图片 token=M586bpguroPIEsxcX7echucMnHb（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Go%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8goframe%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAhttp%E6%8E%A5%E5%8F%A3%EF%BC%88Go%EF%BC%89/","summary":"goframe框架 GoFrame  是一款模块化、高性能的 Go  语言开发框架。 如果您想使用  Golang  开发一个业务型项目，无论是小型还是中大型项目， GoFrame  是您的不二之选。如果您想开发一个  Golang  组件","title":"使用goframe框架3分钟实现一个http接口（Go）"},{"content":"LangChain 是什么？ LangChain 是一个专为大语言模型（LLM）应用开发设计的开源框架。它提供了一套标准化的接口，让开发者能够轻松对接各种大模型（OpenAI、通义千问、DeepSeek 等），而无需关心底层 API 的差异。\n简单来说，LangChain 之于 Python AI 开发，就像 Spring AI 之于 Java AI 开发——统一接口、屏蔽差异、开箱即用。\n它的核心优势：\n模型无关：同一套代码可以无缝切换不同的大模型，只需修改配置\n生态丰富：内置对话记忆、RAG、Agent、工具调用等高级能力\n兼容 OpenAI 协议：国内大多数模型（通义千问、DeepSeek 等）都提供了 OpenAI 兼容接口，LangChain 可以直接对接\n下面我们通过一个最简单的对话接口，带你快速上手。\n源码：使用 LangChain + FastAPI 实现一个 AI 对话接口\n源码：[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/) 使用langchain实现一个ai对话接口\n创建项目 \u0026amp; 安装依赖 创建一个项目目录，新建 requirements.txt，写入以下依赖：\nfastapi uvicorn langchain-openai 然后执行安装：\npip install -r requirements.txt 三个依赖各司其职：\n依赖 说明 fastapi 高性能 Python Web 框架，用于快速构建 HTTP 接口 uvicorn ASGI 服务器，负责启动和运行 FastAPI 应用 langchain-openai LangChain 对 OpenAI 协议的封装，支持所有兼容 OpenAI 接口的模型 2. 配置大模型 使用阿里云百炼（DashScope）提供的 OpenAI 兼容接口来接入通义千问模型。\nAPI Key 获取方式：前往 阿里云百炼控制台 创建 API Key。\n在代码中配置模型时需要三个关键参数：\n参数 说明 api_key 你在百炼控制台获取的 API Key base_url 百炼的 OpenAI 兼容地址：https://dashscope.aliyuncs.com/compatible-mode/v1 model 使用的模型名称，这里选用 qwen3-max 3. 编写对话接口（完整代码） 整个项目只需一个 main.py 文件，代码如下：\nfrom fastapi import FastAPI from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage app = FastAPI() llm = ChatOpenAI( api_key=\u0026#34;sk-你的API Key\u0026#34;, base_url=\u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34;, model=\u0026#34;qwen3-max\u0026#34;, ) @app.get(\u0026#34;/api/chat\u0026#34;) async def chat(id: str = \u0026#34;\u0026#34;, question: str = \u0026#34;\u0026#34;): if not question: return {\u0026#34;message\u0026#34;: \u0026#34;question 不能为空\u0026#34;, \u0026#34;data\u0026#34;: None} response = llm.invoke([HumanMessage(content=question)]) return { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: response.content } } 没看错，总共不到 20 行代码，就实现了一个完整的 AI 对话接口。\n4. 关键代码解析 代码 说明 ChatOpenAI(...) 创建大模型客户端，通过 base_url 指向百炼的 OpenAI 兼容接口，一行配置即可对接通义千问 HumanMessage(content=question) 将用户的问题封装为 LangChain 标准的「用户消息」格式 llm.invoke([...]) 同步调用大模型，传入消息列表，等待返回结果 response.content 提取大模型返回的文本内容 @app.get(\u0026quot;/api/chat\u0026quot;) 使用 FastAPI 定义一个 GET 接口，通过查询参数接收用户提问 整个调用链路一气呵成：封装消息 → 调用模型 → 提取回答，核心逻辑只需一行代码。\n5. 启动 \u0026amp; 测试 启动应用：\nuvicorn main:app --host 0.0.0.0 --port 8080 参数说明：\nmain:app：main 是文件名，app 是 FastAPI 实例的变量名\n--host 0.0.0.0：允许外部访问\n--port 8080：监听 8080 端口，与 Java 版本保持一致\n启动成功后，打开浏览器或使用 curl 访问：\ncurl \u0026#34;http://localhost:8080/api/chat?question=你好，介绍一下你自己\u0026#34; 返回结果示例：\n{ \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;你好！我是通义千问，一个由阿里云开发的大语言模型...\u0026#34; } } 此外，FastAPI 还自带交互式 API 文档，启动后访问 http://localhost:8080/docs 即可在浏览器中直接测试接口，无需任何额外工具。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Python%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8langchain%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95AI%E5%AF%B9%E8%AF%9D%28Python%29/","summary":"LangChain 是什么？ LangChain 是一个专为大语言模型（LLM）应用开发设计的开源框架。它提供了一套标准化的接口，让开发者能够轻松对接各种大模型（OpenAI、通义千问、DeepSeek 等），而无需关心底层 API 的差异","title":"使用langchain框架3分钟实现一个简单AI对话(Python)"},{"content":"Spring Boot框架 Spring Boot 是基于 Java 的轻量级、开箱即用的应用开发框架。它极大简化了 Spring 应用的搭建和开发过程，无论是小型服务还是中大型企业级项目，Spring Boot 都是 Java Web 开发的首选。https://spring.io/projects/spring-boot\n源码：[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/) 使用spring boot实现一个http接口\n安装环境\n安装 Maven（或使用 IDEA 自带的 Maven） 快速创建项目 创建 `pom.xml`，引入 Spring Boot 父工程和 Web Starter：\n\u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.5\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 创建启动类 `src/main/java/com/example/App.java`：\n@SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } } 新增chat接口 下面我们动手写一个 `/api/chat` 接口，体验 Spring Boot 开发的完整流程。\n1. 创建请求和响应类 在 `src/main/java/com/example/model/` 目录下新建两个类：\n// ChatRequest.java package com.example.model; public class ChatRequest { private String id; private String question; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getQuestion() { return question; } public void setQuestion(String question) { this.question = question; } } // ChatResponse.java package com.example.model; public class ChatResponse { private String answer; public ChatResponse(String answer) { this.answer = answer; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } } 2. 创建统一响应包装类 在 `src/main/java/com/example/model/` 目录下新建：\n// Result.java package com.example.model; public class Result\u0026lt;T\u0026gt; { private String message; private T data; public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; ok(T data) { Result\u0026lt;T\u0026gt; r = new Result\u0026lt;\u0026gt;(); r.message = \u0026#34;OK\u0026#34;; r.data = data; return r; } public static Result\u0026lt;?\u0026gt; error(String msg) { Result\u0026lt;?\u0026gt; r = new Result\u0026lt;\u0026gt;(); r.message = msg; return r; } public String getMessage() { return message; } public T getData() { return data; } } 3. 编写 Controller 在 `src/main/java/com/example/controller/` 目录下新建 `ChatController.java`：\npackage com.example.controller; import com.example.model.*; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(\u0026#34;/api\u0026#34;) public class ChatController { @GetMapping(\u0026#34;/chat\u0026#34;) public Result\u0026lt;ChatResponse\u0026gt; chat(ChatRequest req) { return Result.ok(new ChatResponse(\u0026#34;chat demo\u0026#34;)); } } 就这么简单！Spring Boot 通过 `@RestController` 和 `@GetMapping` 注解，几行代码就完成了接口定义。请求参数会自动绑定到 `ChatRequest` 对象上。\n运行 在项目根目录执行：\nmvn spring-boot:run 或者直接在 IDEA 中点击 `App.java` 的运行按钮。\n看到控制台输出类似以下内容就说明启动成功了：\nStarted App in x.xxx seconds Tomcat started on port 8080 打开浏览器访问：\nhttp://localhost:8080/api/chat?id=1\u0026amp;question=hello\n返回结果：\n{ \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;chat demo\u0026#34; } } 大功告成，赶快试一试吧～\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/Java%20%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E5%B0%8F%E5%AE%9E%E6%88%98/%E4%BD%BF%E7%94%A8springboot%E6%A1%86%E6%9E%B63%E5%88%86%E9%92%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAhttp%E6%8E%A5%E5%8F%A3%EF%BC%88Java%EF%BC%89/","summary":"Spring Boot框架 Spring Boot 是基于 Java 的轻量级、开箱即用的应用开发框架。它极大简化了 Spring 应用的搭建和开发过程，无论是小型服务还是中大型企业级项目，Spring Boot 都是 Java Web 开","title":"使用springboot框架3分钟实现一个http接口（Java）"},{"content":"前言 AI IDE：cursor，模型用的auto，让cursor自己选\n建议看视频，一共大概20分钟，ai就把前端写出来了，全程我只提需求\n图片演示 让它实现对话接口 📷 [图片 token=H29FbJsnCoGyh0xy5FOctG2rnKU（未能下载，见飞书原文）]\n📷 [图片 token=X2ucbZOJaoohbdxpP9lc3doFnDg（未能下载，见飞书原文）]\n太丑了，让它模仿chatgpt 📷 [图片 token=Rdo3bvLC5oVRl4xmZBQcvxNAnjf（未能下载，见飞书原文）]\n📷 [图片 token=JdbUb42hEoPFnpxdR4Fct0F4nlg（未能下载，见飞书原文）]\n继续让它实现流式接口 📷 [图片 token=OhfYbxbHAoCXYUxuyf0cQD3vn0g（未能下载，见飞书原文）]\n📷 [图片 token=Rgt8bTnzlo0gXexy8DPcB9Z9nDe（未能下载，见飞书原文）]\n实现上传文件的功能 📷 [图片 token=F33BbDTd4oOHpaxDg2scBOplnDb（未能下载，见飞书原文）]\n📷 [图片 token=IZoNbOTQUohRavxOTpmcBtGynJI（未能下载，见飞书原文）]\n去除没实现的ui 📷 [图片 token=Z3RAbxpHLoRjlBx91czccumUnUe（未能下载，见飞书原文）]\n📷 [图片 token=TLGGbKiEdoNgLmxf5MscJooKnrf（未能下载，见飞书原文）]\n要求实现多会话的功能 📷 [图片 token=YtLVbWUooocqstxNPWXcflc8nEb（未能下载，见飞书原文）]\n📷 [图片 token=LgzBbxbSVorVfkxeAk2cXNWSnid（未能下载，见飞书原文）]\n实现AI OPS功能 📷 [图片 token=Cr30b8HyYoGCP5x9kamcch4hnOb（未能下载，见飞书原文）]\n📷 [图片 token=WLD3bOFywoBDMexkdFQcwkbMn0g（未能下载，见飞书原文）]\n功能实现完成了，改一下风格 📷 [图片 token=QBsxbOYUHoizFMxKLx1cPwGKnrq（未能下载，见飞书原文）]\n📷 [图片 token=ZAaobfNDvokxgJx7QxeclM0Cnof（未能下载，见飞书原文）]\n视频演示 🎬 视频「快速对话与流式对话.mp4」（飞书视频，无法在博客播放）\n🎬 视频「上传文件与AIOPS.mp4」（飞书视频，无法在博客播放）\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%83%E7%AB%A0%EF%BD%9C%E5%89%8D%E5%90%8E%E7%AB%AF%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0/%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0%EF%BC%9Avibe%20coding%20%E5%BC%80%E5%8F%91%E5%89%8D%E7%AB%AF%E9%A1%B5%E9%9D%A2%E6%BC%94%E7%A4%BA/","summary":"前言 AI IDE：cursor，模型用的auto，让cursor自己选 建议看视频，一共大概20分钟，ai就把前端写出来了，全程我只提需求  图片演示 1. 让它实现对话接口 \u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image t","title":"前端实现：vibe coding 开发前端页面演示"},{"content":"各位同学，大家在日常学习或项目中查找技术文档时，有没有遇到过大海捞针的困境？比如，一个关键API的鉴权步骤、或者某个技术方案的细节，可能分散在各种文档中，你不得不在层层嵌套的目录里翻找半天。这不仅效率低下，还特别浪费时间。\n知识库Agent就是为了彻底解决这个痛点而生的。简单来说，它就像团队里最可靠的智能知识管家，它的核心使命，是为所有AI应用提供一个高质量、可复用的知识底座。它将团队积累的海量文档，从传统的信息孤岛，转化为可供AI即时调用的数字资产库。\n📷 [图片 token=KB2BbhOLpohZWoxJe7VcOprYnYf（未能下载，见飞书原文）]\n📷 [图片 token=Lpl0bmquNogaX8xqUYpcFZMnnjh（未能下载，见飞书原文）]\n知识库Agent的核心目标 知识库Agent的核心目标是作为团队知识管理和AI应用的基础设施，通过自动化流程，将我们日常积累的文档（比如PDF、Markdown、技术手册、告警处理记录、历史工单等），转化为可被AI高效检索的向量资产。\n它实现了文档上传后的全流程自动化处理，彻底告别了传统手动整理和向量化的繁琐操作。这个过程为后续的RAG（检索增强生成）机制提供了高质量的向量数据支撑。\n核心流程 文档拆分：Agent会自动识别并按照章节、段落、甚至自定义规则来拆分上传的文档，并适配PDF、Markdown等不同格式的内容结构，确保每个片段都包含完整的语义信息。\n文本向量化转化，调用Embedding模型：Agent会将拆分后的内容，调用的Embedding模型转化为高维向量。这个过程是将人类可读的文本，转化为AI可理解的数学表达，是确保语义信息准确映射的关键步骤。\n向量库存储：将生成的向量及其元信息（例如，文档来源、章节位置、作者、更新时间等）批量存入向量数据库中。\n三大核心应用价值 支撑RAG精准检索，大幅提升AI可靠性 知识库Agent最大的价值，是作为RAG技术的可靠数据底座。它确保了AI的回答能够言之有物，避免瞎猜。\n相似性检索：无论是对话Agent的咨询，还是运维Agent的告警排查，知识库Agent都能通过向量数据库的相似性匹配算法，快速返回最相关、最准确的文档片段。\n例如：当对话Agent回答API鉴权失败怎么办时，它直接定位到文档中对应的鉴权章节。运维Agent在处理错误码xxx告警时，能匹配到《错误码汇总》中的解决方案。\n检索结果增强：返回的结果都会附带文档来源、章节位置等元信息，这不仅方便用户验证答案的准确性，也为RAG的大模型生成最终回答提供了可靠的依据。\n知识沉淀与跨场景复用 知识库Agent将散落的技术文档、告警手册、历史工单等转化为统一的向量资产，实现了知识的结构化和集中化。\n团队知识资产化：通过自动存储和更新知识，它解决了新人上手慢、老同事经验流失的问题。新同事可以通过AI快速调取历史经验。离职员工的宝贵经验也能通过文档留存，转化为团队共享的资产。\n一次入库，多端受益：知识库Agent的向量库具有通用性，能够无缝对接多个上层应用，实现一次存储、多Agent调用。例如，一次上传的《故障处理手册》：\n既能支撑对话Agent回答值班人员的告警问题。 也能让运维Agent在自动排查时调用。 甚至可以赋能工单系统，实现对重复性问题的自动回复，降低客服压力。 从被动查找到主动赋能，解放研发精力 知识库Agent彻底解决了文档分散、查找耗时的问题，实现了从大海捞针到秒级找到的转变。\n想象一下，当你上传《告警处理手册》后，Agent自动完成拆分、向量化和入库。处理完成后，系统就能立刻支持提问：XX告警应该怎么处理。知识库Agent帮助团队解放了文档管理的重复劳动，让研发人员不再浪费时间在找文档上。\n总结 知识库Agent绝不是一个简单的文档存储或管理工具，它是连接静态文档和动态AI应用的桥梁。它通过自动化和结构化的方式，将团队的知识从沉睡状态激活成活的资产，有力地支撑RAG在多个业务场景中发挥价值，最终极大地提升团队的整体效率和知识沉淀能力。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E7%9F%A5%E8%AF%86%E5%BA%93%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"各位同学，大家在日常学习或项目中查找技术文档时，有没有遇到过大海捞针的困境？比如，一个关键API的鉴权步骤、或者某个技术方案的细节，可能分散在各种文档中，你不得不在层层嵌套的目录里翻找半天。这不仅效率低下，还特别浪费时间。 知识库Agent","title":"前置准备：知识库的需求、场景、价值分析"},{"content":"背景说明 在介绍项目时，第一步就是讲述你的背景，告诉面试官你是什么场景，以及为何做这个系统。 前面已经讲了背景挂靠的重要性，以及如何挂靠，这里就不再展开讲述如何包装，简单给一个例子：\n这个项⽬源于我们团队内部的⼀个真实痛点，传统的OnCall依赖⼈⼯值守和排查问题，响应慢且占⽤⼤量研发精⼒。就比如之前上游同事天天问同一个问题报错怎么解决，明明文档里写了解决方案还反复问。把时间耗在重复回答上，非常的打杂，所以我就思考怎么样能主动去突破，做一些高价值工作。\n其实这两年AI非常火嘛，我就在想能不能用AI技术，打造⼀个智能化的OnCall助⼿，让它能⾃动回答常见问题，并在故障发⽣时主动进⾏初步排查。首先我基于Eino框架设计搭建了RAG知识库来让AI能基于内部⽂档回答问题，然后实现了⼏类Agent，⽐如⽤于对话的对话Agent和⽤于运维排障的运维Agent。\n最终，系统上线后，很多重复性的咨询和告警都能由Agent⾃动处理并给出解决方案，显著减轻了值班的负担。\n内容介绍 内容介绍要有层次结构，最忌讳的就是上来说你有哪几个服务，这样会显得很乱。这里给一个建议的说辞：\n项目里一共实现了3个Agent，分别是知识库Agent、对话Agent和运维Agent。\n知识库Agent的核心目标是作为团队文档管理和AI应用的基础设施，通过自动化流程，将我们日常积累的文档(告警处理手册、技术方案、错误码文档)，转化为可被AI高效检索的向量，为后续的RAG提供了高质量的向量数据支撑。举个最常见的例子，当我们想根据一个模糊的回忆找文档的时候，可以根据模糊的提问快速检索到对应文档，不再需要再嵌套目录里面一个一个翻了。\n对话Agent本质上是一个基于大模型+知识库构造的智能交互系统。你可以把它看作是一个能够像真人一样理解问题、调用知识库检索并给出精准回答的小助手。它最重要的使命就是帮助团队挡掉高频的重复咨询，加速问题解决，从而提高整体的工作效率。\n运维 Agent 是为了解决值班排查问题的痛点而做的。我们团队维护了多个服务，这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，告警太多了。传统的处理方式往往依赖于人工排查：看告警、查日志、查监控，最终才能判断问题的根源。整个过程重复、耗时，特别是在晚上或节假日，响应效率和准确性更是难以保障，值班的时候告警一多就很痛苦。运维 Agent 可以通过调用各平台的 API，实现跨系统联动，一站式完成排查。例如，它可以自动从告警中提取接口名和时间范围，查询日志、查询监控、查询告警处理手册，将所有信息汇总成一份结构化的故障排查报告。\n到这里可以略微停顿一下，如果面试官还没打断你，那么可以继续进行要点介绍\n要点介绍 这里我们需要提一下项目的要点或者说重点，面试官是比较容易被要点和重点吸引的，同时也相当于提了几个可以讨论的点，方便于面试官提问，说得直白点，就是给面试官喂饭，让他能更好地完成这次的面试工作。下面我们也给一个建议的说辞：\n项目核心亮点可概括为3点：\n知识库系统：核心就是RAG，只要文档上传到知识库了，就能进行知识检索。\n对话Agent：用ReAct解决交互类问题，彻底从人工客服解放。问题匹配+知识检索无缝对接所有涉及到文档的场景。\n运维Agent：用Plan-Execute-Replan解决流程化任务，打通日志、监控、告警群、知识库。\n讲要点的时候点到即止，不用太过展开，不然时间来不及，面试官有兴趣会深入问下去的。这里一定要明白，前面已经讲得够多了，如果这里你再讲的太拖沓，很容易被中间打断，这样节奏会全乱掉。\n这里再停顿1，2秒，如果依旧没被叫停，那么恭喜你，这次讲述应该还是比较有层次结构和节奏感的，下面就可以兜售一下价值来进行一个总结性的收尾。\n价值兜售 价值兜售，顾名思义就是主动推销你的价值，简单来说，就是阐述通过项目体现了你哪些价值，哪些问题能体现出你的能力，这里的灵感可以从前面的难点亮点里去找，本质上这里是个总结性的发言，是用来作为最后的抓手，吸引面试官注意。\n业务价值：对话Agent承接80%重复咨询，释放中台人力聚焦核心开发； 运维Agent让故障响应时间从30分钟缩短至10秒，跨系统联动日志和监控，提升效率\n技术价值：掌握RAG，ReAct，Plan-Execute-Replan，SSE，多轮对话，AI实践经验 等前沿技术，为团队后续AI项目提供参考\n团队价值：标准化运维与咨询流程，降本增效\n面试节奏 上面的内容都是比较标准化的解答方式，下面来看看真实面试的画风是怎么样的\n项目介绍\n我实习的时候，被重复问题烦到想骂人。之前上游同事天天问同一个问题报错怎么解决，明明文档里写了解决方案还反复问。做Agent项目前，我就天天面对这些事，像全职客服，把时间耗在重复回答上，非常的打杂，所以我就思考怎么样能主动去突破，做一些高价值工作。\n不是说值班就不是高价值工作，确实能很快的根据问题来熟悉组里面的服务。但是遇到重复问题，重复回答还是很难受。所以我想了很久，最后决定用Agent来自动化这些重复工作。但不是随便做一个啊，而是针对不同场景设计了不同的模式。比如对话场景用ReAct模式，运维场景用Plan-Execute-Replan模式。\n主动提问：那我先介绍一下对话场景？\n先说说对话场景的ReAct模式吧。最早做的就是很简单的对话+RAG，但是这种模式只能处理简单的单轮问题，比如直接问这个报错怎么处理。但遇到复杂问题就回答不知道了，用ReAct模式的话，Agent会先拆解步骤，然后一步步调用工具查，最后整合结果给答案。比如我想让AI帮我查一下这个 req id 的所有error日志，没有用ReAct之前肯定是调用不了工具的，但是用了之后，大模型就可以使用查日志的功能，日常使用起来就很方便。\n主动提问：需要我介绍一下RAG是什么吗？\n\u0026hellip;\n主动提问：那我介绍一下ReAct是什么？\n\u0026hellip;\n等面试提问：那你再说说运维场景吧\n运维场景的Plan-Execute-Replan模式核心就是自动规划步骤，然后按照步骤执行，遇到异常情况可以重新规划步骤。比如接口失败就查日志、看监控、问下游，但遇到日志没异常的情况就停滞了。比如CPU使用率突增的告警，按固定步骤查日志没结果，Agent就不知道怎么办了。但用这个模式的话，Agent会根据结果动态调整计划，比如日志没异常就先查高耗CPU进程，找到问题后再继续分析根因。这样即使遇到预设步骤外的情况，Agent也能自主排查。\n主动提出\n我把问题匹配和知识检索的核心逻辑抽象成对话Agent，一次开发就能对接多个场景，比如给业务方用自动答文档问题，给研发用告警自动出方案，给运维用历史工单RAG分析。效率提升非常多，以前人工排查故障要5到10分钟，现在Agent1分钟内就能完成日志检索、错误分类和根因推测。\n主动突出你的思考\n知识沉淀也是一个重要的点，我后面打算接入群聊，把工单问题和解决方案的聊天记录，自动总结沉淀为知识库里的文档，下次遇到了相同的问题问一遍AI，快速解决。\n总结 其实做Agent项目的面试亮点，核心就是把这些真实的痛点讲清楚，再把技术怎么解决这些痛点的过程说透。你不用刻意堆术语，只要让面试官感受到这个项目确实帮团队解决了大问题，而且你对技术选型有自己的思考就行。\n比如你可以说针对 对话和运维场景分别使用了ReAct和Plan-Execute-Replan模式，因为不同场景的需求不一样。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/%E5%A6%82%E4%BD%95%E4%BB%8B%E7%BB%8D%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE/","summary":"背景说明  在介绍项目时，第一步就是讲述你的背景，告诉面试官你是什么场景，以及为何做这个系统。 前面已经讲了背景挂靠的重要性，以及如何挂靠，这里就不再展开讲述如何包装，简单给一个例子：  这个项⽬源于我们团队内部的⼀个真实痛点，传统的OnC","title":"如何介绍你的项目"},{"content":"整体架构 我们先给出整体的服务架构图，简单来说我们的智能OnCall系统有三大Agent：知识库Agent，对话Agent、运维Agent。\n这个的架构图可能一时半会还不能完全看懂，没关系，先有个印象就行，后续章节会进行抽丝剥茧的讲解，后面的实战章节也会层层递进更详细的分析。\n📷 [图片 token=QKQEbFTjyo4JH5xQLYCcCvW6nUg（未能下载，见飞书原文）]\n层次结构 一个服务通常可以分为几层来看，对于智能OnCall Agent系统而言，同样如此，系统可以分为如下几层：\n接入层：对外提供api接口\n业务层：利用服务层的组件进行编排，负责向接入层提供编排完成可使用的Agent\n服务层：核心组件实现层，如加载文件，切分文件，提供工具等。负责向业务层提供原子能力，支撑Agent的编排\n存储层：文档信息我们存储到向量数据库中\n编排流程 下面介绍最核心的三个Agent的编排流程，这三个Agent贯穿整个项目。\n注意：初览下面的流程图，或许会觉其逻辑脉络稍显繁复——不必急于一时。待你通读完《架构设计》篇章，再折返审视，其内在关联便会豁然开朗。\n注意：有些术语看不懂没关系，后续篇幅会详细介绍，先有个概念即可！！！\n知识库RAG 整体流程分为两部分：\n提问前链路（数据准备）\n分片：将原始文档（如业务告警处理手册）切割为多个语义完整的片段。\n索引：\n用Embedding模型将每个片段转为向量。 将片段文本和向量存入向量数据库。 完成后，知识库即构建完毕，等待用户提问。 提问后链路（回答生成）\n召回：将用户问题向量化，去向量数据库里面进行相似度匹配，查询最相似的10个片段（快但准度低）。\n重排：使用专门计算文本对相似度的模型，逐对计算用户问题与每个召回片段的语义相关性（慢但准度高）。\n生成：选出最相似的3个片段+用户问题，交给大模型，让大模型根据提供的3个片段进行回答。\n📷 [图片 token=FxcdbnrjboDxkExZLJlc0FHInCh（未能下载，见飞书原文）]\n对话Agent 对话Agent的核心目标是结合外部知识（RAG召回）与工具调用能力（ReAct模式），解决复杂问题。\n整体流程可概括为：\n从向量数据库中获取与用户问题相关的上下文信息\n构建带上下文(召回的内容)的system prompt\n使用ReAct模式进行多轮交互\n最终输出答案\n举一个技术场景的例子，问题：地球和火星的质量加起来是多少？（AI 不知道具体数值，需要调用工具）\n第1次循环：思考-\u0026gt;行动-\u0026gt;观察\n思考：我需要地球和火星的质量，得先查地球质量。 行动：调用查星球质量工具，输入地球 观察：工具返回地球质量≈5.97×10²⁴ kg 第2次循环：再思考-\u0026gt;行动-\u0026gt;观察\n思考：有了地球质量，还缺火星的，继续查火星。 行动：调用查星球质量工具，输入火星 观察：工具返回火星质量≈6.42×10²³ kg 第3次循环：再思考-\u0026gt;行动-\u0026gt;观察\n思考：现在有两个数了，需要相加，得用计算器。 行动：调用计算器工具，输入5.97e24 + 6.42e23 观察：工具返回≈6.61×10²⁴ kg 结束循环：结果够了，可以回答用户了，地球和火星的质量加起来是6.61×10²⁴ kg。\nflowchart TD Start([开始]) --\u0026gt; Input[/接收用户问题 地球和火星质量总和?/] Input --\u0026gt; Reason{思考} Reason --\u0026gt;|1. 制定计划: 需地球质量| R1[思考 需要地球质量，先查询地球] R1 --\u0026gt; A1[行动 调用工具查询地球质量] A1 --\u0026gt; O1[观察 返回: 5.97×10²⁴ kg] O1 --\u0026gt; Reason Reason --\u0026gt;|2. 继续执行: 需火星质量| R2[思考 已有地球质量，还需火星质量] R2 --\u0026gt; A2[行动 调用工具查询火星质量] A2 --\u0026gt; O2[观察 返回: 6.42×10²³ kg] O2 --\u0026gt; Reason Reason --\u0026gt;|3. 继续执行: 需计算总和| R3[思考 数据齐全，需要计算总和] R3 --\u0026gt; A3[行动 调用计算器工具] A3 --\u0026gt; O3[观察 返回总和: 6.61×10²⁴ kg] O3 --\u0026gt; Reason Reason --\u0026gt;|4. 拥有足够信息| Answer[生成最终答案] Answer --\u0026gt; End([结束]) ReAct = Reasoning（推理）+ Acting（行动），核心是让 AI 像人一样 边想边做、边做边调整，通过 思考-\u0026gt;行动-\u0026gt;观察-\u0026gt;再思考的闭环 解决问题。\nReAct 就是让 AI 模仿这个过程：遇到问题不直接瞎猜答案，而是先想该查什么，再调用工具（比如计算器、数据库、搜索引擎），拿到结果后判断够不够，不够就继续查，直到能给出最终答案。\n📷 [图片 token=Qa7WbrNlpoYHMxxyiGYcFFnsnNd（未能下载，见飞书原文）]\n运维Agent 运维Agent的核心目标是将运维人员的告警处理经验转化为自动化流程，通过计划生成-\u0026gt;工具执行-\u0026gt;动态调整的闭环，替代人工完成重复性告警排查工作。\n整体架构可概括为：\n从向量数据库中获取与告警相关的上下文信息\n构建带上下文(召回的内容和工具信息)的system prompt\n使用Plan-Execute-Replan模式进行多轮交互\nPlanner生成结构化排查计划\nExecutor调用监控/日志工具执行步骤\nReplanner评估结果，决定继续执行/调整计划/输出结论\n最终输出答案\n用大白话举个例子：想象你要装修一套新房，完全没经验的话，你会怎么避免手忙脚乱？\nPlan（规划）：先找设计师出详细方案，拆改哪里、水电怎么走、用什么材料、分几个阶段施工，形成清晰的步骤清单。\nExecute（执行）：施工队按计划开工，先拆旧、再布水电，一步一步推进当前阶段的任务。\nReplan（重规划）：施工中发现原计划的承重墙不能拆，则设计师重新调整布局（比如把书房门改到另一侧），更新计划后继续施工，直到最终完工。\n📷 [图片 token=F387bGybOoq8gxx16aicmwqlnBr（未能下载，见飞书原文）]\nPlan-Execute-Replan 就是让 Agent 模仿这个过程：遇到复杂任务不盲目动手，而是先设计方案，再按图施工，遇到问题时灵活调整方案，确保最终达成目标。\n📷 [图片 token=LQLAbT2aIodUbJxqJEdcgtqVnJd（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%8C%E7%AB%A0%20_%20%E9%A1%B9%E7%9B%AE%E5%85%A8%E5%B1%80%E8%AE%A4%E7%9F%A5%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%EF%BC%9A%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%AE%8F%E8%A7%82%E8%A7%86%E9%87%8E/","summary":"整体架构 我们先给出整体的服务架构图，简单来说我们的智能OnCall系统有三大Agent：知识库Agent，对话Agent、运维Agent。 这个的架构图可能一时半会还不能完全看懂，没关系，先有个印象就行 ，后续章节会进行抽丝剥茧的讲解，后","title":"整体架构：项目的宏观视野"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习[架构设计：Plan-Execute-Replan设计模式核心原理](/oncall/智能 OnCall Agent 项目/旧文档备份/架构设计：Plan-Execute-Replan设计模式核心原理/)\n一、为什么要用Plan-Execute-Replan？ 大家好，今天来聊一个在运维Agent领域非常核心的设计模式——Plan-Execute-Replan。\n如果你正在做运维Agent相关的项目，或者对AI Agent的架构设计感兴趣，这篇文章应该能帮你把这个模式彻底搞明白。\n我会从\u0026quot;为什么需要它\u0026quot;讲起，一步步带你理解它的本质、工作流程，以及它和另一个常见模式ReAct的区别。\n📷 [图片 token=SFaVbNVtQovtH7xXUjgcTxPonui（未能下载，见飞书原文）]\n在聊这个模式之前，我们先想一个问题：工程师平时处理告警的时候，到底在干嘛？\n假设凌晨3点，你手机响了——某台服务器CPU飙到100%了。作为一个值班同学，你大概会这么干：\n先登上去看看日志，有没有明显报错\n再看看监控面板，哪个进程吃的CPU最多\n查一下这个进程最近有没有做什么变更\n根据排查结果，决定是重启服务、回滚版本还是扩容\n看起来挺有条理的对吧？但问题是，不是每个工程师都能做到这么有条理。\n实际工作中，运维告警处理有几个很现实的痛点：\n痛点一：流程全靠经验，新人一脸懵\n老同事知道\u0026quot;先查日志再看监控\u0026quot;，但新人可能上来就手忙脚乱，不知道该先干嘛。排查步骤全在脑子里，没有结构化的流程，效率差距非常大。\n痛点二：遇到\u0026quot;此路不通\u0026quot;就卡住了\n比如你查了日志，发现啥异常都没有——然后就愣住了：\u0026ldquo;日志没问题啊，那是啥原因？\u0026ldquo;人工排查很容易在某个环节卡壳，不知道怎么调整方向。\n痛点三：信息散落在各个系统，来回切换累死人\n日志在ELK里，监控在Prometheus/Grafana里，工单在别的系统里，告警在企业微信群里……人工排查需要在这些系统之间反复横跳，光复制粘贴就够喝一壶的了。\n所以，当我们想让AI Agent来替代人工处理这些告警的时候，就需要一种设计模式，能够：\n像老同事一样有条理地规划排查步骤（解决流程混乱）\n遇到\u0026quot;此路不通\u0026quot;时能灵活调整方向（解决卡壳问题）\n统一调度各个系统的工具（解决信息孤岛）\n这，就是 Plan-Execute-Replan 模式要干的事。\n📷 [图片 token=Izajba1g8o6VOvxq3rncxEW4nkb（未能下载，见飞书原文）]\n二、Plan-Execute-Replan到底是什么？ 一句话定义 Plan-Execute-Replan 是一种Multi-Agent（多智能体）协作的任务执行模式，核心思路是：先规划、再执行、随时调整。\n📷 [图片 token=RfsxbKNHCoiCQ0xWgE3cHDFxn9d（未能下载，见飞书原文）]\n你可以把它理解成一个\u0026quot;三人小团队\u0026quot;在协作完成任务：\n角色 对应组件 干什么 设计师 Planner（规划器） 拿到目标后，拆解成一步一步的计划 施工队 Executor（执行器） 按计划干活，一次只干一步 监理 Replanner（重规划器） 检查干活结果，看看计划要不要调整 这里要解释下什么叫 Multi-Agent（多智能体）。简单说，就是不是一个AI在单打独斗，而是多个AI各司其职、分工协作。每个Agent负责一个专门的职责，就像公司里不同岗位的同事配合完成一个项目。\n装修房子的类比 为了让你更直观地理解，咱们拿装修房子打个比方。\n假设你买了一套新房要装修，完全没经验，你会怎么做？\n第一步：Plan（规划）\n你找了个设计师，让他出一套完整的方案：先拆旧墙→再布水电→然后泥瓦工贴砖→接着木工打柜子→最后刷漆。每一步干什么、用什么材料、大概要多久，都写得清清楚楚。\n这就是 Planner 在做的事——把一个大目标拆成有序的步骤清单。\n第二步：Execute（执行）\n施工队拿到方案后开始干活。先拆旧墙，拆完了汇报：\u0026ldquo;墙拆好了，发现里面有根水管。\u0026rdquo;\n这就是 Executor 在做的事——按计划执行当前步骤，然后把结果反馈回来。注意，Executor不需要操心全局，它只需要把眼前这一步做好就行。\n第三步：Replan（重规划）\n设计师一听\u0026quot;里面有根水管\u0026rdquo;，马上调整方案：\u0026ldquo;水电布线方案要改一下，那根水管得绕过去。后面的步骤也要跟着调整。\u0026rdquo;\n这就是 Replanner 在做的事——根据执行结果评估当前进展，决定要不要调整计划。\n三者不断循环配合，直到房子装修完毕。\n三个核心组件详解 咱们再从技术角度，把这三个组件的职责说清楚。\n1. Planner（规划器）—— \u0026ldquo;军师\u0026rdquo; Planner 是整个模式的起点。它接收用户的目标（比如\u0026quot;排查CPU飙高的原因\u0026rdquo;），然后生成一份结构化的执行计划。\n什么叫结构化？就是不是含糊地说\u0026quot;你去查查吧\u0026quot;，而是明确列出：\n步骤1：调用日志工具，查询服务器近1小时的error/warn级别日志 步骤2：调用监控工具，获取CPU突增时段的进程占用排行 步骤3：调用历史工单，检索该进程过往的CPU异常处理方案 每一步做什么、用什么工具、传什么参数，都规划好。这就像项目经理给你排的Task列表，清晰明了。\nPlanner 的关键能力：理解复杂目标的内在逻辑，把大任务拆成可执行的小步骤，并且安排好合理的执行顺序。\n2. Executor（执行器）—— \u0026ldquo;干活的人\u0026rdquo; Executor 是真正动手的那个。它拿到计划中的当前第一步，然后去调用对应的工具完成任务。\n比如计划说\u0026quot;调用日志工具查error日志\u0026quot;，Executor 就真的去调日志API，传入服务器IP、时间范围、日志级别这些参数，拿到返回结果。\n这里有个重要的设计理念：Executor 只专注做好当前这一步，不操心全局。\n为什么要这么设计？因为\u0026quot;规划\u0026quot;和\u0026quot;执行\u0026quot;本来就是两种不同的能力。你让一个人既要统筹全局又要埋头干活，反而两边都做不好。分开之后，Executor 可以心无旁骛地把当前步骤做到最好。\nExecutor 的关键能力：准确调用工具、处理工具返回的结果。\n3. Replanner（重规划器）—— \u0026ldquo;监理 + 指挥官\u0026rdquo; Replanner 是这个模式里最关键的角色——它让整个系统从\u0026quot;死板执行\u0026quot;变成了\u0026quot;智能适应\u0026quot;。\n每次 Executor 完成一步之后，Replanner 都会介入，做三件事：\n情况一：步骤完成，结果有效 → 继续推进\n\u0026ldquo;日志查到异常了，接下来按计划执行第二步。\u0026rdquo; 这种情况最简单，照着计划往下走就行。\n情况二：结果不符合预期 → 调整计划\n\u0026ldquo;日志查了，啥异常都没有。那原计划的排查顺序可能不太对，得先看监控定位是哪个进程的问题。\u0026rdquo; 这时候 Replanner 会修改后续步骤的内容或顺序。\n情况三：所有步骤完成 → 终止任务，输出结论\n\u0026ldquo;根因已经定位了——是全量同步任务没做分页，导致CPU打满。任务结束，输出报告。\u0026rdquo;\nReplanner 的关键能力：评估执行结果、判断任务进度、识别问题并动态优化计划。\n三、工作流程：从目标到结果的完整闭环 光说概念可能还有点抽象，咱们用一个完整的运维实战案例，把整个流程走一遍。\n📷 [图片 token=SQJCbfJwBoH7IYxHumacAB8snef（未能下载，见飞书原文）]\n场景：凌晨CPU 100%告警 某电商平台凌晨2点，监控系统发出告警：服务器 10.0.1.5 的CPU使用率突然飙到100%。运维Agent接到告警后开始自动排查。\nRound 1：Planner 出手，制定初始计划\nPlanner 拿到目标\u0026quot;排查CPU突增的根因\u0026quot;，结合运维知识，生成了以下计划：\n步骤1：调用日志工具，查询服务器近1小时error/warn级别日志 步骤2：调用监控工具，获取CPU突增时段的进程占用排行 步骤3：调用历史工单，检索该进程过往CPU异常的处理方案 思路很清晰：先看日志有没有直接线索 → 再看监控定位具体进程 → 最后查历史方案。\nRound 2：Executor 执行步骤1——查日志\nExecutor 按照计划，调用日志工具：\n参数：服务器IP=10.0.1.5，时间范围=近1小时，日志级别=error/warn 返回结果：日志中未发现error/warn记录，只有大量info级别的定时任务执行成功日志。\n嗯，没查到有用信息。如果是人工排查，这时候可能就愣住了——\u0026ldquo;日志没问题啊，那是啥情况？\u0026rdquo;\nRound 3：Replanner 介入评估，调整计划\nReplanner 分析执行结果后得出判断：日志无异常，说明问题大概率不是应用报错导致的，更可能是某个进程在\u0026quot;悄悄\u0026quot;吃CPU。那就应该优先看监控数据，定位到底是哪个进程在搞事情。\n于是 Replanner 调整了计划的执行顺序：\n步骤1（已完成）：查日志 → 无异常 步骤2（更新）：调用监控工具，获取02:00-02:10时段进程占用排行 ← 优先执行这个 步骤3（更新）：针对步骤2定位的异常进程，查其详细日志 步骤4：调用历史工单，检索处理方案 看到没？Replanner 不是简单地\u0026quot;跳到下一步\u0026quot;，而是根据已有的结果，重新优化了后续的计划。这就是\u0026quot;Replan\u0026quot;的精髓——动态调整，而不是死板地按原计划走。\nRound 4：Executor 执行更新后的步骤2——查监控\nExecutor 调用监控工具（比如Prometheus），查询CPU突增时段的进程排行。\n返回结果：02:00-02:10时段，进程「data-sync-service」CPU占用率达95%（正常情况下通常不到10%）。\n好家伙，找到嫌疑人了。\nRound 5：Replanner 再次评估\nReplanner 看到结果：\u0026ldquo;已经定位到异常进程了，接下来需要查这个进程的详细日志，看看它到底在干什么。\u0026rdquo; 计划无需大改，继续执行步骤3。\nRound 6：Executor 执行步骤3——查进程日志\nExecutor 调用日志工具，这次参数更精准了：\n进程名=data-sync-service，时间范围=近1小时 返回结果：日志显示02:00触发了一次全量数据同步任务，遍历数据库1000万条记录，没有做分页处理。\n真相大白了！\nRound 7：Replanner 最终评估——任务结束\nReplanner 判断：根因已经明确了——全量同步任务没做分页，导致一次性加载1000万条数据把CPU打满了。不需要再查历史工单了（因为问题已经定位清楚），直接输出结论。\n最终输出：\n故障根因：服务器进程 data-sync-service 在02:00执行全量数据同步时，未做分页处理，遍历1000万条记录导致CPU使用率突增至100%。\n建议方案：优化同步逻辑，添加分页参数（如每次拉取1000条），并将同步任务调整到非高峰时段执行。\n流程小结 把上面的过程抽象一下，Plan-Execute-Replan的工作流程就是这样一个循环闭环：\n📷 [图片 token=D0jXbHKqEodAkExc8ZQco7yWnne（未能下载，见飞书原文）]\n核心就八个字：规划→执行→评估→调整，不断循环，直到任务完成。\n四、和ReAct模式的区别 聊到Agent的设计模式，很多人会拿 ReAct 来对比。这两个模式都能让Agent完成复杂任务，但思路完全不同。\n📷 [图片 token=A8jeb3gT6oVxRXxOrOPcCWBJnne（未能下载，见飞书原文）]\nReAct是什么？ ReAct 的全称是 Reasoning + Acting（推理+行动）。它的核心思路是：边想边做。\n用大白话说就是：Agent每走一步之前，先想一下\u0026quot;我现在该干嘛\u0026quot;，然后干一步，看看结果，再想下一步该干嘛……如此循环。\n它没有一个预先的完整计划，而是每一步都是\u0026quot;临场决策\u0026quot;。\n打个比方来区分 Plan-Execute-Replan 像是按导航开车：\n出发前先看好路线（Plan），然后按导航走（Execute），遇到堵车了导航会重新规划路线（Replan）。你始终知道\u0026quot;我现在在第几步，还有几步到终点\u0026quot;。\nReAct 像是凭感觉找路：\n到了一个路口，想一下\u0026quot;往左好像对\u0026quot;，就往左走；走到下一个路口再想一下\u0026quot;好像该右转了\u0026quot;……每一步都是现想现走，没有全局路线图。\n具体对比 对比维度 Plan-Execute-Replan ReAct 核心思路 先拆步骤，按计划执行，动态调整 边想边做，每步临场决策 有没有全局计划 有，一开始就生成完整计划 没有，走一步看一步 适合什么场景 多步骤、流程化的复杂任务（运维排查、报告生成） 灵活探索类任务（开放问答、信息检索） 任务进度 可追踪，知道执行到第几步了 不太好追踪，因为没有预设步骤 应对变化 通过Replan机制调整计划 天然灵活，每步都能变方向 Agent数量 Multi-Agent协作（Planner + Executor + Replanner） 通常是单Agent完成所有事 什么时候该用哪个？ 用 Plan-Execute-Replan 的场景：\n任务比较复杂，步骤多，需要有条理地推进。比如运维告警排查、自动化报告生成、多系统联动的工作流。这类任务如果\u0026quot;边想边做\u0026quot;，很容易跑偏或者遗漏步骤。\n用 ReAct 的场景：\n任务比较灵活，事先不确定需要几步，探索性强。比如回答一个开放性问题、在网上搜索信息等。这类任务如果非要先出计划，反而显得僵硬。\n对于运维Agent来说，告警排查这种场景天然适合 Plan-Execute-Replan——因为排查步骤是有章可循的，而且需要跨多个系统协作，用结构化的方式来管理更靠谱。\n五、核心优势 聊了这么多，我们总结一下 Plan-Execute-Replan 模式到底好在哪里。\n优势一：结构化拆解，把复杂问题变简单 面对一个复杂任务，人容易\u0026quot;不知从何下手\u0026quot;，Agent也一样。Plan-Execute-Replan 通过 Planner 把大任务拆成小步骤，每一步都清晰可执行，大大降低了认知负荷。\n就像你面对一道很长的数学大题，把它拆成 (1)(2)(3) 三小问来做，每一问就没那么难了。\n优势二：动态适应，遇到问题不\u0026quot;死磕\u0026quot; 这是 Replanner 带来的最大价值。在实际执行中，各种意外都可能发生——工具调用失败、返回数据为空、中间结果和预期不符……\n如果没有 Replan 机制，Agent 就只能傻傻地按原计划继续走，明明方向错了还在往前冲。有了 Replanner，Agent 就能像老司机一样灵活应变：此路不通，换条路走。\n优势三：职责分离，各司其职更高效 Plan-Execute-Replan 把\u0026quot;规划\u0026quot;、\u0026ldquo;执行\u0026rdquo;、\u0026ldquo;评估\u0026quot;三个职责分开，交给不同的Agent来做。这种设计有几个好处：\nPlanner 可以专注于理解目标和拆解任务，不用操心工具怎么调\nExecutor 可以专注于执行当前步骤，不用操心全局进度\nReplanner 可以专注于评估和决策，不用操心具体执行细节\n各管一摊，反而比\u0026quot;一个Agent包揽所有事\u0026quot;效率更高，这也是 Multi-Agent 架构的核心思想。\n优势四：任务进度可观测 因为有明确的步骤计划，我们随时知道\u0026quot;Agent现在执行到第几步了\u0026quot;\u0026ldquo;还剩几步没做\u0026quot;\u0026ldquo;哪一步出了问题\u0026rdquo;。这对于运维场景特别重要——你需要知道Agent的排查进展，而不是把任务扔给它后就只能干等结果。\n六、总结 最后，我们用一张\u0026quot;知识导图\u0026quot;来回顾一下这篇文章的核心内容：\n为什么用？ → 运维告警排查面临流程无序、遇阻卡壳、跨系统低效三大痛点，需要一种结构化且灵活的任务执行模式。\n是什么？ → Plan-Execute-Replan 是一种 Multi-Agent 协作模式，由 Planner（规划）、Executor（执行）、Replanner（重规划）三个智能体角色协同工作。\n怎么工作？ → 规划→执行→评估→调整，不断循环直到任务完成。每次执行完一步都会评估结果，根据实际情况动态调整后续计划。\n和ReAct的区别？ → Plan-Execute-Replan 是\u0026quot;先规划再执行\u0026rdquo;，适合复杂流程任务；ReAct 是\u0026quot;边想边做\u0026rdquo;，适合灵活探索任务。运维排查场景更适合前者。\n核心优势？ → 结构化拆解降低复杂度、动态适应应对意外、职责分离提升效率、任务进度可观测。\n一句话概括：Plan-Execute-Replan 让运维Agent像一个经验丰富的老运维一样——拿到告警先理清思路，然后按步骤排查，遇到意外灵活调整，最终给你一个清晰的结论和建议。\n如果你正在设计运维Agent的架构，Plan-Execute-Replan 几乎是绕不开的核心模式。理解了它的原理，后面再去看具体的代码实现，就会顺畅很多。\n希望这篇文章对你有帮助，有问题欢迎一起交流讨论！\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9APlan-Execute-Replan%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习\u0026lt;mention-doc token=\u0026ldquo;LUn8wkoK4ikvrCktBZrcPHsMnUd\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 架构设计：Plan-Execute-Repla","title":"架构设计：Plan-Execute-Replan设计模式核心原理"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习[架构设计：RAG全流程解析](/oncall/智能 OnCall Agent 项目/旧文档备份/架构设计：RAG全流程解析/)\n先聊聊，为什么需要RAG？ 大家好，今天来聊一个在AI应用领域非常核心的技术——RAG。\n📷 [图片 token=IZUPbTgrNoxvESxpIjtcz1ESnRh（未能下载，见飞书原文）]\n你有没有遇到过这种情况：你兴冲冲地把公司的产品手册丢给ChatGPT，问它一个具体的业务问题，结果它要么答非所问，要么直接\u0026quot;编\u0026quot;了一个听起来很像回事但完全不对的答案？\n这其实不怪大模型\u0026quot;笨\u0026quot;，而是它确实没学过你们公司的内部资料。大模型的知识来自训练数据，你公司那本500页的产品手册，它压根没见过。\n那怎么办呢？最直觉的方式就是——把整本手册全部塞给模型，让它\u0026quot;现学现卖\u0026quot;。\n但这样做有三个硬伤：\n上下文窗口有限：模型一次能处理的文本量是有上限的，500页的手册根本塞不进去。\n成本高：就算塞得进去，每次提问都要把整本手册发过去，token费用谁顶得住？\n大海捞针效果差：内容太多，模型反而容易\u0026quot;迷路\u0026quot;，找不到真正相关的信息。\n所以，聪明人就想了一个办法：我不把整本手册都给你，我先帮你把最相关的几页找出来，你只看这几页就行了。\n这个办法，就是RAG（Retrieval-Augmented Generation，检索增强生成）。\n简单说：先检索，再生成——先从知识库里捞出相关内容，再让大模型基于这些内容回答问题。\nRAG现在被广泛用在企业知识库、智能客服、产品问答助手等场景里，可以说是落地AI应用的\u0026quot;标配\u0026quot;技术了。\n📷 [图片 token=IFPzbhV5ooWqY8xzeu8cIuQInVg（未能下载，见飞书原文）]\n📷 [图片 token=JX5hbO42qoiuDSxhMFAcXXJZnkf（未能下载，见飞书原文）]\nRAG的核心流程：两条链路 RAG的整体流程可以分为两个阶段，我喜欢把它们叫做 \u0026ldquo;提问前\u0026rdquo; （离线阶段）和 \u0026ldquo;提问后\u0026rdquo;（在线阶段）：\n提问前（数据准备）：分片 → Embedding → 存储\n提问后（回答生成）：召回 → 重排 → 生成\n📷 [图片 token=DoKPbgk3roxb1cxhcI0cz8tEnpe（未能下载，见飞书原文）]\n提问前做的事情，本质上就是把你的文档变成一个AI能理解的知识库。提问后做的事情，就是从知识库里找答案，然后让AI组织语言回答你。\n接下来我们一步步来拆解。\n第一阶段：提问前的数据准备 这个阶段的目标只有一个：把你的原始文档，变成一个可以被快速检索的知识库。\n整个过程分三步：分片、向量化、存储。我们一个个来说。\n📷 [图片 token=Lh9eb2rQ7oz1eVx1ER5cGI9En1g（未能下载，见飞书原文）]\n第一步：分片——把大文档\u0026quot;切碎\u0026quot; 想象一下，你有一本1000页的产品手册。如果有人问你\u0026quot;产品A的退货政策是什么\u0026quot;，你不会把整本书递给他，而是会翻到退货政策那一页，把那一段内容指给他看。\n分片做的就是这件事——提前把文档切成一段一段的小片段，每个片段聚焦一个具体的知识点。\n常见的分片方式有：\n按固定字数切：比如每1000个字切一段\n按段落切：以自然段落为单位\n按章节/标题切：按文档本身的结构来\n按页码切：一页一个片段\n这里有一个非常重要的原则：每个片段的语义要完整。\n什么意思呢？比如有这么一段话：\n\u0026ldquo;产品A支持7天无理由退货，但需要注意的是，拆封后的电子产品不在此范围内。\u0026rdquo;\n如果你恰好在\u0026quot;但需要注意的是\u0026quot;这里把它切断了，前半段说\u0026quot;支持退货\u0026quot;，后半段说\u0026quot;不在范围内\u0026quot;，两个片段各自都传达了不完整甚至矛盾的信息。用户搜到前半段就会以为随便退，搜到后半段又不知道说的是什么产品。\n所以，分片不是无脑切，而是要保证切出来的每一段都能独立表达一个完整的意思。\n一本1000页的手册，分完片可能变成500个左右的独立片段，每个片段就像一张知识卡片，聚焦一个具体问题。\n第二步：Embedding——让计算机\u0026quot;听懂\u0026quot;文字 片段切好了，但计算机不认识中文啊。你跟它说\u0026quot;退货政策\u0026quot;，它只看到一堆字符编码，完全不理解这几个字是什么意思。\n所以我们需要一种方式，把人类的语言翻译成计算机能理解的\u0026quot;数学语言\u0026quot;。\n这就是Embedding（向量化）的作用。\n什么是向量？ 别被\u0026quot;向量\u0026quot;这个词吓到，它其实很简单。\n向量就是一组数字，比如 [0.8, 0.2, -0.5] 就是一个三维向量（三个数字，所以是三维）。你可以把它理解为一个坐标点——在三维空间里，这个点的位置就是 (0.8, 0.2, -0.5)。\n低维的向量（1到3维）我们可以在坐标轴上画出来，但实际使用中，Embedding模型生成的向量维度非常高，可能有几百甚至上千维。虽然我们没法想象一个1000维的空间长什么样，但维度越高，能表达的信息就越丰富，对文本特征的刻画就越细腻。\n什么是Embedding？ Embedding就是把一段文字变成一个向量的过程。\n关键来了：意思相近的文字，转出来的向量也会很相近。\n举个例子：\n\u0026ldquo;小林写Python\u0026rdquo; → [0.80, 0.20, -0.50]\n\u0026ldquo;小林写Golang\u0026rdquo; → [0.78, 0.22, -0.48]\n\u0026ldquo;今天天气真好\u0026rdquo; → [0.10, -0.85, 0.30]\n你看，前两句话讲的都是\u0026quot;小林在写代码\u0026quot;，虽然编程语言不同，但核心语义是相近的，所以它们的向量非常接近。而\u0026quot;今天天气真好\u0026quot;跟写代码完全不搭边，向量就差得很远。\n这意味着什么？意味着我们可以通过计算向量之间的距离，来判断两段文字的意思是否相近。这就是RAG能够\u0026quot;检索相关内容\u0026quot;的数学基础。\n所以在这一步，我们要做的就是：把上一步切好的每一个文档片段，都用Embedding模型转成一个向量。\n第三步：存储——把片段和向量存进\u0026quot;数据库\u0026quot; 向量生成好了，我们需要一个地方来存放它们。这个地方就是向量数据库（比如Milvus）。\n向量数据库存的不只是向量，而是向量 + 原始文本，两个一起存。\n为什么要存原始文本？因为向量只是用来做相似度计算的\u0026quot;索引\u0026quot;，最终我们要拿给大模型看的，还是原始的文字内容。\n用一个更直观的方式来理解，向量数据库里的每条数据大概长这样：\n{ \u0026#34;content\u0026#34;: \u0026#34;产品A支持7天无理由退货，拆封后的电子产品除外。\u0026#34;, \u0026#34;vector\u0026#34;: [0.12, 0.34, -0.56, 0.78, ...] } content 是原始文本，vector 是这段文本对应的向量。查询时用向量算相似度，返回结果时给你原始文本。\n到这里，提问前的准备工作就完成了。你的文档已经被切成片段、转成向量、存进数据库，知识库构建完毕，静静等待用户来提问。\n第二阶段：提问后的回答生成 用户终于来提问了！比如他问：\u0026ldquo;产品A怎么退货？\u0026rdquo;\n接下来的流程分三步：召回、重排、生成。\n📷 [图片 token=CSWZb7DYCoWpCOxZP9IclVBVnQc（未能下载，见飞书原文）]\n第一步：召回——从知识库里\u0026quot;广撒网\u0026quot; 召回阶段做的事情，和提问前的索引过程是对称的：\n先把用户的问题也通过Embedding模型转成向量（用的是同一个Embedding模型，这样才能保证在同一个\u0026quot;语义空间\u0026quot;里比较）。\n拿这个问题向量，去向量数据库里找最相似的片段，挑出相似度最高的Top N个（比如10个）。\n这个过程的核心是向量相似度计算。常用的算法有两种：\n余弦相似度：\n计算两个向量夹角的余弦值，结果在-1到1之间\n夹角越小，余弦值越接近1，说明两个向量方向越一致，语义越相近\n它只看方向，不看长度，所以特别适合文本语义匹配（因为我们关心的是\u0026quot;意思像不像\u0026quot;，不关心\u0026quot;文本长不长\u0026quot;）\n欧式距离：\n就是两个点之间的直线距离，距离越小说明越相似\n它既看方向也看长度，适合需要考虑数值大小的场景\n在文本检索中，余弦相似度用得更多一些。\n召回阶段的特点是：速度快、成本低，但精度有限。\n你可以把它类比成HR筛简历——先按关键词从1000份简历中快速捞出10份\u0026quot;看起来还行\u0026quot;的，至于这10份里到底谁最合适，需要下一步来细看。\n第二步：重排——精挑细选，优中选优 召回拿到了10个片段，但这里面可能有些是\u0026quot;擦边球\u0026quot;——跟问题沾点边但不太相关。我们需要进一步精筛。\n这时候登场的是重排模型（Cross Encoder）。\n它的工作方式和召回阶段不一样：\n召回阶段是把问题和片段分别转成向量，然后算向量之间的距离（所以叫Bi-Encoder，双编码器）\n重排阶段是把问题和片段拼在一起输入模型，让模型直接判断\u0026quot;这两段话到底有多相关\u0026quot;（所以叫Cross Encoder，交叉编码器）\n为什么Cross Encoder更准？因为它能同时\u0026quot;看到\u0026quot;问题和片段的完整内容，可以捕捉到更细微的语义关系。代价是——速度更慢。\n这就像HR面试：筛简历可以很快（看关键词就行），但面试必须一个个来（要深入了解每个人）。\n所以我们不会用Cross Encoder去处理整个知识库的所有片段（太慢了），而是只对召回阶段筛出来的10个片段做重排，从中挑出Top K个（比如3个）最相关的。\n你可能会问：为什么不直接召回3个，省掉重排这一步？\n好问题！原因在于：\n召回用的向量相似度虽然快，但精度有限，直接取Top 3很可能漏掉真正最相关的片段\n重排用的Cross Encoder虽然准，但太慢，不能对所有片段都做\n两者结合，先用快但粗的方法广撒网，再用慢但准的方法精挑细选，这是效果和效率的最佳平衡\n第三步：生成——让大模型组织答案 现在我们手里有了3个最相关的知识片段，最后一步就是把它们和用户的问题一起交给大模型，让它生成最终的回答。\n实际发给大模型的内容大概是这样的（简化版）：\n请根据以下参考资料回答用户的问题。 参考资料： 1. 产品A支持7天无理由退货，拆封后的电子产品除外... 2. 退货流程：登录官网→我的订单→申请退货... 3. 退货运费由买家承担，质量问题除外... 用户问题：产品A怎么退货？ 大模型拿到这些信息后，就能基于真实的知识内容来组织语言、生成回答，而不是凭自己的\u0026quot;想象\u0026quot;瞎编。\n这一步的好处非常明显：\n减少幻觉：答案有据可依，模型不容易\u0026quot;编故事\u0026quot;\n成本更低：只需要发送3个相关片段，而不是整本手册，token用量大幅减少\n速度更快：输入内容少了，模型推理速度自然更快\n准确率更高：信息聚焦，模型不用在海量文本里\u0026quot;大海捞针\u0026quot;\n串起来看：一个完整的例子 📷 [图片 token=PCSIblHswog8lbxIKzycTygZnUc（未能下载，见飞书原文）]\n假设你搭建了一个公司产品的智能客服，整个RAG流程是这样的：\n提问前（只需要做一次）：\n把500页产品手册分片成300个知识片段\n用Embedding模型把每个片段转成向量\n把片段文本和向量一起存入向量数据库\n用户提问时（每次提问都走一遍）：\n用户问：\u0026ldquo;产品A怎么退货？\u0026rdquo;\n召回：问题转成向量 → 向量数据库找出10个最相似的片段\n重排：Cross Encoder从10个中精选出3个最相关的\n生成：3个片段 + 用户问题 → 大模型 → 输出准确的退货指南\n整个过程对用户来说就是\u0026quot;问了一个问题，得到了一个靠谱的答案\u0026quot;，但背后其实经历了 分片 → Embedding → 存储 → 召回 → 重排 → 生成 这六个环节。\n总结 最后来回顾一下RAG的核心思路：RAG的本质就是：别让AI硬背，让它\u0026quot;开卷考试\u0026quot;。\n提问前，把你的知识文档加工成一个\u0026quot;可检索的题库\u0026quot;（分片 → 向量化 → 存储）\n提问后，先从题库里找到最相关的几道\u0026quot;参考答案\u0026quot;，再让AI基于参考答案来组织回答（召回 → 重排 → 生成）\n这样一来，AI既能回答你领域内的专业问题，又不会胡说八道，还能控制成本和速度。无论你是要搭企业知识库、智能客服，还是产品问答助手，RAG都是你绕不开的核心技术。\n希望这篇文章能让你对RAG有一个清晰的全局认知。有了这个框架，后续深入到每个环节的优化细节时，你就不会迷路了。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9ARAG%E5%85%A8%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习\u0026lt;mention-doc token=\u0026ldquo;MwVHwX0SeiPRD9ktqIscZiQHnef\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 架构设计：RAG全流程解析\u0026lt;/mention-","title":"架构设计：RAG全流程解析"},{"content":" [!WARNING] 可以在本文看完后，去旧文档看看以前的评论，也值得学习[架构设计：ReAct设计模式核心原理](/oncall/智能 OnCall Agent 项目/旧文档备份/架构设计：ReAct设计模式核心原理/)\nReAct 设计模式：让 AI 学会「边想边做」 大家好，今天咱们来聊一个 AI Agent 领域非常核心的概念——ReAct。\n如果你正在学习 AI Agent、对话机器人，或者想搞明白 ChatGPT 是怎么调用工具的，那这篇文章你一定要看完。我会从最基础的地方讲起，一步步带你搞懂 ReAct 到底是什么、怎么实现的、以及它为什么这么重要。\n📷 [图片 token=H1vQbmEzVoEeRix7lbJcplvvnjs（未能下载，见飞书原文）]\n一、ReAct 到底是什么？ 📷 [图片 token=Kki3bkgrIoMy4kxISBWctzC1nwc（未能下载，见飞书原文）]\n先说结论：ReAct = Reasoning（推理）+ Acting（行动）。\n一句话概括：让 AI 像人一样，边想边做，边做边调整。\n你可能会想：AI 不是直接就能回答问题吗，为什么还需要「边想边做」？\n好，我举个例子你就明白了。\n一个 AI 回答不了的问题 假设你问 AI：\n\u0026ldquo;地球和火星的质量加起来是多少？\u0026rdquo;\n这个问题看起来简单，但 AI 其实很为难——它的训练数据里未必记住了精确的行星质量数值，就算记住了也可能不准确。更关键的是，它不会做精确计算，大语言模型本质上是在\u0026quot;预测下一个词\u0026quot;，不是在算数学题。\n那怎么办？\n如果是你，你会怎么做？你大概率会这样：\n想一想：我需要地球的质量和火星的质量，然后把它们加起来\n查一查：打开搜索引擎，先查地球质量 → 得到 5.972 × 10²⁴ kg\n再查一下：继续查火星质量 → 得到 6.42 × 10²³ kg\n算一算：掏出计算器，5.972e24 + 6.42e23 = 6.614e24\n回答：加起来大概是 6.614 × 10²⁴ kg\n看到没？你作为一个人类，在解决这个问题的时候，经历了一个 \u0026ldquo;想→做→看结果→再想→再做\u0026rdquo; 的循环过程。\nReAct 就是让 AI 模仿你这个过程。\n用做菜来类比 还可以换个更生活化的例子。想象你第一次做红烧肉：\n思考：\u0026ldquo;我得先知道红烧肉怎么做，需要什么调料、什么火候……\u0026rdquo;\n行动：打开手机，刷抖音搜\u0026quot;红烧肉教程\u0026quot;\n观察：教程说要炒糖色，用冰糖最好\n再思考：\u0026ldquo;可是家里只有白糖啊，白糖能代替吗？我再搜搜看\u0026rdquo;\n再行动：继续搜索\u0026quot;白糖代替冰糖炒糖色\u0026quot;\n再观察：搜索结果说可以，但颜色会稍浅\n结论：好的，那我就用白糖来做\n这个\u0026quot;想→做→看→再想\u0026quot;的循环，就是 ReAct 的精髓。\n正式定义 用稍微正式一点的话来说，ReAct 是一个思考→行动→观察→再思考的闭环机制：\n步骤 做什么 举个例子 Thought（思考） 分析问题，决定下一步该做什么 \u0026ldquo;我需要先查地球的质量\u0026rdquo; Action（行动） 调用外部工具获取信息 调用「查星球质量」工具，输入\u0026quot;地球\u0026quot; Observation（观察） 查看工具返回的结果 工具返回：5.972 × 10²⁴ kg 循环 / 结束 信息够了就回答，不够就继续循环 \u0026ldquo;还缺火星的质量，继续查\u0026rdquo; 这个循环会一直进行，直到 AI 认为\u0026quot;信息够了，我可以回答用户了\u0026quot;，才会跳出循环给出最终答案。\n二、最早的 ReAct 是怎么实现的？（古法手搓版） 了解了 ReAct 的思想，你可能会好奇：代码层面到底是怎么做到的？\n最早期的 ReAct 实现，说实话，有点\u0026quot;原始\u0026quot;——靠的是 Prompt 工程 + 字符串解析。\n我给你拆解一下。\n📷 [图片 token=UgBMb8Q0koLIsYx6cxIcn2MQnXe（未能下载，见飞书原文）]\n第一步：用 Prompt 告诉 AI \u0026ldquo;你该怎么输出\u0026rdquo; 核心思路是：在 System Prompt 里严格规定 AI 的输出格式，让它必须按照 Thought → Action → PAUSE 的套路来回复。\nSystem Prompt 大概长这样：\n你是一个 ReAct 代理，遵循 思考→行动→暂停→观察 的循环来解决问题。 工作流程： 1. Thought：描述你的推理过程 2. Action：执行一个动作（格式：Action: 工具名: 输入） 3. PAUSE：停下来，等待工具返回结果 4. Observation：分析工具返回的结果 可用工具： - calculation：数学计算（如 \u0026#34;5*7/4\u0026#34;） - planet_mass：查询行星质量（如 \u0026#34;Mars\u0026#34;） 看到了吧？我们是在用自然语言\u0026quot;约束\u0026quot;AI的行为——你必须按这个格式输出，不能乱来。\n第二步：代码解析 AI 的输出，调用对应工具 AI 收到这个 Prompt 后，它会乖乖按格式输出，比如：\nThought: 我需要地球和火星的质量，先查地球的。 Action: planet_mass: Earth PAUSE 然后我们的代码要做的事情就是：用正则表达式去解析这段文本，把 Action: planet_mass: Earth 拆出来，知道要调用 planet_mass 这个函数，参数是 Earth。\n调用完函数拿到结果后，我们把结果包装成 Observation 塞回给 AI：\nObservation: Earth has a mass of 5.972 × 10^24 kg 然后 AI 继续思考、继续行动……如此循环，直到它不再输出 Action，而是直接给出 Answer，循环就结束了。\n完整执行日志 咱们来看一个完整的例子，问题是：\u0026ldquo;地球和火星的质量加起来是多少？\u0026rdquo;\nQuestion: What is the mass of Earth plus Mars? -------- step 1 -------- Thought: 我需要地球和火星的质量，先查地球的。 Action: planet_mass: Earth PAUSE → [代码解析出工具名 planet_mass，参数 Earth，执行后返回结果] Observation: Earth has a mass of 5.972 × 10^24 kg -------- step 2 -------- Thought: 有了地球质量，还缺火星的，继续查。 Action: planet_mass: Mars PAUSE → [代码解析出工具名 planet_mass，参数 Mars，执行后返回结果] Observation: Mars has a mass of 6.4171 × 10^23 kg -------- step 3 -------- Thought: 两个质量都有了，需要计算它们的和。 Action: calculation: 5.972e24 + 6.4171e23 PAUSE → [代码解析出工具名 calculation，参数为算式，执行后返回结果] Observation: 5.972e24 + 6.4171e23 = 6.614e24 -------- step 4 -------- Answer: 地球和火星的质量加起来约为 6.614 × 10²⁴ kg。 4 次循环，3 次工具调用，最终给出答案。整个过程 AI 一直在\u0026quot;想→做→看→再想\u0026quot;。\n用伪代码梳理流程 def query(question): messages = [system_prompt, question] # 初始化对话 for i in range(max_turns): # 最多循环 N 轮 result = call_llm(messages) # 1. 把 prompt 发给 AI if \u0026#34;Action:\u0026#34; in result: # 2. AI 输出里有 Action 吗？ tool, input = parse(result) # 有 → 正则解析出工具名和参数 observation = run_tool(tool, input) # 3. 执行工具，拿到结果 messages.append(f\u0026#34;Observation: {observation}\u0026#34;) # 4. 结果塞回去 else: return result # 5. 没有 Action → 最终答案，结束 核心就这么几行代码，本质上就是一个 while 循环 + 字符串解析。\n古法 ReAct 的问题 这种方式虽然能跑通，但有个很大的硬伤：一切都靠字符串。\n工具的定义是字符串写在 Prompt 里的，AI 的调用格式也是字符串，我们解析也是靠正则匹配字符串。\n这就带来几个问题：\n格式脆弱：AI 有时候不完全按格式输出，比如多了个空格、换了个冒号，正则就匹配不上了\n复杂参数搞不定：如果工具的输入是一个嵌套的 JSON 对象（比如 Map 套 Map），用字符串来描述和解析就是噩梦\n错误处理全靠自己：AI 输出了一个不存在的工具名怎么办？格式错了怎么办？全得手动写异常处理\n开发成本高：每个项目都要自己设计格式、写解析逻辑、处理各种边界情况\n说白了，古法 ReAct 就像是在用胶水和绳子把零件绑在一起——能用，但不够稳，也不够优雅。\n三、现代 ReAct 是怎么实现的？（Function Call 版） 大模型厂商也意识到了古法 ReAct 的痛点，于是推出了一个重要特性：Function Call（函数调用）。\n简单来说，Function Call 就是把\u0026quot;古法 ReAct 的手工活\u0026quot;标准化了——用 JSON 来统一工具的定义和调用格式。\n📷 [图片 token=HSemb6XrVo8VjwxVSygcwMXpnvb（未能下载，见飞书原文）]\n什么是 Function Call？ 你可以把 Function Call 理解为大模型厂商制定的一套\u0026quot;协议\u0026quot;：\n工具怎么描述：用标准的 JSON 格式来定义工具的名字、功能、参数类型\nAI 怎么调用工具：AI 不再输出自然语言字符串，而是直接返回结构化的 JSON\n结果怎么回传：工具的返回结果也按照规定的格式传回给 AI\n举个例子，以前古法 ReAct 里，工具描述是这样的（写在 Prompt 文本里）：\n可用工具： - calculation：数学计算（如 \u0026#34;5*7/4\u0026#34;） - planet_mass：查询行星质量（如 \u0026#34;Mars\u0026#34;） 现在用 Function Call，工具描述变成了标准的 JSON：\n{ \u0026#34;name\u0026#34;: \u0026#34;planet_mass\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询太阳系行星的质量\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;planet\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;行星名称，如 Earth、Mars\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;planet\u0026#34;] } } AI 要调用工具时，也不再输出 Action: planet_mass: Earth 这样的字符串了，而是直接返回：\n{ \u0026#34;tool_calls\u0026#34;: [{ \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;planet_mass\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{\\\u0026#34;planet\\\u0026#34;: \\\u0026#34;Earth\\\u0026#34;}\u0026#34; } }] } 看到区别了吗？从\u0026quot;人约定的文本格式\u0026quot;变成了\u0026quot;机器可以直接解析的 JSON 结构\u0026quot;。\n现代 ReAct 的执行流程 📷 [图片 token=AAIubwuZ8omrxuxBQ4BcMbNdnjg（未能下载，见飞书原文）]\n用 Function Call 之后，ReAct 的循环变得简洁多了：\n1. 把工具的 JSON 描述 + 用户问题 一起发给大模型 （不需要在 Prompt 里写 \u0026#34;Action:xxx:xxx\u0026#34; 这种格式说明了） 2. 大模型自己判断要不要调用工具 → 需要调用 → 返回标准 JSON 格式的 tool_calls → 不需要 → 直接返回文本答案 3. 我们解析 JSON，执行对应的工具函数，拿到结果 4. 把工具结果按规定格式加到对话里，开始下一轮循环 5. 直到大模型不再返回 tool_calls → 循环结束，输出最终答案 你会发现，核心循环的思路和古法 ReAct 完全一样——还是\u0026quot;想→做→看→再想\u0026quot;。变的只是交互的格式，从\u0026quot;手搓字符串\u0026quot;变成了\u0026quot;标准化 JSON\u0026quot;。\n为什么 Function Call 这么好用？ 这里多解释一下。可能有同学会问：不就是换了个格式吗，有那么大区别吗？\n区别非常大，我举个类比：\n你和同事沟通工作，之前你们约定用微信语音留言说任务，比如\u0026quot;帮我查下上周的销售数据，按地区分组，要 Excel 格式\u0026quot;。对方得听完语音，自己理解、自己整理，中间但凡听错一个字就可能搞岔了。\n后来公司上了一个项目管理工具，你只需要填一个表单：任务类型=数据查询，数据范围=上周，分组方式=地区，输出格式=Excel。结构清晰，不会误解。\nFunction Call 就相当于那个项目管理工具的表单——把原本靠\u0026quot;人说人听\u0026quot;的沟通，变成了结构化的、机器可理解的标准协议。\n而且因为格式标准化了，大模型厂商可以针对性地训练 AI 模型，让它更擅长生成正确的 JSON 调用格式。如果 AI 偶尔生成了格式错误的 JSON，服务端还能自动检测并重试，不需要你在代码里写一堆错误处理逻辑。\n四、古法 vs 现代，到底差在哪？ 说了这么多，咱们来做一个清晰的对比：\n对比维度 古法 ReAct 现代 ReAct 工具调用格式 自然语言字符串（如 Action: planet_mass: Mars） 标准化 JSON 结构（通过 Function Call 规范） 工具描述方式 依赖用户自定义 Prompt 中的自然语言说明 工具信息标准化（如 JSON 对象定义工具名、参数、功能） 解析方式 正则表达式解析字符串（易出错，依赖格式严格匹配） 结构化 JSON 解析（大模型/框架原生支持，可靠性高） 复杂数据处理 困难（如嵌套结构的输入输出，字符串解析易混乱） 可靠（JSON 天然支持复杂参数类型，如嵌套对象） 错误处理 需手动实现（如未知 Action 抛出异常） 框架自动支持（如工具调用格式错误时，大模型/框架自动重试） 依赖技术 纯 Prompt 工程 + 字符串处理 大模型 Function Call 功能（厂商官方支持） 格式规范来源 用户自定义 Prompt 中的格式约束 大模型厂商定义的标准化协议 实现复杂度 高（需手动处理字符串生成、解析、异常捕获） 低（框架封装了格式处理、工具调用逻辑） 一句话总结：核心差异就是从\u0026quot;人工字符串约定\u0026quot;升级到了\u0026quot;机器可理解的结构化协议\u0026quot;。\n思想没变（都是 ReAct 循环），但实现方式的可靠性和开发效率，提升了一个量级。\n五、ReAct 的好处是什么？ 最后我们来聊聊，ReAct 模式到底给 AI 带来了什么好处。\n📷 [图片 token=HE52b4Xhcolo59xE4Teczhppneb（未能下载，见飞书原文）]\n1. 让 AI 具备了\u0026quot;使用工具\u0026quot;的能力 大语言模型最大的短板是什么？它的知识是有截止日期的，它也不擅长精确计算。\n但有了 ReAct，AI 就可以像人一样去调用外部工具——查数据库、调 API、搜索引擎、计算器……这就大大扩展了 AI 的能力边界。\nAI 不再是一个\u0026quot;只会聊天的嘴巴\u0026quot;，而变成了一个能动手干活的助手。\n2. 让 AI 的回答更准确、更可靠 没有 ReAct 的时候，AI 遇到不确定的问题只能\u0026quot;硬编\u0026quot;——也就是我们常说的\u0026quot;AI 幻觉\u0026quot;，看着像那么回事，但其实是瞎说的。\n有了 ReAct，AI 会先想\u0026quot;这个问题我不确定，我得查一下\u0026quot;，然后去调用工具获取真实数据，再基于真实数据来回答。从\u0026quot;凭记忆猜\u0026quot;变成了\u0026quot;查了再说\u0026quot;，准确率自然大幅提升。\n3. 解题过程透明可追踪 ReAct 的每一步——思考了什么、调了什么工具、拿到了什么结果——都是明明白白写在对话记录里的。\n这意味着如果 AI 给了一个错误的答案，你可以回溯整个过程，找到到底是哪一步出了问题：是思考方向不对？是调用了错误的工具？还是工具本身返回的数据有误？\n这种可追踪性对于调试和优化 AI Agent 来说非常重要。\n4. 具备了处理复杂多步骤任务的能力 有些问题不是查一次就能解决的，需要多步骤、多工具协作。比如：\n\u0026ldquo;帮我查一下北京今天的天气，如果温度低于 10 度，就发一条提醒消息给我\u0026rdquo;\n这个任务需要：先查天气 → 判断温度 → 条件满足则发消息。这就是一个典型的多步骤任务。\nReAct 的循环机制天然支持这种场景——每一轮循环解决一个子问题，观察结果后再决定下一步做什么，直到整个任务完成。\n5. 它是 AI Agent 的基石 现在市面上你看到的各种 AI Agent 产品——能帮你写代码、做数据分析、自动化办公的那些——底层几乎都是 ReAct 模式在驱动。\n可以说，ReAct 是 AI 从\u0026quot;聊天机器人\u0026quot;进化为\u0026quot;智能助手\u0026quot;的关键技术跳板。学懂了 ReAct，你就理解了 AI Agent 的核心运作原理。\n总结 最后，让我们来做一个总复盘：\nReAct 是什么？ → Reasoning + Acting，让 AI 学会\u0026quot;边想边做\u0026quot;的解题方法论。核心是 思考→行动→观察→再思考 的闭环循环。\n古法 ReAct 怎么实现的？ → 用 Prompt 规范 AI 的输出格式（Thought→Action→PAUSE），然后代码用正则表达式解析字符串来调用工具。能用，但脆弱。\n现代 ReAct 怎么实现的？ → 借助大模型厂商提供的 Function Call 能力，用标准 JSON 来定义工具和解析调用。更稳定、更可靠、开发成本更低。\n古法和现代的核心区别？ → 从\u0026quot;人工字符串约定\u0026quot;升级为\u0026quot;机器可理解的结构化协议\u0026quot;，思想没变，实现方式脱胎换骨。\nReAct 为什么重要？ → 它让 AI 有了调用工具的能力、更准确的回答、可追踪的推理过程，是 AI Agent 的核心技术支柱。\n如果你是刚入门 AI Agent 开发的同学，ReAct 是你必须要搞懂的第一个核心概念。理解了它，后面学什么框架、什么架构，都会轻松很多。\n希望这篇文章能帮到你，咱们下篇见！\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AReAct%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/","summary":"!WARNING  可以在本文看完后，去旧文档看看以前的评论，也值得学习\u0026lt;mention-doc token=\u0026ldquo;MJX3wCyaViI83NkTp9EccyTMnkd\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 架构设计：ReAct设计模式核心原理\u0026lt;/men","title":"架构设计：ReAct设计模式核心原理"},{"content":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。\n核心代码目录：SuperBizAgent/internal/ai/tools\n当前时间查询工具 那么就可以直接按照eino框架的规范，组装接口：\n第一个参数是toolName，用于表示工具名\n第二个参数是toolDesc，用于告诉大模型这个工具的功能\n第三个参数是一个函数，里面写核心逻辑即可\n// NewGetCurrentTimeTool 创建获取当前时间的工具 func NewGetCurrentTimeTool() tool.InvokableTool { t, err := utils.InferOptionableTool( \u0026#34;get_current_time\u0026#34;, \u0026#34;Get current system time in multiple formats. Returns the current time in seconds (Unix timestamp), milliseconds, and microseconds. Use this tool when you need to retrieve current system time for logging, timing operations, or timestamping events.\u0026#34;, func(ctx context.Context, input *GetCurrentTimeInput, opts ...tool.Option) (output string, err error) { // 获取当前时间 now := time.Now() // 计算各种时间格式 seconds := now.Unix() // 秒 milliseconds := now.UnixMilli() // 毫秒 microseconds := now.UnixMicro() // 微秒 timestamp := now.Format(\u0026#34;2006-01-02 15:04:05.000000\u0026#34;) // 可读格式 // 构建输出 timeOutput := GetCurrentTimeOutput{ Success: true, Seconds: seconds, Milliseconds: milliseconds, Microseconds: microseconds, Timestamp: timestamp, Message: \u0026#34;Current time retrieved successfully\u0026#34;, } // 转换为JSON jsonBytes, err := json.MarshalIndent(timeOutput, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) if err != nil { log.Printf(\u0026#34;Error marshaling result to JSON: %v\u0026#34;, err) return \u0026#34;\u0026#34;, err } return string(jsonBytes), nil }) if err != nil { log.Fatal(err) } return t } 函数的入参和出参，都可以用jsonschema的description来描述参数含义。如此一来，我们的工具和工具描述就写好了\n// GetCurrentTimeInput 获取当前时间的输入参数（无需输入） type GetCurrentTimeInput struct { // 无需输入参数 } // GetCurrentTimeOutput 获取当前时间的输出结果 type GetCurrentTimeOutput struct { Success bool `json:\u0026#34;success\u0026#34; jsonschema:\u0026#34;description=Indicates whether the time retrieval was successful\u0026#34;` Seconds int64 `json:\u0026#34;seconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in seconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Milliseconds int64 `json:\u0026#34;milliseconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in milliseconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Microseconds int64 `json:\u0026#34;microseconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in microseconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Timestamp string `json:\u0026#34;timestamp\u0026#34; jsonschema:\u0026#34;description=Human-readable timestamp in format \u0026#39;YYYY-MM-DD HH:MM:SS.microseconds\u0026#39;\u0026#34;` Message string `json:\u0026#34;message\u0026#34; jsonschema:\u0026#34;description=Status message describing the operation result\u0026#34;` } 腾讯云日志MCP工具 通过 MCP Server 查询日志服务 CLS 中存储的日志数据，以实现大模型平台/工具与日志数据的结合。例如使用自然语言查询日志，降低日志查询复杂度 https://cloud.tencent.com/developer/mcp/server/11710\nMCP配置：[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n首先我们创建一个 SSE MCP 客户端。创建后进行初始化，最后调用GetTools获取所有可用的工具即可\nimport ( \u0026#34;context\u0026#34; e_mcp \u0026#34;github.com/cloudwego/eino-ext/components/tool/mcp\u0026#34; \u0026#34;github.com/cloudwego/eino/components/tool\u0026#34; \u0026#34;github.com/mark3labs/mcp-go/client\u0026#34; \u0026#34;github.com/mark3labs/mcp-go/mcp\u0026#34; ) func GetLogMcpTool() ([]tool.BaseTool, error) { ctx := context.Background() // 1. 创建客户端 cli, err := client.NewSSEMCPClient(\u0026#34;https://mcp-api.tencent-cloud.com/sse/ac4XXXXXX\u0026#34;) if err != nil { return []tool.BaseTool{}, err } err = cli.Start(ctx) if err != nil { return []tool.BaseTool{}, err } // 2. 协商协议 initRequest := mcp.InitializeRequest{} initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initRequest.Params.ClientInfo = mcp.Implementation{ Name: \u0026#34;example-client\u0026#34;, Version: \u0026#34;1.0.0\u0026#34;, } if _, err = cli.Initialize(ctx, initRequest); err != nil { return []tool.BaseTool{}, err } // 3. 获取工具 mcpTools, err := e_mcp.GetTools(ctx, \u0026amp;e_mcp.Config{Cli: cli}) if err != nil { return []tool.BaseTool{}, err } return mcpTools, nil } ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AD%E7%AB%A0%EF%BD%9CTool%20%E5%92%8C%20MCP%20%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ATool%20%E5%92%8C%20MCP%20%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%98%28Go%29/","summary":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。 核心代码目录： SuperBizAgent/internal/ai/tools  当前时间查询工具 那么就可以直接","title":"源码分析：Tool 和 MCP 代码实战(Go)"},{"content":"OncallAgent 的权限模型从一个朴素但明确的规则开始：当前已认证用户的 ID 同时充当 tenant scope，直到未来引入独立的组织 tenant。这个决定贯穿 bearer session、FastAPI 依赖、SQLite 查询、Milvus filter、后台任务与工具审计。它不是在记录写入后再补的一列，而是每次 list、get、create、update 和 delete 都必须显式传递的访问条件。\n📷 [图片 token=BvK4bjFYJoSbh1xlgptcKIyinCc（未能下载，见飞书原文）]\n认证回答“调用者是谁”，授权回答“这个调用者能否访问目标资源”。OncallAgent 对两者使用不同错误语义：token 缺失、未知或被撤销返回统一 401；有效用户访问其他 owner 的会话、文档、索引任务或诊断对象返回统一 403。对 Agent 系统而言，这个区分尤其重要，因为越权请求必须在模型、工具或向量搜索启动前被拒绝，不能依靠生成后的内容过滤补救。\n📷 [图片 token=UBAwbMKDnobpAcxweiRcdiuEnrb（未能下载，见飞书原文）]\n浏览器保存 bearer token 并提供路由守卫，但安全根仍在后端。前端状态可以被清除、绕过或手工修改，真正的数据隔离来自 Depends(_current_user) 解析的 UserRecord、owner-scoped Repository 方法和 tenant-scoped Milvus 表达式。阅读本主题时，应沿着身份、业务对象和向量对象三条链同时核对。\n📷 [图片 token=De6WbNcx3oVHXzx7cv8cBTIYn7f（未能下载，见飞书原文）]\n学习目标 追踪注册、登录、当前用户查询、登出和 session 撤销的完整生命周期。\n理解密码哈希、token 随机值与 token 哈希分别保存在哪里。\n识别 401 与 403 的运行时触发点，以及跨 tenant 请求为何在 Agent 前被拒绝。\n掌握 SQLite owner 条件、Milvus tenant 条件与前端清理状态的互补关系。\n能审查新增 Repository 或路由是否遗漏 owner/tenant scope。\n功能入口与完整调用链 注册从 apps/frontend/src/authClient.ts 的 register 开始，发送共享 RegisterRequest 到 POST /auth/register。apps/backend/src/super_ai/api/app.py 的 register 路由调用 AuthService.register；服务把邮箱去空白并转小写，校验显示名、邮箱格式和至少八位密码，用 PasswordHash.recommended 生成密码哈希，再调用 AuthRepository.create_user。成功后生成 32 字节 URL-safe 随机 token，将 SHA-256 token hash 与 session 写入 SQLite，HTTP 响应只返回用户资料、原始 access token 和 bearer 类型。\n📷 [图片 token=KgrObw9e8oXxKxxC6iLcbASQnhI（未能下载，见飞书原文）]\n登录链路调用 AuthService.login。已知邮箱用保存的 password hash 校验；未知邮箱仍会对固定的 Argon2id dummy hash 执行一次验证，再返回与密码错误相同的 AUTH_INVALID_CREDENTIALS。这个实现减少了通过错误文案和显著不同代码路径枚举账号的风险。数据库不保存原始密码，也不保存原始 bearer token。\n📷 [图片 token=WF7CbG5U7oa5fdx6JBvcKcdOnJs（未能下载，见飞书原文）]\n受保护请求使用 FastAPI 的 HTTPBearer(auto_error=False)。_bearer_token 检查 scheme，_current_user 调用 AuthService.authenticate_token：先 hash 请求 token，查询 AuthSessionModel.token_hash，检查 revoked_at，读取 user，并更新 session 的 last_seen_at。随后路由将 user.id 传入下游。登出调用 revoke_session 写入撤销时间；同一个 token 此后由 AUTH_SESSION_REVOKED 拒绝。\n📷 [图片 token=Hd9AbMTSlo8BcexkqR1c49TKnRh（未能下载，见飞书原文）]\n注册或登录 → AuthService → SQLiteAuthRepository → users + auth_sessions → 返回一次原始 bearer token 受保护请求 → _bearer_token → _current_user → SHA-256(token) 查询 session → UserRecord.id 作为 owner_user_id 与 tenant_id → Repository / Milvus / Agent 工具 📷 [图片 token=WQpcbKhvrokYIdxvwdncoIBmnuc（未能下载，见飞书原文）]\n以流式聊天为例，stream_chat_message 收到当前用户后，先调用 repositories.chat.get_session(owner_user_id=user.id, session_id=...)。另一个用户的 session ID 不能匹配 owner 条件，路由抛出 AUTH_FORBIDDEN；ChatStreamingService 和 Agent runner 都不会被创建执行。知识文档、索引任务、诊断、后台任务、反馈与 MCP 连接采用相同模式。\n📷 [图片 token=PH1ubgqRjoOw7uxV1THceIUZnkd（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 packages/api-contracts/src/auth.ts RegisterRequest、LoginRequest、AuthUser、AuthTokenResponse 定义前后端共享认证 DTO，不暴露密码哈希或 session 内部字段。 apps/backend/src/super_ai/auth/service.py AuthService、normalize_email、hash_token 校验凭证、哈希密码、签发和验证可撤销 session。 apps/backend/src/super_ai/auth/repositories.py AuthRepository、UserRecord、AuthSessionRecord 隔离认证业务逻辑与 SQLAlchemy 存储细节。 apps/backend/src/super_ai/auth/sqlite.py SQLiteAuthRepository 持久化用户、token hash、last seen 与 revoked 状态。 apps/backend/src/super_ai/api/app.py _current_user、_bearer_token、认证路由 建立统一认证依赖，并把 user ID 传入每个受保护处理器。 apps/backend/src/super_ai/memory/models.py UserModel、AuthSessionModel、各类 owner_user_id 定义持久化身份与 owner-scoped 业务模型及索引。 apps/backend/src/super_ai/memory/sqlite.py SQLiteChatMemoryRepository、_require_document、_find_diagnostic_task 在查询和变更中同时匹配业务 ID 与 owner_user_id。 apps/backend/src/super_ai/memory/vector_scope.py build_vector_chunk_metadata、build_milvus_tenant_filter 生成向量权限 metadata 与 Milvus tenant/知识库过滤表达式。 apps/frontend/src/stores/auth.ts useAuthStore、createAuthRouteAccess 恢复当前用户、管理登录态，并在登出或失效时清理受保护 store。 apps/frontend/src/router/index.ts createAppRouter 为工作区路由提供前端访问守卫，但不替代后端授权。 📷 [图片 token=BEhqbfGARoXTjQxcm5ycWDVenie（未能下载，见飞书原文）]\n代码调用流程图 认证解决“你是谁”，owner scope 解决“你能访问什么”。两者必须连续发生，不能只检查 bearer token 就直接读取业务对象。\n📷 [图片 token=Kqigb7CQuoWwJDxU34Gcqe4FnLb（未能下载，见飞书原文）]\n关键实现拆解 凭证和 session 的存储语义 UserModel.password_hash 是最多 512 字符的哈希字段。默认 PasswordHash.recommended 在当前测试中产生 Argon2 哈希；服务从不把密码或哈希放进 _user_payload。注册重复邮箱由数据库唯一约束触发 IntegrityError，服务转换成 BUSINESS_CONFLICT，同样不回显密码数据。\n📷 [图片 token=C0XsbMTFfoF4eAxxYiWcqbIEnjd（未能下载，见飞书原文）]\nsession token 的原始值只在签发结果中返回给客户端，数据库保存 hash_token 产生的 SHA-256。AuthSessionModel 还保存 user 外键、创建、最后访问和撤销时间。SHA-256 在这里不是密码哈希算法，而是高熵随机 token 的查找标识；密码仍需使用抗暴力破解的专用哈希。登出不是删除浏览器字符串就结束，而是服务端持久化撤销状态。\n📷 [图片 token=BLf5bcKVCoLiVQxOwlpcUuNFnxe（未能下载，见飞书原文）]\n看什么：认证服务先把 bearer 转成稳定 hash 查询 session，再检查撤销状态；原始 token 不作为数据库查找值。\n📷 [图片 token=KGNeb0wjaoB8PxxX0kacV366nOf（未能下载，见飞书原文）]\nasync def authenticate_token(self, token: str) -\u0026gt; UserRecord: if not token.strip(): raise AuthError(\u0026#34;AUTH_UNAUTHENTICATED\u0026#34;, \u0026#34;Authentication is required.\u0026#34;) # 1. 数据库查询键是 token hash，原始 bearer 不持久化。 token_hash = hash_token(token) session = await self._repository.find_session_by_token_hash(token_hash) if session is None: raise AuthError(\u0026#34;AUTH_UNAUTHENTICATED\u0026#34;, \u0026#34;Authentication is required.\u0026#34;) # 2. 被注销的 session 即使 token 正确也不能继续使用。 if session.revoked_at is not None: raise AuthError(\u0026#34;AUTH_SESSION_REVOKED\u0026#34;, \u0026#34;The authentication session has been revoked.\u0026#34;) user = await self._repository.find_user_by_id(session.user_id) if user is None: raise AuthError(\u0026#34;AUTH_UNAUTHENTICATED\u0026#34;, \u0026#34;Authentication is required.\u0026#34;) await self._repository.touch_session(session.id, _utc_now()) return user 📷 [图片 token=Hc3WbF6pVoV6StxybPfc9p1onYb（未能下载，见飞书原文）]\n片段证明认证是服务端可撤销状态，而不是只验证 bearer 字符串格式。未知 token 与缺 token 都隐藏为未认证；已撤销 session 有稳定错误码。当前模型没有过期时间判断，因此不能从这段代码推断自动过期、刷新 token 或设备管理已经实现。\n📷 [图片 token=JjM9byylAoVhMsxufP9csKKqnhf（未能下载，见飞书原文）]\n看什么：把“签发、使用、撤销”画成 session 状态机，可以避免把 localStorage 清理误当成服务端登出。\n📷 [图片 token=LEdQbBPN7oFx7wxURNjcNQcFn8e（未能下载，见飞书原文）]\n状态图强调撤销在数据库中生效，所以同一个 token 即使仍留在浏览器也会被拒绝。失败分支不会暴露 session 是否属于哪个用户；密码验证则走独立的 Argon2 边界，不能与 token hash 的用途混淆。\n📷 [图片 token=IZQSbEk13oX0Lbxq4p7cKqg4nab（未能下载，见飞书原文）]\nOwner scope 必须进入查询条件 apps/backend/src/super_ai/memory/repositories.py 的协议方法普遍要求 owner_user_id。例如 chat 的 get_session、document 的 get_document、diagnostic 的 get_task 都不能只收资源 ID。SQLite 实现把 owner 条件写进同一条 SELECT；_require_chat_session、_require_document 等 helper 在找不到组合键时抛出 TenantScopeError。父对象检查也发生在写入前，例如 append message 会先验证同 owner 的 chat session，创建索引任务会先验证同 owner、知识库与文档。\n📷 [图片 token=TYRbbYCuKoR5Zfx1NWQc4cI2npf（未能下载，见飞书原文）]\n这种模式避免了危险的“先按全局 ID 查记录，再在 Python 中比较 owner”。后者容易在异常、日志或并发路径中泄露对象存在性。当前 API 对很多资源把 owner-scoped 未命中映射成 403，因此已认证用户即使猜中其他 tenant 的 ID，也不能读取、更新或删除其内容。\n📷 [图片 token=LxsHb4bqPoTGv7xamyKc8bBsnPh（未能下载，见飞书原文）]\n看什么：聊天流入口在构造服务和 Agent runner 之前，用当前认证用户 ID 与 session ID 组成同一条 Repository 查询。\n📷 [图片 token=KngJbsiCBogzLXxilO5cV9D9nTb（未能下载，见飞书原文）]\n@app.post(\u0026#34;/chat/sessions/{session_id}/messages:stream\u0026#34;) async def stream_chat_message( request: Request, session_id: str, body: StreamChatMessageRequest, user: Annotated[UserRecord, Depends(_current_user)], ) -\u0026gt; StreamingResponse: repositories = _memory_repositories(request) # 1. 查询条件同时携带当前 owner 和 session id。 session = await repositories.chat.get_session( owner_user_id=user.id, session_id=session_id, ) # 2. 越权在 ChatStreamingService 和 Agent runner 之前结束。 if session is None: raise ApiErrorException(\u0026#34;AUTH_FORBIDDEN\u0026#34;) service = ChatStreamingService( repositories=repositories, agent_runner=_chat_agent_runner(request), memory_service=_chat_memory_service(request), ) 📷 [图片 token=C839bhtZRoIUZaxqttRculHDnMh（未能下载，见飞书原文）]\n代码证明跨 tenant 会话不会启动模型、知识检索或 MCP 工具，也不会先泄露会话标题。安全边界依赖 Repository 真正把 owner 写进 SQL；如果某个新实现只在 Python 返回后比较，就会削弱这种“未命中即拒绝”的统一语义。\n📷 [图片 token=DFCnbSuUDot474xmVh6cMgrVnwL（未能下载，见飞书原文）]\n看什么：请求身份如何一路成为数据条件，而不是停留在路由装饰器上。\n📷 [图片 token=ZrY2bbSWto84HfxsyuRcDwognCc（未能下载，见飞书原文）]\n身份解析只建立 tenant scope，具体对象授权仍由每次查询完成。匿名请求在进入用户 Repository 前返回 401；已认证但 owner 不匹配返回 403，这两类失败不能合并成同一种前端恢复动作。\n📷 [图片 token=Y8Nzbwgjro8MD9xqHlNc440unzb（未能下载，见飞书原文）]\n向量隔离是双层的 Milvus chunk 在标量字段和 JSON metadata 中都包含 ownerUserId、tenantId、knowledgeBaseId、documentId 和 chunkId。build_milvus_tenant_filter 生成 tenant 相等且 knowledge base 位于授权集合的表达式；MilvusVectorStore.search_chunks 和 list_chunks 都调用它。授权后的知识库 ID 集合为空时直接返回空结果，完全不连接或查询 Milvus，从结构上阻止无范围搜索。\n📷 [图片 token=MQBUbYrIYoIhszxVcCkcxi5Lnfd（未能下载，见飞书原文）]\n删除也必须带范围。delete_document_chunks 要求 tenant、知识库和文档 ID 都非空，再构造三条件过滤器。文档 API 先确认 owner-scoped document 存在，才调用向量删除和 SQLite 软删除。向量库不是权限真相来源；允许的知识库集合来自当前认证用户和业务边界，Milvus 只执行已经明确的范围。\n📷 [图片 token=Jpv4bkFfRoQERnxV5X3cTAV1nog（未能下载，见飞书原文）]\n看什么：过滤器把 tenant 相等与允许知识库集合合成一个表达式，输入值在拼接前转义。\n📷 [图片 token=Em5xbRTO3oB9y4xs309cdJs3nPg（未能下载，见飞书原文）]\ndef build_milvus_tenant_filter(*, tenant_id: str, knowledge_base_ids: Sequence[str]) -\u0026gt; str: # 1. 每个知识库 ID 先进行 Milvus 字符串转义。 quoted_kb_ids = \u0026#34;,\u0026#34;.join(f\u0026#39;\u0026#34;{_escape_milvus_string(item)}\u0026#34;\u0026#39; for item in knowledge_base_ids) escaped_tenant_id = _escape_milvus_string(tenant_id) # 2. tenant 与授权知识库必须同时满足。 return f\u0026#39;tenantId == \u0026#34;{escaped_tenant_id}\u0026#34; \u0026amp;\u0026amp; knowledgeBaseId in [{quoted_kb_ids}]\u0026#39; 📷 [图片 token=KfBXbyH4YoCw0xxzL4ocsf8Ynab（未能下载，见飞书原文）]\n片段证明 Milvus 查询不是仅按向量相似度运行，而是携带显式范围表达式。它本身不决定哪些知识库可访问，也不处理空集合；调用边界必须先授权并短路空范围，返回结果后检索工具还会再核对 owner、tenant 与 metadata。\n📷 [图片 token=FO78bJvm6oh5OlxAtYacWZeqnjf（未能下载，见飞书原文）]\n看什么：双层隔离不是重复字段堆叠，而是“业务授权生成允许集、向量边界执行过滤、结果再校验”的纵深防御。\n📷 [图片 token=FkZvbrR3UoEqWxxNlCtczB4lnme（未能下载，见飞书原文）]\n📷 [图片 token=HnZNbWAqgoYJxBxsPa5c3zjEnfg（未能下载，见飞书原文）]\n三条分支分别表达权限拒绝、安全空结果和受限查询。任何为了提高召回而在空范围时搜索全 collection 的实现都会破坏隔离；同样，客户端传入 knowledge base ID 不能成为授权来源。\n📷 [图片 token=LYuFb1yY6oWm3VxVA7zcA5imn6I（未能下载，见飞书原文）]\n前端状态清理不是授权，但仍然必要 AUTH_TOKEN_STORAGE_KEY 当前使用浏览器 localStorage。useAuthStore.initialize 在存在 token 时调用 /auth/me 恢复 user；失败则移除 token，并 reset chat、knowledge 与 aiops store。logout 即使后端请求失败，也在 finally 中清除本地 token、用户和受保护状态。这能避免上一位用户的数据残留在同一浏览器界面，但它不能替代后端 owner 过滤。\n📷 [图片 token=YVqxbMZlkohSDixl0x3cXjCxnid（未能下载，见飞书原文）]\n路由守卫等待初始化完成后，再判断 requiresAuth 与 publicOnly。这解决刷新页面时的竞态和视觉跳转。安全审查仍应假定攻击者可以直接调用 API，因为 localStorage、Pinia 和 Vue Router 都处于调用者控制范围。\n📷 [图片 token=CLmlbr2keock1nxmQFRcghHdnuP（未能下载，见飞书原文）]\n看什么：注销把服务端请求放在 try，本地 token 和三个受保护 store 的清理放在 finally。\n📷 [图片 token=SyavbDZo4o7PTCxylh4cjXc5nxd（未能下载，见飞书原文）]\nlogout: (): Promise\u0026lt;void\u0026gt; =\u0026gt; run(async () =\u0026gt; { try { // 1. 尝试让服务端持久化撤销当前 session。 await client.logout(); } finally { // 2. 即使网络失败，也不让旧用户数据继续留在界面。 storage().removeItem(AUTH_TOKEN_STORAGE_KEY); user.value = null; useChatStore().reset(); useKnowledgeStore().reset(); useAiopsStore().reset(); } }), 📷 [图片 token=TPekb3zjTosmtSxVdpqceqHbnDf（未能下载，见飞书原文）]\n代码证明前端在不可用或网络失败时仍会消除可见残留，这是共享浏览器场景的重要隐私边界。它不证明 token 已在服务端撤销：若 logout 请求没有到达后端，旧 token 仍可能有效，因此真正权限判断必须始终在每次 API 请求中重新执行。\n📷 [图片 token=WqzIbRMmSoDSpvx9fpfcrcSSnMa（未能下载，见飞书原文）]\n后台任务和审计同样属于 tenant 数据 异步执行容易成为权限传递的断点。OncallAgent 创建文档索引或 AIOps 诊断时，把当前用户 ID 同时写入业务任务与 BackgroundJobModel.owner_user_id。worker handler 从持久 job 取出资源 ID 后，仍以 owner 调用索引或诊断服务；查询、取消和重试后台任务也要求同一个 owner。这样浏览器请求结束后，权限上下文不会退化为一个无主的全局任务。\n📷 [图片 token=SGHobpW1MoVs7Rxkf2Oc0zGLnDd（未能下载，见飞书原文）]\n工具审计、诊断步骤、证据、报告证据链接和 checkpoint 也保存 owner。它们不仅是“附属日志”，而是可能含用户查询、外部工具摘要和诊断推理的受保护业务数据。读取某个诊断证据链时，不能只验证最外层 task；Repository helper 会把 owner、task ID 与具体 step、report 或 evidence ID 同时放入条件，防止使用另一个任务下的子对象 ID 拼接越权请求。\n📷 [图片 token=G2sMbubLXoUa0Ix9trscJQ0en3e（未能下载，见飞书原文）]\n看什么：后台 job 在入队时就保存 owner，之后查询资源也要求同一 owner，而不是只靠 worker 内存里的请求上下文。\n📷 [图片 token=EBFNba4kSo5XM6xjuo1cHT3DnVg（未能下载，见飞书原文）]\nasync def enqueue( self, *, owner_user_id: str, job_id: str, kind: str, resource_type: str, resource_id: str, payload: JsonDict | None = None, max_attempts: int = 3, timeout_seconds: int = 900, retry_of_job_id: str | None = None, available_at: datetime | None = None, ) -\u0026gt; BackgroundJobRecord: now = utc_now() # 1. owner 与资源标识一起成为持久 job 的业务事实。 row = BackgroundJobModel( id=job_id, owner_user_id=owner_user_id, kind=kind, resource_type=resource_type, resource_id=resource_id, status=\u0026#34;queued\u0026#34;, payload=payload or {}, 📷 [图片 token=W8WMbbdH7oesSLxmXJOcYzu5nuh（未能下载，见飞书原文）]\n片段证明请求结束后 owner 仍随 job 持久化，worker 可以把它继续传给索引或诊断服务。风险边界在 payload：即使记录有 owner，也不能把凭据、完整工具参数或用户正文随意持久化；列表、详情、取消、重试和事件读取仍必须分别按 owner 过滤。\n📷 [图片 token=FXaIbvNgUoUxLIxjWNsc8kMxnDe（未能下载，见飞书原文）]\n看什么：异步链路中 owner 需要跨越业务任务、durable job、worker、工具审计和证据子对象，任何一跳缺失都会形成权限断点。\n📷 [图片 token=HIGTb7nuConFaqxmKH9c47FCngb（未能下载，见飞书原文）]\n📷 [图片 token=ZIarbDBz7omlRpxCqhNcrKrHnxh（未能下载，见飞书原文）]\n图中 owner 不是为了展示，而是每次读写查询的组成部分。durable runtime 的全局 worker 可以领取任务，但它不能因此获得跨 tenant 的业务读取能力；资源服务仍用 job.owner_user_id 重新进入 Repository 边界。\n📷 [图片 token=RiZDbaOWGoUe6nxODYyc5csYn3b（未能下载，见飞书原文）]\n从列表、详情到变更的统一审查 权限缺陷经常只修详情接口，却漏掉列表、统计或批量操作。审查一个新实体时，应按 create、list、get、update、delete 五类方法逐一确认：create 的 owner 必须来自当前用户而非请求 body；list 必须先按 owner 过滤再分页或排序；get 与 update 需要在同一查询中匹配 ID 和 owner；delete 需要同样范围并检查关联清理；重试、复制、导出和 search 也应视为独立读写入口。\n📷 [图片 token=M7RgbUR22o3cslx6o3Bc6gbznqb（未能下载，见飞书原文）]\n关联对象还需要父子范围一致。例如创建 document index task 前，_require_document 同时匹配 owner、knowledge base 和 document；追加 chat message 前，_require_chat_session 确认父会话；添加诊断 report 或 evidence 前，helper 确认 task 同 owner。只在子记录写 owner，而不验证父对象，会留下跨 tenant 关联污染。\n📷 [图片 token=JAlQbuQVloSS4Yx9bYyc7AtPnSZ（未能下载，见飞书原文）]\n看什么：文档详情查询把 document、owner 和 knowledge base 放在同一个 SQL WHERE 中，并可继续叠加软删除条件。\n📷 [图片 token=WN6Wb0Dw7ovX91x7kWQcTrHWnRb（未能下载，见飞书原文）]\nasync def get_document( self, *, owner_user_id: str, knowledge_base_id: str, document_id: str, include_deleted: bool = False, ) -\u0026gt; KnowledgeDocumentRecord | None: # 1. ID、owner 与父知识库在同一查询中匹配。 stmt = select(KnowledgeDocumentModel).where( KnowledgeDocumentModel.id == document_id, KnowledgeDocumentModel.owner_user_id == owner_user_id, KnowledgeDocumentModel.knowledge_base_id == knowledge_base_id, ) # 2. 软删除可见性在 scoped 查询上继续收窄。 stmt = _apply_deleted_filter(stmt, include_deleted) async with self._session_factory() as session: row = (await session.scalars(stmt)).one_or_none() return _knowledge_document_record(row) if row is not None else None 📷 [图片 token=ZqLGb2cO7oQCp1xa5HncWHH5nH9（未能下载，见飞书原文）]\n这段实现避免先查全局 document 再比较 owner，也避免把相同 document ID 错挂到另一个知识库。审查 update、delete、retry 或导出时必须重复检查同样的组合条件；一个安全的 get 方法不能自动证明其他入口安全。\n📷 [图片 token=MfV0byA6CotjXnxSu0Yca0vRn5f（未能下载，见飞书原文）]\n错误语义与对象存在性 已认证用户访问另一个 owner 的资源时，owner-scoped 查询得到空，API 返回统一 403。响应不会告诉调用者该 ID 是否真实存在、属于谁或资源类型详情。对于未认证调用者，处理器在进入 Repository 前就返回 401。这个顺序既保持统一契约，也减少通过状态码和文案枚举资源的机会。\n📷 [图片 token=QLKRbvuyiogZBIxbxWjcURpGnhg（未能下载，见飞书原文）]\n内部 TenantScopeError 的消息可能包含资源 ID，适合在受控服务边界定位问题，但不应直接作为客户端响应。API 使用 AUTH_FORBIDDEN 的安全默认消息。观测日志也只应记录请求路径、错误 code 或异常类别；如果把 Repository 异常原文和用户输入一起写日志，就会绕过 HTTP 层的隐藏策略。\n📷 [图片 token=LmZrbDRNooFDOHxNQbjcBgD9nPc（未能下载，见飞书原文）]\n看什么：用判定图区分“没有认证上下文”和“有身份但 scoped 查询无结果”，两者发生在不同层。\n📷 [图片 token=VTLAbcnhgo4VFCxQuJ1c37VznNh（未能下载，见飞书原文）]\n📷 [图片 token=NqXZbING4oVUgbx7bmScS9zZnJg（未能下载，见飞书原文）]\n这个分支保证已认证攻击者不能从 404、资源标题或异常原文枚举其他 tenant 对象。内部诊断仍可记录安全 code 与异常类别，但把含资源 ID 的 TenantScopeError 原文直接返回客户端会破坏隐藏策略。\n📷 [图片 token=I7cdbbgahoR4NXxf9Mvcyeetnib（未能下载，见飞书原文）]\n浏览器 token 的现实边界 当前 localStorage 方案支持刷新恢复和 bearer API，但脚本若能在同源页面执行，就可能读取 token。因此前端仍需避免不可信 HTML 注入，内容展示组件必须保持安全渲染；这不是后端 tenant 过滤能够补救的风险。另一方面，服务端 session 可撤销意味着 token 泄露后的处置不只依赖等待过期，用户登出后相同 token 会被拒绝。\n📷 [图片 token=U5m9bMltgonqPlxhh4ucByY2n7f（未能下载，见飞书原文）]\n当前模型没有 session 过期时间字段，AuthSessionModel 记录 created、last seen 与 revoked。不能把不存在的自动过期描述成已实现能力。若未来增加有效期，需要同步数据迁移、认证服务判断、统一错误、前端恢复和测试，而不能只在浏览器设置一个计时器。\n📷 [图片 token=KhM1bJrJWo1CFVxWRhOc9CHRnyb（未能下载，见飞书原文）]\n认证相关的安全评审还应检查响应缓存与页面切换。当前客户端每次受保护请求都从存储读取 token，服务端每次都重新验证 session，没有把某次 /auth/me 成功当作后续请求的永久通行证。用户切换后，旧页面中的异步响应也不应重新写回已清空 store；前端状态测试与后端 owner 查询共同限制这一竞态的影响。\n📷 [图片 token=OlelboDFwoZn4zxLaKzcl7eCn9g（未能下载，见飞书原文）]\n看什么：认证客户端每次请求都从当前 storage 读取 token，并仅在存在时添加 bearer header。\n📷 [图片 token=YHdFbhIubogDj9xWOwKcPCTDnrg（未能下载，见飞书原文）]\nconst token = storage.getItem(AUTH_TOKEN_STORAGE_KEY); const headers = new Headers(init.headers); headers.set(\u0026#34;Accept\u0026#34;, \u0026#34;application/json\u0026#34;); if (init.body !== undefined \u0026amp;\u0026amp; !headers.has(\u0026#34;Content-Type\u0026#34;)) { headers.set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;); } // 1. 每次请求读取当前 token，而不是缓存一次 /auth/me 结论。 if (token !== null) { headers.set(\u0026#34;Authorization\u0026#34;, `Bearer ${token}`); } const response = await fetchImpl(`${baseUrl}${path}`, { ...init, headers }); if (!response.ok) { // 2. 服务端仍会逐次验证 session 与撤销状态。 throw new AuthClientError(await parseApiError(response)); } 📷 [图片 token=CPjLbpMFQo5dKoxk0fbcSvGrnMe（未能下载，见飞书原文）]\n代码证明前端能在 token 被清除后停止发送旧 bearer，却也暴露 localStorage 的现实风险：同源脚本可以读取同一个值。防 XSS、安全渲染和依赖治理属于客户端边界；owner-scoped 后端查询与服务端撤销则限制 token 被盗后的数据范围和处置路径。\n📷 [图片 token=JmfhbjU6no7Ot1x5bF0cl5oSnNh（未能下载，见飞书原文）]\n数据、契约与状态 共享 AuthUser 只包含 id、email、displayName 和 createdAt。AuthTokenResponse 增加 accessToken 与固定的 bearer tokenType；LogoutResponse 只表达 revoked 为真。后端 _auth_result_payload 与这些字段一一对应，密码哈希、session ID、token hash、last seen 和 revokedAt 都留在服务端。\n📷 [图片 token=FnfLbQvJHoncytx664BcHsMvnZf（未能下载，见飞书原文）]\n认证状态至少有三层：浏览器有没有 token、服务端 session 是否存在且未撤销、业务资源是否属于当前 user。第一层缺失会触发前端跳转；第二层失败返回 401；第三层失败返回 403。不能用“token 格式合法”推导“session 有效”，也不能用“session 有效”推导“资源可访问”。\n📷 [图片 token=LvNEbX8U5obZNtxgGcOccfcznKc（未能下载，见飞书原文）]\n业务数据中的 owner_user_id 与向量数据中的 tenantId 当前都取 UserRecord.id。主规格 openspec/specs/authorization-and-tenant-isolation/spec.md 明确这是现阶段约束而非永久组织模型。未来若引入组织 tenant，不能简单把一个字段改名；需要重新审查成员关系、owner 与 tenant 的区别、迁移、共享资源与 Milvus filter。\n📷 [图片 token=Aas9btISpoMKo2xatjucVFYCnqg（未能下载，见飞书原文）]\n权限、安全与失败边界 匿名请求没有 tenant scope，因此不得访问用户专属 Repository。HTTPBearer 配置 auto_error=False 后，项目自己用 AUTH_UNAUTHENTICATED 生成统一 envelope。未知 token 和未知邮箱不会暴露额外细节；撤销 token 使用明确的 session-revoked 代码，仍是 401。\n📷 [图片 token=SyuIb7cOJovH10xu2gacc9B4n8f（未能下载，见飞书原文）]\n所有 Agent 和工具装配都必须发生在授权后。聊天会话权限在 Agent runner 前确认，知识检索工具只接收当前用户可访问知识库，MCP 只装配当前用户启用且真实发现的连接，工具审计带 owner。AIOps 的任务、步骤、证据、报告与 checkpoint 也通过同一个 owner 传递。任何“为了提高召回”而移除 tenant filter 的做法都会突破系统最重要的数据边界。\n📷 [图片 token=GsoPb85Vjo3ApJx1W3JcElKunMf（未能下载，见飞书原文）]\n日志不得记录 Authorization、bearer token、密码、完整用户消息或工具参数。apps/backend/src/super_ai/observability.py 的敏感键匹配与递归 _redact 是一道通用防线，具体调用点还应只传 ID、状态、耗时和错误类别。跨 tenant 拒绝也不应把目标对象内容或 owner 信息写进响应。\n📷 [图片 token=WPRMbSVXVoZeQQxnCNxcpiljncT（未能下载，见飞书原文）]\n阅读顺序与小结 从 packages/api-contracts/src/auth.ts 认识公开的身份数据形状。\n阅读 apps/backend/src/super_ai/auth/service.py 与 apps/backend/src/super_ai/auth/sqlite.py，区分密码哈希、原始 token 和 token hash。\n在 apps/backend/src/super_ai/api/app.py 追踪 _current_user 和一个具体受保护路由。\n在 apps/backend/src/super_ai/memory/sqlite.py 检查 owner 条件是否进入 SELECT、UPDATE 与父对象验证。\n最后阅读 apps/backend/src/super_ai/memory/vector_scope.py 与 Milvus 实现，确认 tenant 和授权知识库范围同时进入向量过滤条件。\nOncallAgent 的 tenant 隔离不是一处中间件，而是一条贯穿认证、路由、Repository、向量、Agent 与前端状态的传递链。最可靠的审查方法是选择一个资源 ID，确认每一步都同时携带当前用户 ID，并确认空授权范围不会退化成全量操作。只要有一层把 owner 当成可选参数，AI 工具和高召回检索就会放大那一处缺口。\n📷 [图片 token=CJDhbHvjRoxSV3xT8EbcftybnDf（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/03.%20%E7%94%A8%E6%88%B7%E8%AE%A4%E8%AF%81%E4%B8%8E%20tenant%20%E6%95%B0%E6%8D%AE%E9%9A%94%E7%A6%BB/","summary":"OncallAgent 的权限模型从一个朴素但明确的规则开始：当前已认证用户的 ID 同时充当 tenant scope，直到未来引入独立的组织 tenant。这个决定贯穿 bearer session、FastAPI 依赖、SQLite","title":"03. 用户认证与 tenant 数据隔离"},{"content":"主规格位于 openspec/specs/\u0026lt;capability\u0026gt;/spec.md，描述系统当前已经同意并生效的行为。它与 change 下的 delta spec 是两种不同时间视角：delta 回答“这次准备改变什么”，main spec 回答“系统现在是什么样”。\n📷 [图片 token=MiMVboo3zoF6t6xGhoJcel3unZb（未能下载，见飞书原文）]\n主规格为什么是权威基准 OncallAgent 的代码量和能力边界较多，包括用户认证与 tenant 隔离、流式聊天、Prompt/Skill/记忆配置、知识文档索引、Milvus 与 BM25L 混合检索、用户级 MCP、CLS 日志访问、Alertmanager 告警和 LangGraph AIOps 诊断。只读代码很难快速判断哪些行为是有意设计，哪些只是实现细节。\n主规格把已经生效的行为按 capability 固化下来。开始新 change 时，Codex 必须先读相关主规格，避免重复实现已有能力、破坏权限边界，或者把历史行为误判为偶然代码。\n📷 [图片 token=FQcAbolpLo4e0fxuzfocrSqonpe（未能下载，见飞书原文）]\n主规格的典型结构 # qwen-openai-provider Specification ## Purpose ## Requirements ### Requirement: ... #### Scenario: ... - **WHEN** ... - **THEN** ... Purpose 用一段话说明 capability 的长期职责。它不描述某次工单，而是解释该领域为什么存在。例如 qwen-openai-provider 的 Purpose 是通过 OpenAI-compatible 协议定义后端模型提供商合约，同时保持业务代码与供应商无关。\nRequirements 收纳当前有效的全部行为约束。每个 Requirement 下包含一个或多个 Scenario。随着 change 归档，新的行为加入、既有行为修改、废弃行为删除，但文件始终试图描述当前状态。\n📷 [图片 token=Fa2TbS65poxsTrx7wiHcLsHCnDf（未能下载，见飞书原文）]\nsync 不是复制粘贴 OncallAgent 的 openspec-sync-specs 是 Agent 驱动的智能合并：\nADDED 在不存在时新增；若同名 requirement 已存在，则按修改处理，避免重复。\nMODIFIED 只应用 delta 提到的变化，保留主规格中未被触及的其他描述和 Scenario。例如只新增一个越权场景时，不需要把正常场景全部复制到 delta。\nREMOVED 删除整个 requirement；RENAMED 按 FROM/TO 修改名称。新 capability 不存在时，sync 会创建对应目录与主 spec，并补充 Purpose。\n这个过程应保持幂等：同一个 delta 重复同步，不应不断追加重复 requirement。归档前需要比较 delta 与 main spec，明确说明将新增、修改、删除或重命名什么。\n📷 [图片 token=SciCbXxU1owDYwxebPAcd7jOnAe（未能下载，见飞书原文）]\n主案例怎样进入主规格 limit-qwen-embedding-batch-size 的 delta 在 qwen-openai-provider 下 ADDED 了 Qwen embedding batch compatibility。归档后，同名 requirement 和三个 Scenario 已出现在：\nopenspec/specs/qwen-openai-provider/spec.md 它与原有的模型配置、ChatOpenAI Provider、可替换抽象、readiness、Embedding 原始输入和显式维度等 requirement 共存。主规格没有变成“批量限制工单说明”，而是把新约束纳入 Qwen Provider 的完整当前契约。\n📷 [图片 token=EMgybYzLlogx9ZxI6HOcAxNAnFd（未能下载，见飞书原文）]\n这也说明主规格与 archive 分工不同。想知道“系统现在是否要求每批最多 10 条”，查 main spec；想知道“为什么在 2026-07-11 引入这个约束、当时有哪些取舍和任务”，查 archive change。\n📷 [图片 token=XoPNb2umdoba7axEF2QcuSLZn6c（未能下载，见飞书原文）]\n主规格、代码和测试怎样互证 主规格不是代码的自动镜像，也不能单独证明实现正确。主案例有三类证据：\n**规格证据。**主 spec 明确每批上限、顺序完整性和大文档 succeeded。\n实现证据。apps/backend/src/super_ai/llm/provider.py 定义 QWEN_EMBEDDING_BATCH_SIZE = 10，构造 OpenAIEmbeddings 时传入 chunk_size。\n**测试证据。**Provider 单测用 11 条输入验证调用被拆成 10+1 且向量顺序不变；索引回归测试生成 11 个 chunk，验证任务 succeeded、11 个 chunk 全部写入。不同证据共同覆盖 Requirement，而不是只靠文档自证。\n📷 [图片 token=OkolbCLFIoFtATx74aEc32gmnVe（未能下载，见飞书原文）]\n为什么不能直接编辑主规格完成新需求 直接改 main spec 会丢失“变化从哪里来”的上下文：没有 proposal 解释动机，没有 design 记录取舍，没有 tasks 追踪交付，也没有独立 delta 便于审查。多人并行时，多个需求还会直接争抢同一主文件。\n先在 change 中写 delta，可以隔离并行工作；实现和验证完成后再 sync，主规格才接收已经交付的变化。这个设计类似数据库迁移与当前 schema 的关系：迁移记录演进过程，当前 schema 描述最终状态。\n📷 [图片 token=VBPObV2HFoNyavx0ejQcLmcQnUh（未能下载，见飞书原文）]\n常见错误 **把 main spec 当产品愿望清单。**主规格只能声明已经生效的行为，未实现规划应留在 active change。\n**归档了 change，却没同步主规格。**这会造成 archive 说功能已完成，而 main spec 不知道该行为。OncallAgent 的 wiki-sync 默认会阻断这种不一致。\n**只看主规格，不看实现和测试。**规格是意图基准，不是运行证据。verify 必须建立文档、代码、测试之间的对应关系。\n**在归档文件上继续开发。**后续变化应创建新的 delta，再合入主规格，不能修改旧 change 改写历史。\n📷 [图片 token=HaRdbS7Zqo7d6gxy2f5crggUnfT（未能下载，见飞书原文）]\n面试表达 主规格保存当前事实，delta 保存一次变化。我们不直接在主 spec 上堆需求，而是在独立 change 中完成 proposal、delta、design、tasks 和实现验证，再智能合并。这样主规格适合新任务读取，archive 适合追溯决策，二者分别解决“现在是什么”和“为什么变成这样”。\n📷 [图片 token=H13pbSz5Lo0DQmxxeEzcBTx6n7d（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/main%20spec.md%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"主规格位于  openspec/specs/\u0026lt;capability /spec.md ，描述系统当前已经同意并生效的行为。它与 change 下的 delta spec 是两种不同时间视角：delta 回答“这次准备改变什么”，main s","title":"main spec.md文件作用介绍"},{"content":"wiki-sync 是 OncallAgent 在通用 OpenSpec 之外增加的仓库级 Skill。OpenSpec 已经把 proposal、design、tasks 和 specs 保存进 Git，但直接在几十个目录里查找并不方便。wiki-sync 为它们生成 VitePress 浏览层，让变更可搜索、可导航，同时坚持 OpenSpec 文件是唯一事实来源。\n📷 [图片 token=MXLtb8s8Bomd3KxvWnRcDzbvnRg（未能下载，见飞书原文）]\n先区分三个容易混淆的概念 OpenSpec sync 把 change 下的 delta spec 合入 openspec/specs/ 主规格。\nOpenSpec archive 把完整 change 移到 openspec/changes/archive/YYYY-MM-DD-name/。\nOncallAgent wiki-sync 根据 active/archive 目录生成 docs/changes/ 页面、索引和 VitePress Sidebar。它不实现业务功能，也不把内容发布到飞书。\n三者顺序通常是：先验证实现，再 sync 主规格，再 archive 历史，最后 wiki-sync 文档展示。\n📷 [图片 token=BiVqbBDw0opSzKxuHIZco8G4nGg（未能下载，见飞书原文）]\n确定性入口 python3 .codex/skills/wiki-sync/scripts/sync_wiki.py active \u0026lt;change-name\u0026gt; python3 .codex/skills/wiki-sync/scripts/sync_wiki.py archive \u0026lt;name-or-dated-name\u0026gt; python3 .codex/skills/wiki-sync/scripts/sync_wiki.py all active 用于完整规划产物生成后建立进行中页面；archive 用于归档后更新状态；all 全量重建。虽然 archive 命令接受一个名称，脚本仍会扫描全部 active 和 archive、校验全部归档并重建完整快照，避免人工维护造成漂移。\n📷 [图片 token=CPxsbGiWPoJDeAx37XKcHaCdn1b（未能下载，见飞书原文）]\n裸 openspec-new-change 后通常只有 .openspec.yaml，而 wiki-sync 页面固定 include proposal、design、tasks 和 delta specs，因此不能在裸 new 后立即成功同步。应在 openspec-propose 完成后，或 new + continue 补齐产物后再执行。\n📷 [图片 token=WQ7LbTQ9VobkDhxva5TcwGkpnRv（未能下载，见飞书原文）]\n生成哪些文件 docs/ ├── openspec -\u0026gt; ../openspec ├── changes/ │ ├── index.md │ ├── active/\u0026lt;name\u0026gt;/index.md │ └── archive/YYYY-MM-DD-\u0026lt;name\u0026gt;/index.md └── .vitepress/ └── config.mts 每个 change 只生成一个聚合 index.md，不会把四类 OpenSpec Markdown 再复制一份。归档页包含 title、status: archived、createdDate、archivedDate 等 frontmatter，以及 proposal、design、tasks 和每个 delta spec 的 include。\n📷 [图片 token=J8NsbWveNo36E6xeHKUcDMEvnih（未能下载，见飞书原文）]\n\u0026lt;!--@include: ../../../openspec/changes/archive/ 2026-07-11-limit-qwen-embedding-batch-size/proposal.md--\u0026gt; 这里的 docs/openspec 是指向仓库根 openspec 的符号链接。VitePress 从 docs 内的路径读取，实际内容仍来自 OpenSpec 原文件。页面是“窗口”，不是第二份事实。\n📷 [图片 token=Ln71bpRHnoKiZbxaxlpcUoDtnFb（未能下载，见飞书原文）]\n为什么使用 symlink 加 @include 如果同步脚本把正文复制到 docs，归档中修复一个错字后还要同步副本；任何一次遗漏都会出现 OpenSpec 与 WIKI 内容不同。include 让页面每次构建都读取源文件，消除正文双写。\n它还保留清晰责任：OpenSpec artifact 可以被 CLI、Codex 和 Git 直接处理；VitePress 只负责浏览体验。生成页只维护标题、状态、导航和 include 关系，脚本可以安全地确定性重建。\n📷 [图片 token=U7f0bknMWoOgJOx7d7ccGU4cnsf（未能下载，见飞书原文）]\n归档同步前的严格校验 目录布局。docs/openspec 必须是指向 ../openspec 的正确符号链接，openspec/changes 必须存在。\n**归档名称解析。**可以传完整日期名称或短名；短名必须恰好匹配一个目录，否则失败，避免更新错误目标。\n**delta/main spec 一致性。**每个 archive 至少有一个 delta spec。脚本解析 ADDED、MODIFIED、REMOVED、RENAMED：新增和修改的 requirement 必须在 main spec 中，删除的必须已消失。未同步默认阻断。\n📷 [图片 token=LWKObnFq5o3gOSxkCOFcXiyXnZA（未能下载，见飞书原文）]\n**include 完整性。**每页至少包含 proposal、design、tasks，并为所有 delta spec 生成 include；每个路径必须存在，归档页不能继续引用归档前的 active 路径。\n镜像与导航。docs/changes/active、archive 页面目录必须与 OpenSpec 目录一一对应；docs/changes/index.md 和 Sidebar 必须来自同一顺序。陈旧页面会被删除，生成目录内不应手写额外内容。\n--allow-unsynced 只能在明确批准后绕过 delta/main spec 同步问题，不能绕过 symlink、include、frontmatter、目录镜像或导航校验。\n📷 [图片 token=RXUEbc2FzoqIotxrIcicdJfynaf（未能下载，见飞书原文）]\n为什么还要单独运行 docs:build wiki-sync 脚本检查业务完整性，但不在脚本内部运行 VitePress 生产构建。同步成功后仍须：\nnpm run docs:build 结构校验和构建验证解决不同问题。include 路径全部真实存在，不代表 Markdown 一定能被 VitePress 正确构建；VitePress 构建通过，也不代表页面没有漏 include。因此两道门禁都需要保留。生成的 docs/.vitepress/dist/ 和 cache 被 Git 忽略，不提交 HTML 产物。\n📷 [图片 token=XahtbibFGo1L0XxXl5Pcivp7ngc（未能下载，见飞书原文）]\n主案例的 WIKI 页面 归档 2026-07-11-limit-qwen-embedding-batch-size 对应：\ndocs/changes/archive/ 2026-07-11-limit-qwen-embedding-batch-size/index.md 页面有 archived frontmatter，固定 include proposal、design、tasks，再 include qwen-openai-provider delta spec。.openspec.yaml 仍保存在 archive，但不面向 WIKI 读者展示。总索引和 Sidebar 中也存在同一条目。\n📷 [图片 token=Ck1qbEHdHoVMFcxWW3acAd4Mnpb（未能下载，见飞书原文）]\n另一个很适合说明机制的自举案例是 add-openspec-wiki：它通过 OpenSpec change 建立 wiki-sync，归档后又被自己建立的同步机制收录。面试中可以用“dogfooding”解释：工具自己的演进也遵守同一套规格、任务、验证和归档规则。\n面试表达 我们没有维护第二份 Wiki 正文，而是让 VitePress 聚合页通过 symlink 和 @include 读取 OpenSpec 源文件。wiki-sync 全量扫描 active/archive，校验 delta 已进入主规格、include 真实存在、页面与导航严格镜像，再单独跑 docs build。这样 WIKI 是可浏览视图，OpenSpec 仍是唯一事实来源。\n📷 [图片 token=BRYBbRIx2oYFSZxfcsec3FvQnTb（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E5%BD%92%E6%A1%A3%E4%B8%8E%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/wiki-sync%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"wiki-sync  是 OncallAgent 在通用 OpenSpec 之外增加的仓库级 Skill。OpenSpec 已经把 proposal、design、tasks 和 specs 保存进 Git，但直接在几十个目录里查找并不方便","title":"wiki-sync文件作用介绍"},{"content":"答案不复杂：边界清楚的话，一周可以做出第一版 先说结论：大约一周。\n这里的“一周”并不是指做出几张演示页面，也不是把现成接口拼成一个只能跑通 happy path 的样品，而是由一个人借助 AI，从零搭建一套前后端完整、能够在本地运行的 AIOps Agent 工作台。这个项目叫 OncallAgent，它面向真实的值班和故障处理场景，把账号与租户隔离、流式对话、知识库检索、MCP 工具接入、告警诊断、证据留存和报告生成串在了一起。\n📷 [图片 token=I64uba5wEo3nlcxo5aBcLoZ4naf（未能下载，见飞书原文）]\nOncallAgent 的前端使用 Vue 3、Vite 和 TypeScript，后端基于 FastAPI；聊天 Agent 由 LangChain 组织，诊断流程由 LangGraph 编排；SQLite 保存业务数据，Milvus 负责知识向量，腾讯云官方 CLS MCP Server 提供真实日志能力。它采用本地优先的模块化结构，Docker Compose 只托管必要的基础设施，前端、后端和 MCP Server 都可以在宿主机直接启动。\n📷 [图片 token=AX3KbMot0ovz1ixdDLJcoqixn4m（未能下载，见飞书原文）]\n听到这里，质疑很自然。认证、聊天、RAG、工具调用和 AIOps 中的任何一项，单独拿出来都不是一天两天能随便做完的功能。更何况系统还要处理 SSE 事件、后台任务、权限过滤、错误契约和可追溯证据。按照常规估算，这通常像是一个小团队需要推进数周的项目。\n📷 [图片 token=EU2ZbkHoUofs1LxQB1UcajeQnue（未能下载，见飞书原文）]\n所以这篇文章并不准备靠一句“一周做完了”制造惊讶。我更想说明的是：一周究竟完成了什么，哪些工作因为 AI 明显加速，哪些问题仍然必须由人判断，以及为什么同样使用 AI，有的人能交付完整系统，有的人却只得到一堆互相接不上的代码。\n效率真正发生变化，是从亲自编码转向持续判断 刚开始使用 AI 时，我也习惯把它当成更强的代码助手：写一个函数、补一组类型、重构重复逻辑，或者生成常见的测试样板。这些用法当然有效，却没有改变项目的推进方式。人依旧需要逐段设计、逐段实现，AI 只是让键盘输入快了一些。\n复杂工程的慢，往往不是慢在代码敲得不够快，而是慢在大量相互关联的判断：产品范围应该收在哪里，数据边界如何建立，模块之间通过什么契约协作，异常如何传递，哪些状态必须持久化，哪些基础设施可以暂时不引入。只要这些问题含糊，代码生成得越快，返工通常也来得越快。\n📷 [图片 token=PMN1bSA9eoifs9xKpsgcNiqCnAg（未能下载，见飞书原文）]\nOncallAgent 让我第一次真正感受到角色变化。我的主要工作不再是把每行代码写出来，而是不断回答几个问题：现在最应该解决什么？这一步的输入和输出是否清楚？AI 交付的结果要怎样验收？如果方案超出了当前目标，应该从哪里裁掉？\n这更像一个架构师带着一支执行速度极快的工程团队。AI 可以阅读仓库、实现跨文件改动、补测试、运行检查并根据失败继续修正，但它不会天然知道什么才是这个项目的“合适答案”。产品边界、风险偏好和完成标准，仍然掌握在人手里。\n📷 [图片 token=XuJRb0yMkoyIESxgw8xcj884nSd（未能下载，见飞书原文）]\n最有价值的产出，不是代码 项目一开始，我曾让 AI 直接为一套 AIOps Agent 系统设计架构。它很快给出了一套看起来相当完整的方案：拆分多个服务，引入注册中心、分布式配置、统一网关、全链路追踪，并为模型、知识索引、MCP 和诊断编排分别建立独立部署单元。\n方案并非错误，只是解决了一个并不存在的问题。OncallAgent 的第一阶段服务对象是本地环境中的小团队，目标是在短时间内验证从告警到诊断报告的完整闭环，而不是支撑百万用户的云平台。此时引入复杂的分布式治理，只会让部署、调试和联调成本迅速膨胀。\n📷 [图片 token=K95BbigFDobEORxgIVac7ovSnZf（未能下载，见飞书原文）]\n那天晚上，我停下了实现工作，先完成三件更重要的事。\n**先画清产品边界。**必须做的是认证与用户隔离、持久化流式聊天、知识文档索引与混合检索、用户级 MCP 连接、告警触发诊断、真实工具取证、证据报告和案例沉淀。不做通用的多模型运营后台，不做拖拽式 Agent 编排，也不为了未来可能出现的规模提前拆微服务。\n**再确定系统骨架。**前后端在同一仓库中协作，FastAPI 按领域组织后端，Vue 负责工作台交互；SQLite 保存可审计的业务状态，Milvus 只保存带权限字段的知识 chunk 向量；LangGraph 的 Planner、Executor、Replanner 和 Report 节点共同完成诊断；基础设施与应用进程保持清晰边界。\n**最后把规则写进仓库。**用户可观察行为先进入 OpenSpec，HTTP 与 SSE 结构由共享契约统一定义，前后端不能各写一套 DTO；所有数据访问必须携带 owner 或 tenant 范围；MCP 只能调用真实发现的工具；没有证据时必须明确说证据不足，不能生成看似合理的根因。\n📷 [图片 token=RuRFbhsOVoTjTtxPZP6cjAcGnDg（未能下载，见飞书原文）]\n从第二天开始，交给 AI 的任务不再是“帮我把 AIOps 平台做出来”，而变成了可以核验的工作单元。例如：先更新共享契约，再实现用户级 MCP 连接的增删改查和连通性检查，随后接入后端审计与前端状态展示，最后运行对应的类型检查和测试。输入越明确，结果越稳定。\n把大需求改写成一串可验收的纵向闭环 与 AI 协作时，拆分粒度决定了返工成本。只按技术层切任务，比如“先写所有数据库表，再写所有接口，最后统一做页面”，很容易在最后联调时集中暴露契约错位。OncallAgent 更适合按用户能感知的闭环推进：一次完成一条从契约、存储、服务、接口到页面和测试的完整链路。\n📷 [图片 token=Xl2fbrvFao6pqcxFFOWch8urneb（未能下载，见飞书原文）]\n知识库就是一个典型例子。它不是简单的“上传一个文件”，而是一条连续链路：用户创建知识库并上传 Markdown 或 PDF，后台任务解析与切块，索引写入 Milvus，聊天 Agent 以当前用户权限执行向量、BM25 和 rerank 检索，前端再展示来源、阶段排名和分数。删除文档时，元数据与向量也必须在相同 tenant 和知识库范围内清理。\n📷 [图片 token=IMJYbtEb5o180Ix84prckwx6nth（未能下载，见飞书原文）]\nAIOps 诊断则是另一条闭环：系统读取真实活跃告警，Planner 先检索当前用户可访问的 SOP，Executor 调用实际发现的 MCP 工具获取日志、指标或告警证据，Replanner 根据已有结果决定继续还是调整，Report 最终只依据持久化证据给出结论。成功的诊断还可以沉淀为案例，再参与后续检索。\n📷 [图片 token=WnUqbY54WoFb6bxpdICc0zxSnHl（未能下载，见飞书原文）]\n当任务被拆到这个程度，AI 才能获得足够上下文，也更容易接受明确的验收标准。每一轮都可以问：共享契约是否同步？跨 tenant 是否真的被拒绝？SSE 是否包含开始、工具调用、完成和结构化错误？工具失败有没有如实进入证据链？页面是否能区分等待、执行、成功与失败？\n📷 [图片 token=JmULbSh94oarqqxXNiqcjokjnbb（未能下载，见飞书原文）]\n这种做法的重点不是让 AI 多写代码，而是让每次生成都落在一个可以独立验证的范围里。做完一条闭环，系统就多出一项真实能力，而不是多出一批暂时无法证明能协同工作的文件。\n和 AI 协作，有两种完全不同的节奏 当方案已经明确时，我使用的是执行式协作。任务中会给出目标、影响范围、必须遵守的规范、相关事实来源和验证命令，AI 负责检查现状、实现最小改动、补齐测试并报告实际结果。人的重点是审查意图是否一致、实现质量是否合格、改动有没有越过边界。\n当问题本身还没有想清楚时，直接要求实现往往适得其反。此时更适合使用讨论式协作：先让 AI 阅读规格和代码，列出数据流、约束、遗漏点与可选方案；人根据项目目标做取舍，形成决定之后，再切换到执行阶段。\n📷 [图片 token=C3Jnb5UgQoOCsixsIL5cNuPjndc（未能下载，见飞书原文）]\n这两种节奏不能混为一谈。探索阶段需要允许多个方案存在，执行阶段则必须有唯一、明确的目标。如果一边让 AI 自由发挥，一边又期待它精准实现一个尚未定义的需求，最后得到的通常是表面完整、内部摇摆的代码。\n我在实践中还形成了一个简单的检查顺序。先看它是否理解了真正的需求，再看实现是否通过类型、测试和运行约束，最后检查有没有悄悄扩大范围。这个顺序比逐行阅读所有生成代码更高效，因为许多严重问题在第一层就能被发现：解决错了问题，后面的代码写得再漂亮也没有意义。\n📷 [图片 token=OG7rbQiZaofpu8xxyTncp1JGnFe（未能下载，见飞书原文）]\n一周交付的背后，是几次关键取舍 OncallAgent 能够快速成形，不是因为把所有想法都实现了，而是因为多次主动拒绝了“看起来更完整”的方案。\n架构上，它选择本地优先的模块化实现，没有把应用服务全部塞进 Compose，也没有为了想象中的扩展性拆成微服务。模型接入沿用统一的 OpenAI-compatible provider，不在业务代码里混入多个厂商 SDK。前后端通过共享 API 与 SSE 契约协作，避免在联调阶段依靠口头约定修修补补。\n数据上，当前 user ID 就是 tenant 范围。聊天、知识库、向量、MCP 连接、诊断任务、证据、报告和工具审计都必须显式隔离。Milvus 没有授权知识库时直接返回空结果，不能用无范围检索“碰碰运气”。这类约束看起来会增加开发量，却能在早期消灭大量隐蔽返工。\n📷 [图片 token=YooLbWtuPoage3xuXZcc5GHAnpe（未能下载，见飞书原文）]\n诊断上，系统宁愿说“没有匹配 SOP”或“证据不足”，也不允许编造成功的工具调用、日志内容和根因。真实 MCP 连接失败时，失败本身就是需要保留的事实。对 AIOps 来说，可追溯和不造假比答案显得聪明更重要。\n测试上，每次改动先跑最相关的检查，再按影响范围扩大验证。数据库变化验证迁移，API 或 SSE 变化同时检查共享契约、后端和前端，界面变化验证关键状态与布局。AI 能显著降低编写这些测试和修复反馈的成本，但“哪些风险必须被覆盖”仍然需要人来判断。\n📷 [图片 token=I4YfbMm5ZorBoHx9Yomc5yo2n6f（未能下载，见飞书原文）]\nAI 会放大方向，也会放大含糊 整个过程中，AI 的确承担了大量工作：理解已有仓库、生成跨层实现、补齐类型、编写测试、根据失败日志定位问题、同步文档。若完全手写，同样的工作量很难在一周内完成。\n但它也会带来麻烦。约束不足时，它可能给出过度设计；上下文不完整时，它可能在前端和后端各自发明一个相似却不一致的数据结构；跨模块状态复杂时，一次修改也未必能抓住真正原因。AI 的速度不会自动转化为正确性，它只是让正确路线和错误路线都跑得更快。\n因此，我更愿意把 AI 看作能力放大器，而不是项目的方向盘。你已经知道目标、边界和判断标准时，它可以把工程推进速度提高一个量级；如果这些前提不存在，它放大的往往是模糊、摇摆和返工。\n📷 [图片 token=R4TSbgio5oihepxz916c2ozenth（未能下载，见飞书原文）]\n这也解释了为什么“一周”不是一个可以脱离条件复制的数字。技术栈是否熟悉，需求是否克制，仓库规范是否明确，任务能否拆成闭环，验收是否及时，都会改变最终时间。一个人加上 AI，并不自动等于一个成熟团队；只有当这个人真正承担起产品、架构和质量决策，AI 才能成为高效的执行力量。\n📷 [图片 token=HrLRbbVWfoYj1mxmlJrccogenxd（未能下载，见飞书原文）]\n真正值得复用的，不是某个工具版本 工具更新很快，今天常用的命令、交互方式和上下文能力，几个月后都可能变化。如果学习重点只停留在某个功能按钮或提示词模板上，经验很容易随着版本迭代失效。\n更持久的能力是：面对复杂需求时，能否先识别系统边界；能否把模糊目标转成明确规范；能否把大工程拆成可验证的纵向任务链；能否从意图、质量和边界三个层面审查 AI 输出；能否在证据不足时拒绝一个看似漂亮的答案。\n📷 [图片 token=H8G1bfGZno3xVUxb0XrcG2P7nGg（未能下载，见飞书原文）]\n所以回到标题：一个人用 AI 写一个 Agent 项目需要多久？对于 OncallAgent 这样的首个可运行版本，一周左右是可以实现的。但真正让这个数字成立的，不是 AI 替人完成了思考，而是人先完成关键判断，再让 AI 把判断快速变成代码、测试和可运行的系统。\n当这种协作方式建立起来，你得到的不只是一个项目，也不是对某款工具的依赖，而是一套可以迁移到下一套技术栈、下一个 Agent 和下一代 AI 编程工具上的工程方法。\n📷 [图片 token=YrB5bwkhcoAcMGxykZZcR8XSnfO（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/02%EF%BD%9CAI%20Coding%20%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A6%E6%9D%9F/%E4%B8%80%E4%B8%AA%E4%BA%BA%E7%94%A8AI%E5%86%99%E4%B8%80%E4%B8%AAAgent%E9%A1%B9%E7%9B%AE%E9%9C%80%E8%A6%81%E5%A4%9A%E4%B9%85%EF%BC%9F/","summary":"答案不复杂：边界清楚的话，一周可以做出第一版 先说结论：大约一周。 这里的“一周”并不是指做出几张演示页面，也不是把现成接口拼成一个只能跑通 happy path 的样品，而是由一个人借助 AI，从零搭建一套前后端完整、能够在本地运行的 A","title":"一个人用AI写一个Agent项目需要多久？"},{"content":"前几章我们把 Agent 的工具体系搭清楚了：Function Calling 让大模型能开口下指令，MCP 让工具标准化接入，Skills 让专业知识按需加载。到这里，Agent 能干活了，工具也能跑起来了。\n但有一个根本问题，前面几章都没正面回答：大模型压根看不到你的私有数据，怎么办？\n你的公司内部有几千页运维手册、几百个历史故障报告、一套只有内部才知道的排查 SOP，这些大模型从来没见过。你让它帮你做 OnCall 排查，它能给出很通用的思路，但就是没法给出针对你们系统的精准答案。\n这就是 RAG 要解决的核心问题。\n一、大模型为什么「看不到」你的数据？ 咱们先把问题说清楚，有三类「看不到」，每一类的原因都不同。\n📷 [图片 token=HrIqbdoeHonrrLxTPtycIRbQnrb（未能下载，见飞书原文）]\n第一类：知识截止日期\n大模型是用历史数据训练的，训练完之后它的知识就冻结了。比如 Claude 的训练数据截止到 2025 年某个时间点，之后发生的事情它一概不知。你去问它「我们系统今天凌晨发生了什么故障」，它根本没有这份信息，只能瞎猜。\n第二类：私有数据\n你们公司的内部文档、产品设计方案、运维手册、客服问答库，这些都是保密的，不可能出现在大模型的训练数据里。模型从没见过这些内容，自然没法基于它们来回答问题。\n第三类：实时数据\n今天刚写完的技术方案、刚产生的 bug 日志、刚更新的配置文件，这些是动态变化的，即便大模型更新了训练数据也追不上。你的 Agent 需要访问的，往往就是这类「最新鲜」的数据。\n三类问题的共同点：大模型的知识是静态的、封闭的，但你的业务数据是动态的、私有的。\n二、暴力解法为什么行不通？ 遇到这个问题，很多同学第一反应是：直接把数据塞进 Prompt 里不就行了？\n听起来很直接，但这个方案有三个致命问题。\n问题一：Context Window 有上限\n大模型一次能处理的文本是有限的，这个限制叫做 Context Window（上下文窗口）。就算是目前最大的模型，一次也只能处理几十万 token，换算成中文大概是几十万个字。\n你们公司的内部文档可能有几百个文件，光是运维手册就几十万字，根本塞不进去。更别提那些有几千个历史工单的故障数据库了。\n问题二：费钱又慢\nToken 是要花钱的。你塞进去 10 万 token 的背景资料，每次提问都要花那 10 万 token 的钱。一天问几百次，成本直接爆炸。而且 token 越多，模型推理越慢，用户等待时间越长。\n问题三：注意力稀释\n这是最容易被忽视的问题。大模型处理很长的上下文时，注意力会被稀释，相关内容埋在几万字的堆料里，模型很可能找不到重点，反而表现变差。你塞进去越多，有时候回答质量越低。\n三、RAG 的核心思路：先检索，再回答 直接塞数据行不通，那 RAG 到底怎么解决这个问题？\n核心思路其实非常简单，一句话就能说明白：\n不把所有数据都塞进 Prompt，而是在每次回答之前，先去找到最相关的那几段，再把这几段塞进 Prompt。\n📷 [图片 token=ZKYpbsMouomEdNxCyuPcYuy5njd（未能下载，见飞书原文）]\n这就是 RAG 名字的含义：\nRetrieval（检索）：从知识库里找出和当前问题最相关的内容\nAugmented（增强）：把检索到的内容加进 Prompt，增强大模型的上下文\nGeneration（生成）：大模型基于增强后的上下文生成回答\n打个考试时查字典的比方：你考试的时候不需要把整本字典背下来，你只需要知道去字典里查哪个词，查到了那一页，看完再写答案。\nRAG 就是这个思路，大模型不需要「记住」所有知识，它只需要在需要的时候，从知识库里检索到相关内容，然后基于这些内容来回答。\n四、RAG 的两大阶段 RAG 的整个工作流程分两个阶段：离线建库和在线检索生成。\n📷 [图片 token=V314bkDgsoXtIIxINzbc0Kfin4e（未能下载，见飞书原文）]\n离线建库是一次性的准备工作（数据更新时重做），在线检索是每次用户提问时触发的实时流程。\n离线阶段 这个阶段的目标是把你的私有数据，处理成大模型能快速检索的格式，存进向量数据库。一共四步：\n📷 [图片 token=PU8mbpn4noLOsoxeDe2ckt4rnSP（未能下载，见飞书原文）]\nStep 1：加载文档\n把你的数据源统一接入进来，PDF 文档、Word 文件、Markdown 笔记、网页内容、数据库记录、代码文件……各种格式的数据，统一解析成纯文本。\nStep 2：文本切块（Chunking）\n拿到文本之后，不能直接整篇存起来，要先切成小块。为什么要切？\n原因有两个：一是一篇文档几千字，里面涉及多个主题，检索的时候很可能只需要其中一个主题的内容，不需要整篇都召回；二是每个 chunk 要转化成向量存起来，chunk 太长，向量就无法精确代表那段文字的核心含义，检索准确率会变差。\n切块就像是图书馆整理书籍：不是把整本书放一个标签，而是按章节、按小节分别标注，这样读者想找某个具体知识点，能直接找到那几页，而不用翻整本书。\nStep 3：向量化（Embedding）\n这是 RAG 里最难理解的一步，咱们重点讲。\n切好的每个 chunk，要通过一个「Embedding 模型」转化成一串数字，这串数字就叫做向量（Vector）。\n这串数字有什么神奇之处？它编码了这段文字的语义含义。意思相近的文字，转化出来的向量在数学空间里距离也相近。\n用城市坐标来类比：每个城市都有经度和纬度，北京大约是北纬 40°、东经 116°；天津大约是北纬 39°、东经 117°，两组坐标数字很接近，在地图上两座城市确实挨着。广州是北纬 23°、东经 113°，坐标差别大，在地图上和北京也确实很远。\n向量就是文字的「语义坐标」，「数据库响应慢」和「查询性能问题」虽然词语不同，但意思相近，它们转化出来的向量在空间里也是挨着的；而「数据库响应慢」和「今天天气不错」意思毫不相关，向量距离就很远。\nStep 4：存入向量数据库\n转化好的向量，连同对应的原文，一起存进向量数据库（比如 Milvus、Pinecone、Weaviate 等）。向量数据库专门优化了「在海量向量里找最近邻」这个操作，能在几毫秒内从百万级向量里找出最相似的几条。\n在线阶段 每次用户提问，实时触发以下四步：\n📷 [图片 token=SwmCbDMPxoXKRFxzuMVcrj4xnYd（未能下载，见飞书原文）]\nStep 1：问题向量化\n用同一个 Embedding 模型，把用户的问题也转化成向量。\nStep 2：语义搜索\n拿着问题的向量，去向量数据库里找「最相似的几个 chunk 的向量」，这就是语义搜索。找到之后，把对应的原文 chunk 取出来，通常取 3-5 个最相关的片段。\n注意，这里找的不是「包含相同关键词」的内容，而是「语义最相近」的内容。这是语义搜索和传统关键词搜索的本质区别。\nStep 3：构造增强 Prompt\n把检索回来的几段原文，加上用户的原始问题，拼成一个增强后的 Prompt：\n以下是相关背景资料： [检索到的 chunk 1] [检索到的 chunk 2] [检索到的 chunk 3] 根据以上资料，请回答：[用户的原始问题] Step 4：大模型生成回答\n把这个 Prompt 发给大模型，大模型基于提供的背景资料生成有据可查的精准回答。\n五、关键概念再讲透：语义搜索和关键词搜索的差别 这里展开说一下语义搜索，因为这是 RAG 能工作的核心，也是最容易搞混的地方。\n传统关键词搜索（比如 SQL 的 LIKE 查询）：逐字匹配，你搜「数据库慢」，它就找包含「数据库慢」这几个字的文档，找不到就返回空。如果文档里写的是「query performance degradation」或者「查询响应时间过长」，关键词搜索就找不到，哪怕说的是同一件事。\n语义搜索：找意思相近的，不要求字面相同。你搜「数据库慢」，语义搜索能找到「查询性能问题」「SQL 响应延迟」「慢查询优化」这些片段，因为它们的向量距离近，语义上是相关的。\n再举一个对比例子：\nBqPwOe 用户的问题 文档里的原文 关键词搜索能找到？ 语义搜索能找到？ 数据库连接失败 connection pool exhausted ✗（没有相同关键词） ✓（语义相关） 如何重启服务 service restart procedure ✗ ✓ 昨晚报警原因 2024-01-15 23:00 alert: high latency ✗ ✓（如果时间能对上） 语义搜索让 RAG 能真正「理解」问题，而不只是机械地找字符串。这也是为什么 RAG 系统的检索准确率通常比普通关键词搜索高得多。\n六、完整流程图 把整个 RAG 的两阶段用图串起来：\n📷 [图片 token=AqaDb8GBsoumQcxPmfTck6qUn2d（未能下载，见飞书原文）]\n七、RAG 在 Agent 里处于什么位置？ 学完 RAG 的原理，你可能会问：它和我们之前学的 Agent、MCP、Function Calling 是什么关系？\n其实非常简单：RAG 本质上就是一个特殊的工具。\n还记得 Agent 章节讲的吗？Agent = 大模型（大脑）+ 工具（双手）+ 执行循环。「查询知识库」这个动作，被封装成了 Agent 里的一个 Tool。\n当 Agent 接到一个需要私有知识的任务，大模型通过 Function Calling 下达「调用知识库检索工具，查询：XXX」的指令，Agent 执行这个工具调用，触发 RAG 的在线流程，检索结果回传给大模型，大模型基于检索结果继续推理。\n这就是 mcp 和 agent 里预告 RAG 时说的：「把知识库检索封装成工具」。\n八、一张表看懂：没有 RAG vs 有 RAG nsqOiA 对比维度 没有 RAG 有 RAG 私有数据访问 无法访问，模型只有通用知识 可以访问，基于私有知识库生成回答 知识更新 需要重新训练模型，慢且贵 更新文档重新建库即可，快速生效 Token 消耗 要么全量塞入（超限），要么放弃 只召回相关的几段，token 消耗极低 回答准确性 依赖训练数据，私有领域容易瞎编 基于检索到的真实文档，有据可查 可追溯性 无法知道回答依据是什么 每个回答都可以附上来源文档 扩展性 知识库扩大 = 重新训练 直接往向量数据库里加数据即可 总结 整理一下这一章的核心认知：\nRAG 是什么：Retrieval-Augmented Generation，检索增强生成。先从知识库里找到相关内容，再把这些内容加进 Prompt，让大模型基于私有数据生成有据可查的回答。\n为什么需要它：大模型的知识是静态的、封闭的，而业务数据是动态的、私有的。把数据全量塞进 Prompt 行不通，超 token 限制、费钱、注意力稀释。RAG 用「按需检索 + 动态注入」绕开了这个死局。\n两大阶段：离线阶段建知识库（加载→切块→向量化→存库），在线阶段查询生成（问题向量化→语义搜索→构造增强 Prompt→大模型生成回答）。\n向量化和语义搜索：文字转化为代表语义的数字坐标（向量），语义相近的内容向量距离近。语义搜索不靠关键词匹配，靠语义相似度，能找到「意思相关但词语不同」的内容。\n在 Agent 里的位置：RAG 就是一个特殊的工具，「检索知识库」被封装成一次 Function Call，是 Agent 能力体系的重要组成部分。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%20RAG%EF%BC%9F/","summary":"前几章我们把 Agent 的工具体系搭清楚了：Function Calling 让大模型能开口下指令，MCP 让工具标准化接入，Skills 让专业知识按需加载。到这里，Agent 能干活了，工具也能跑起来了。   但有一个根本问题，前面几","title":"什么是 RAG？"},{"content":"在现代微服务架构下，每个技术团队都需要维护多个服务，日常工作中的一个主要负担就是处理海量的告警。这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，种类繁多。传统的处理方式往往依赖于人工轮值：工程师需要紧盯告警群，手动切换系统查日志、分析监控指标，最终才能判断问题的根源。整个过程重复、耗时，尤其在夜间或节假日，响应效率和准确性更是难以保障。\n运维 Agent 正是为了解决这些痛点而诞生的。它能像一位经验丰富的工程师那样，自动接收告警、按照预设步骤快速排查问题、分析根因并给出处理建议，甚至能够自动执行标准化的操作，从而将工程师从大量的重复性劳动中解放出来。\n📷 [图片 token=K3wObzNY5o5KsYxk17gc451AnDd（未能下载，见飞书原文）]\n核心价值：为什么我们需要运维 Agent？ 有人可能会问，人工处理告警虽然麻烦，但最终也能解决问题，为什么一定要引入 Agent 呢？这是因为人工处理在实际工作中存在几个关键的痛点：\n经验依赖与响应延迟 经验依赖性强： 人工排查极易遗漏关键信息，新人在面对告警群消息刷屏时，往往会因不熟悉业务或排查流程而手足无措。\n重复劳动与效率低下： 实际工作中，80%的告警都是如超时、限流之类的老问题。工程师每次都需要重复查日志、看监控的固定流程，效率非常低。\n夜间人力成本高昂： 为了实现 7×24 小时值守，团队不得不进行轮值排班，夜间值班容易疲劳。Agent 可以承担绝大多数常规告警的处理工作，大幅减少人工介入的频率。\n实现跨系统联动 在人工排查故障时，日志系统、监控平台和告警工具往往是割裂的。工程师可能需要先从告警消息中获取关键信息，然后手动切换到日志平台搜索，再打开监控面板查看指标，最后可能还需要在办公软件中询问下游团队。\n运维 Agent 可以通过调用各平台的 API，实现跨系统联动，一站式完成排查。例如，它可以自动从告警中提取接口名和时间范围，查询日志、获取下游错误率曲线，甚至调取历史故障复盘记录，将所有信息汇总成一份结构化的故障排查报告。\n沉淀经验，实现流程标准化 运维排查的核心逻辑其实是非常固定的。比如，处理接口失败时，通常会有一套固定的步骤：查最近响应日志 -\u0026gt; 看是否为上下文超时 -\u0026gt; 判断是偶发抖动还是持续故障 -\u0026gt; 决定观察还是联系下游。\n一个成熟的运维 Agent 能够将这些资深工程师的排查逻辑（如超时先看下游服务状态，特定错误码优先查错误码文档）固化为可执行的流程和规则。这不仅保证了每个告警都按最优路径被处理，避免人为失误，也使得团队知识不再依赖于口口相传，成员只需维护和迭代规则库即可，有效避免了知识断层。\nAgent 的典型应用场景与核心能力 Agent 的核心能力在于替代人工重复操作，通过日志监控联动 -\u0026gt;智能匹配原因 -\u0026gt;按步骤处理来提升响应速度和准确性。\n实时告警响应与排查 对于服务接口失败率突增、数据库连接池耗尽等紧急告警，Agent 可在秒级启动排查流程，比人工响应快得多。\n联动查询： Agent 能接收告警，并自动执行查询：\n调用日志 API 查询最近 1 小时包含特定错误关键词的日志。 调取监控系统该接口的失败率曲线图和相关指标。 将两者数据聚合，返回近 30 分钟 context cancel 错误占比 90%等关键信息。 智能匹配根因： 根据日志中的错误特征（如错误码 error_code），Agent 会匹配预设的错误码问题汇总文档：\n若错误码为 A -\u0026gt;匹配grpc 调用下游服务超时，建议持续观察 10 分钟。 若错误码为 B -\u0026gt;匹配XX 服务不可用，触发自动 @ 值班人员的操作。 周期性问题汇总 Agent 不仅能处理实时告警，也能承担周期性的统计工作。例如，它可以自动统计全周的错误码数据和原因分析，生成分类报告（如超时占比、下游依赖故障次数），省去人工整理周会汇报数据的时间。\n经验沉淀的自动化闭环 Agent 的价值会随着使用而增长。初期，它依赖人工编写的排查步骤和规则。但在每次处理完告警后，Agent 可以自动总结本次排查过程，并更新到问题知识库中，形成一个处理-沉淀-复用的闭环。用得越久，问题库越丰富，Agent 处理的准确率和速度就会持续提升。\n总结 总而言之，运维 Agent 的出现，不是要替代工程师，而是要成为工程师最可靠的助手。它用机器的快速和准确来解决重复性劳动，从而让人的经验和创造力能够聚焦在更复杂的系统优化和架构升级上。在一个微服务规模不断扩大、告警量爆炸的今天，一个能自动排查、分析、建议的运维 Agent，正在迅速成为保障后端服务稳定性的基础核心设施。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%EF%BC%9A%E8%BF%90%E7%BB%B4%E7%9A%84%E9%9C%80%E6%B1%82%E3%80%81%E5%9C%BA%E6%99%AF%E3%80%81%E4%BB%B7%E5%80%BC%E5%88%86%E6%9E%90/","summary":"在现代微服务架构下，每个技术团队都需要维护多个服务，日常工作中的一个主要负担就是处理海量的告警。这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，种类繁多。传统的处理方式往往依赖于人工轮值：工程师需要紧盯告警群，手动切换系统查日志、","title":"前置准备：运维的需求、场景、价值分析"},{"content":" 📷 [图片 token=ELS5btcapoAylgx5ajac0ivOnbh（未能下载，见飞书原文）]\n📷 [图片 token=GUMobyn31oGpQrxVloecO8ZVnyh（未能下载，见飞书原文）]\n从RAG召回到ReAct多轮交互 架构总览 对话Agent的核心目标是结合外部知识（RAG召回）与工具调用能力（ReAct模式），解决复杂问题。\n整体流程可概括为：\n用户输入 -\u0026gt; embedding -\u0026gt; 向量数据库召回\n构建带上下文(召回的内容)的 prompt\nReAct模式多轮交互\n最终输出答案\n📷 [图片 token=DFeObpyePosVkhxcyztcH4F0nAf（未能下载，见飞书原文）]\n核心流程拆解 📷 [图片 token=S8IkbCtNsohAt4xqZe9cDCehn53（未能下载，见飞书原文）]\n这个图现在看不懂没关系，在实战篇章会有讲解\nRAG召回：让Agent学习外部知识 目标：从向量数据库中获取与用户问题相关的上下文信息，避免大模型出现\u0026quot;幻觉\u0026quot;。\n步骤：\n用户输入经InputToRag Lambda Node处理，生成用于召回的字符串\n调用 Retriever 组件（以Milvus数据库为例），通过Embedding将问题向量化\n向量数据库执行相似度匹配，返回相关文档\n结果存入map[\u0026quot;documents\u0026quot;]，作为后续Prompt的上下文来源 。\nPrompt构建：动态拼接上下文与对话历史 目标：将用户输入、RAG召回内容、对话历史整合成大模型可理解的prompt。 prompt构建好后，将prompt移交给ReAct组件使用。\n核心组件：ChatTemplate\n输入：两个lambda node的输出合并 // 示例Prompt结构 SystemPrompt: \u0026#34;你是知识库助手，用{documents}回答问题，当前时间{date}\u0026#34; UserPrompt: \u0026#34;{content}\\n历史对话：{history}\u0026#34; 占位符设计： {content}：用户原始问题 {documents}：RAG召回的相关文档 {date}：当前时间（增强时效性） {history}：历史对话 ReAct模式：让Agent学会\u0026quot;思考-行动-观察\u0026quot;循环 目标：通过多轮工具调用解决复杂问题，核心是\u0026quot;显式思考→工具调用→结果观察\u0026quot;。\n循环四步骤：\nReason（思考）：大模型分析问题，判断是否需要调用工具（如\u0026quot;需要查实时数据→调用搜索工具\u0026quot;）。\nAction（执行）：返回工具调用请求（含函数名、参数，如{\u0026quot;name\u0026quot;:\u0026quot;Search\u0026quot;,\u0026quot;parameters\u0026quot;:{\u0026quot;query\u0026quot;:\u0026quot;2025世界杯冠军\u0026quot;}}，调用工具获取返回结果（如搜索到\u0026quot;阿根廷夺冠\u0026quot;）。\nObservation（观察）：工具结果返回给大模型，开始新一轮循环（若无需继续调用工具，则输出最终答案）。\n结束条件：大模型在Reason阶段判断\u0026quot;现有信息足够回答，无需工具\u0026quot;，循环终止。\n关键组件深析 Lambda Node：数据流转的\u0026quot;转换器\u0026quot; InputToRag：\n输入：用户原始问题（可自定义预处理，如过滤无关信息）。 输出：用于RAG召回的字符串（直接影响召回精度，需确保与向量数据库存储内容匹配）。 InputToChat：\n输入：用户问题+对话历史。 输出：map结构（含content/history等key），作为ChatTemplate的动态参数来源。 Retriever：向量召回的\u0026quot;连接器\u0026quot; 以Milvus实现为例，Retrieve方法核心逻辑：\nfunc (r *MilvusRetriever) Retrieve(ctx context.Context, input string) ([]*schema.Document, error) { // 1. 问题向量化 embedding, _ := r.embedding.Embed(ctx, input) // 2. 向量数据库查询（TopK相似度匹配） results, _ := r.client.Search(ctx, embedding, 5) // 取Top5相关文档 // 3. 格式转换为schema.Document return convertToDocuments(results), nil } Tool：Agent的\u0026quot;双手\u0026quot; Tool本质是带描述的函数，需明确告知大模型：\n函数名称（如\u0026quot;查询当前时间\u0026quot;）\n入参/返参格式（json来描述）\n使用场景（如\u0026quot;当问题涉及当前时间时调用\u0026quot;）\n例：定义一个时间工具\n// GetCurrentTimeInput 获取当前时间的输入参数（无需输入） type GetCurrentTimeInput struct { // 无需输入参数 } // GetCurrentTimeOutput 获取当前时间的输出结果 type GetCurrentTimeOutput struct { Success bool `json:\u0026#34;success\u0026#34; jsonschema:\u0026#34;description=Indicates whether the time retrieval was successful\u0026#34;` Seconds int64 `json:\u0026#34;seconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in seconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Milliseconds int64 `json:\u0026#34;milliseconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in milliseconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Microseconds int64 `json:\u0026#34;microseconds\u0026#34; jsonschema:\u0026#34;description=Current Unix timestamp in microseconds since epoch (1970-01-01 00:00:00 UTC)\u0026#34;` Timestamp string `json:\u0026#34;timestamp\u0026#34; jsonschema:\u0026#34;description=Human-readable timestamp in format \u0026#39;YYYY-MM-DD HH:MM:SS.microseconds\u0026#39;\u0026#34;` Message string `json:\u0026#34;message\u0026#34; jsonschema:\u0026#34;description=Status message describing the operation result\u0026#34;` } // NewGetCurrentTimeTool 创建获取当前时间的工具 func NewGetCurrentTimeTool() tool.InvokableTool { t, err := utils.InferOptionableTool( \u0026#34;get_current_time\u0026#34;, \u0026#34;Get current system time in multiple formats. Returns the current time in seconds (Unix timestamp), milliseconds, and microseconds. Use this tool when you need to retrieve current system time for logging, timing operations, or timestamping events.\u0026#34;, func(ctx context.Context, input *GetCurrentTimeInput, opts ...tool.Option) (output string, err error) { // 计算各种时间格式 seconds := now.Unix() // 构建输出 // ... // return }) return t } 总结：Agent能力的三角支柱 对话Agent的智能源于三方面协同：\nRAG召回：赋予外部知识记忆能力。\n动态Prompt：让大模型学习外部知识，并带有记忆(历史对话)。\nReAct模式：实现复杂任务拆解与工具调用。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%AF%B9%E8%AF%9DAgent%E7%9A%84%E6%A0%B8%E5%BF%83%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;ELS5btcapoAylgx5ajac0ivOnbh\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2070\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"架构设计：对话Agent的核心流程解析"},{"content":" 📷 [图片 token=G14Ob8vCgojif1xVaktcgL2wnPt（未能下载，见飞书原文）]\n规划-执行-重规划 运维Agent的核心目标是将运维人员的告警处理经验转化为自动化流程，通过 计划生成-\u0026gt;工具执行-\u0026gt;动态调整 的闭环，替代人工完成重复性告警排查工作。\n整体架构可概括为：\nPlanner生成结构化排查计划\nExecutor调用监控/日志工具执行步骤\nReplanner评估结果，决定继续执行/调整计划/输出结论\n📷 [图片 token=Nmb9bLOD2oH6ysxP07KcS2l3nDh（未能下载，见飞书原文）]\n核心流程拆解 计划生成（Planner）：将经验转化为结构化步骤 目标：基于告警类型和召回的运维手册，生成可执行的多步骤排查计划，替代人工凭经验梳理流程的过程。\n核心逻辑：\n输入：告警信息（alertname、description）+ 召回的处理文档\n输出：结构化计划（JSON格式），包含步骤描述、工具调用参数、预期结果\n示例计划结构：\n{ \u0026#34;goal\u0026#34;: \u0026#34;排查接口失败率过高问题\u0026#34;, \u0026#34;steps\u0026#34;: [ { \u0026#34;step_id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;action\u0026#34;: \u0026#34;query_log\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;根据接口名和\u0026#39;response\u0026#39;关键词搜索最近1小时日志\u0026#34;, \u0026#34;parameters\u0026#34;: {\u0026#34;service\u0026#34;: \u0026#34;ad_app\u0026#34;, \u0026#34;keyword\u0026#34;: \u0026#34;response error\u0026#34;, \u0026#34;time_range\u0026#34;: \u0026#34;1h\u0026#34;}, \u0026#34;expected_result\u0026#34;: \u0026#34;返回包含error信息的日志片段\u0026#34; }, { \u0026#34;step_id\u0026#34;: \u0026#34;2\u0026#34;, \u0026#34;action\u0026#34;: \u0026#34;analyze_error\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;解析日志中的error类型，判断是否为\u0026#39;context cancel\u0026#39;\u0026#34;, \u0026#34;parameters\u0026#34;: {\u0026#34;log_content\u0026#34;: \u0026#34;{{step1_result}}\u0026#34;}, \u0026#34;expected_result\u0026#34;: \u0026#34;明确错误原因（如超时/下游异常）\u0026#34; } ] } 计划执行（Executor）：调用工具完成单步排查 目标：通过集成监控/日志工具，自动执行计划中的步骤，替代人工 查日志/查监控 的操作。\n核心工具示例：\n告警查询工具（query_prometheus_alerts）：从Prometheus API获取当前活跃告警详情\n日志查询工具（query_log）：通过腾讯云CLS的MCP服务，用自然语言查询日志（如 搜索ad_app最近1小时的response error日志 ）\n动态重规划（Replanner）：评估进度并调整策略 目标：根据Executor的执行结果，判断是否需要调整计划（如增加步骤、终止排查），替代人工 持续关注/判断是否需进一步处理 的决策过程。\n评估逻辑：\n成功条件：当前步骤结果符合预期（如步骤1返回error日志，步骤2明确原因为 context cancel ），执行下一步\n调整条件：结果不符合预期（如日志查询无结果），触发计划修正（如扩大时间范围、更换关键词）\n终止条件：达到目标（如确认 下游发版导致临时超时，无需处理 ）或无法继续（如需人工介入下游沟通）\n示例重规划决策：\n// 步骤1执行结果：未查询到error日志 Replanner判断：可能关键词不准确 -\u0026gt; 修正计划步骤1的parameters为{\u0026#34;keyword\u0026#34;: \u0026#34;timeout OR cancel\u0026#34;, \u0026#34;time_range\u0026#34;: \u0026#34;2h\u0026#34;} // 步骤2执行结果：error类型为\u0026#34;context cancel\u0026#34;且持续时间\u0026lt;30分钟 Replanner判断：符合 下游发版临时问题 案例 -\u0026gt; 终止计划，输出结论 总结 运维Agent的自动化能力源于三方面协同：\n结构化规划：Planner将模糊的运维经验转化为可执行步骤，降低复杂告警的处理门槛\n工具化执行：Executor集成监控/日志系统，替代人工重复操作，提升响应速度\n动态的调整：Replanner根据实时结果修正计划，适配 下游发版/临时抖动 等不确定性场景\n通过这套架构，运维Agent可接管80%的重复性告警排查工作，让工程师聚焦于根因分析和流程优化。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9A%E8%BF%90%E7%BB%B4Agent%E7%9A%84%E6%A0%B8%E5%BF%83%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90/","summary":"\u0026lt;image token=\u0026ldquo;G14Ob8vCgojif1xVaktcgL2wnPt\u0026rdquo; width=\u0026ldquo;2618\u0026rdquo; height=\u0026ldquo;2074\u0026rdquo; align=\u0026ldquo;center\u0026rdquo;/  规划-执行-重规划 运维Agent的核心目标是将运维人员的告警处理","title":"架构设计：运维Agent的核心流程解析"},{"content":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：[小试牛刀：使用goframe框架3分钟实现一个http接口](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/Go 语言入门小实战/使用goframe框架3分钟实现一个http接口（Go）/)\n文件上传接口的定义 该接口用于上传文档到知识库中，便于后续召回使用\n请求方法：POST /api/upload（multipart/form-data）\nmultipart/form-data ：是 HTTP 请求的一种内容类型（Content-Type），用于在表单中上传文件或二进制数据。\n请求字段：\n字段名 类型 描述 响应字段：\n字段名 类型 描述 fileName string 保存的文件名 filePath string 文件保存路径 fileSize int64 文件大小（字节） 示例：\n# 用curl上传一个 Markdown 文件 # -F 参数会自动设置 multipart/form-data 格式 # @ 符号后面跟文件的绝对路径或相对路径 curl -X POST http://localhost:6872/api/upload \\ -F \u0026#34;file=@README.md\u0026#34; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;fileName\u0026#34;: \u0026#34;README.md\u0026#34;, \u0026#34;filePath\u0026#34;: \u0026#34;/path/to/saved/file/example.txt\u0026#34;, \u0026#34;fileSize\u0026#34;: 1024 } } 文件上传接口的核心实现(Go) 代码路径：SuperBizAgent/internal/controller/chat/chat_v1_file_upload.go\n首先将用户上传到文件保存到本地\nbuildIntoIndex然后去数据库中查找是否有元数据一样的文件，如果一样，则说明用户是更新文档，则先删除原来的数据。\n调用 knowledge_index_pipeline.BuildKnowledgeIndexing(ctx)创建知识库Agent的执行器\n调用 r.Invoke执行Agent，将文件向量化。（BuildKnowledgeIndexing在《RAG代码实战1》里有详细解释）\n最后构造返回结构体返回请求\nfunc (c *ControllerV1) FileUpload(ctx context.Context, req *v1.FileUploadReq) (res *v1.FileUploadRes, err error) { // 从请求中获取上传的文件 r := g.RequestFromCtx(ctx) uploadFile := r.GetUploadFile(\u0026#34;file\u0026#34;) if uploadFile == nil { return nil, gerror.New(\u0026#34;请上传文件\u0026#34;) } // 确保保存目录存在 if !gfile.Exists(common.FileDir) { if err := gfile.Mkdir(common.FileDir); err != nil { return nil, gerror.Wrapf(err, \u0026#34;创建目录失败: %s\u0026#34;, common.FileDir) } } // 获取原始文件名 newFileName := uploadFile.Filename // 完整的保存路径 savePath := filepath.Join(common.FileDir) // 保存文件 _, err = uploadFile.Save(savePath, false) if err != nil { return nil, gerror.Wrapf(err, \u0026#34;保存文件失败\u0026#34;) } // 获取文件信息 fileInfo, err := os.Stat(savePath) if err != nil { return nil, gerror.Wrapf(err, \u0026#34;获取文件信息失败\u0026#34;) } res = \u0026amp;v1.FileUploadRes{ FileName: newFileName, FilePath: savePath, FileSize: fileInfo.Size(), } err = buildIntoIndex(ctx, common.FileDir+\u0026#34;/\u0026#34;+newFileName) if err != nil { return nil, gerror.Wrapf(err, \u0026#34;构建知识库失败\u0026#34;) } return res, nil } func buildIntoIndex(ctx context.Context, path string) error { r, err := knowledge_index_pipeline.BuildKnowledgeIndexing(ctx) // 删除biz数据metadata中_source一样的数据 loader, err := loader2.NewFileLoader(ctx) if err != nil { return err } // 加载文件到内存 docs, err := loader.Load(ctx, document.Source{URI: path}) if err != nil { return err } cli, err := client.NewMilvusClient(ctx) if err != nil { return err } // 查询所有metadata中_source一样的数据并删除 expr := fmt.Sprintf(`metadata[\u0026#34;_source\u0026#34;] == \u0026#34;%s\u0026#34;`, docs[0].MetaData[\u0026#34;_source\u0026#34;]) queryResult, err := cli.Query(ctx, common.MilvusCollectionName, []string{}, expr, []string{\u0026#34;id\u0026#34;}) if err != nil { return err } else if len(queryResult) \u0026gt; 0 { // 提取所有需要删除的id var idsToDelete []string for _, column := range queryResult { if column.Name() == \u0026#34;id\u0026#34; { for i := 0; i \u0026lt; column.Len(); i++ { id, err := column.GetAsString(i) if err == nil { idsToDelete = append(idsToDelete, id) } } } } // 删除这些数据 if len(idsToDelete) \u0026gt; 0 { deleteExpr := fmt.Sprintf(`id in [\u0026#34;%s\u0026#34;]`, strings.Join(idsToDelete, `\u0026#34;,\u0026#34;`)) err = cli.Delete(ctx, common.MilvusCollectionName, \u0026#34;\u0026#34;, deleteExpr) if err != nil { fmt.Printf(\u0026#34;[warn] delete existing data failed: %v\\n\u0026#34;, err) } else { fmt.Printf(\u0026#34;[info] deleted %d existing records with _source: %s\\n\u0026#34;, len(idsToDelete), docs[0].MetaData[\u0026#34;_source\u0026#34;]) } } } // 重新构建 ids, err := r.Invoke(ctx, document.Source{URI: path}, compose.WithCallbacks(log_call_back.LogCallback(nil))) if err != nil { return fmt.Errorf(\u0026#34;invoke index graph failed: %w\u0026#34;, err) } fmt.Printf(\u0026#34;[done] indexing file: %s, len of parts: %d\\n\u0026#34;, path, len(ids)) return nil } 文件上传接口的核心实现(Java) 代码路径：SuperBizAgent/src/main/java/org/example/controller/FileUploadController.java\n首先将用户上传到文件保存到本地\n调用vectorIndexService.indexSingleFile对文件进行索引存储\n最后构造返回结构体返回请求\n@PostMapping(value = \u0026#34;/api/upload\u0026#34;, consumes = \u0026#34;multipart/form-data\u0026#34;) public ResponseEntity\u0026lt;?\u0026gt; upload(@RequestParam(\u0026#34;file\u0026#34;) MultipartFile file) { try { // 1. 首先将用户上传到文件保存到本地 Files.copy(file.getInputStream(), filePath); logger.info(\u0026#34;文件上传成功: {}\u0026#34;, filePath); // 文件上传成功后，自动调用向量索引服务 try { logger.info(\u0026#34;开始为上传文件创建向量索引: {}\u0026#34;, filePath); vectorIndexService.indexSingleFile(filePath.toString()); logger.info(\u0026#34;向量索引创建成功: {}\u0026#34;, filePath); } FileUploadRes response = new FileUploadRes( originalFilename, filePath.toString(), file.getSize() ); return ResponseEntity.ok(apiResponse); } } public void indexSingleFile(String filePath) throws Exception { // 1. 读取文件内容 String content = Files.readString(path); logger.info(\u0026#34;读取文件: {}, 内容长度: {} 字符\u0026#34;, path, content.length()); // 2. 删除该文件的旧数据（如果存在） deleteExistingData(path.toString()); // 3. 文档分片 List\u0026lt;DocumentChunk\u0026gt; chunks = chunkService.chunkDocument(content, path.toString()); logger.info(\u0026#34;文档分片完成: {} -\u0026gt; {} 个分片\u0026#34;, filePath, chunks.size()); // 4. 为每个分片生成向量并插入 Milvus for (int i = 0; i \u0026lt; chunks.size(); i++) { DocumentChunk chunk = chunks.get(i); try { // 生成向量 List\u0026lt;Float\u0026gt; vector = embeddingService.generateEmbedding(chunk.getContent()); // 构建元数据（包含文件信息） Map\u0026lt;String, Object\u0026gt; metadata = buildMetadata(path.toString(), chunk, chunks.size()); // 插入到 Milvus insertToMilvus(chunk.getContent(), vector, metadata, chunk.getChunkIndex()); } } logger.info(\u0026#34;文件索引完成: {}, 共 {} 个分片\u0026#34;, filePath, chunks.size()); } 文件上传接口的核心实现（Python） 代码路径：app/api/file.py\n验证文件名和扩展名（只允许 .txt / .md），规范化文件名（空格转下划线，过滤特殊字符）\n将文件内容保存到本地 uploads/ 目录，如果同名文件已存在则先删除（覆盖更新）\n调用 vector_index_service.index_single_file 对文件进行向量化索引存储\n构造响应结构体返回请求\n@router.post(\u0026#34;/upload\u0026#34;) async def upload_file(file: UploadFile = File(...)): # 1. 验证文件名 if not file.filename: raise HTTPException(status_code=400, detail=\u0026#34;文件名不能为空\u0026#34;) # 2. 规范化文件名（去除空格和特殊字符） safe_filename = _sanitize_filename(file.filename) # 3. 验证文件扩展名（仅支持 txt / md） file_extension = _get_file_extension(safe_filename) if file_extension not in ALLOWED_EXTENSIONS: raise HTTPException(status_code=400, detail=f\u0026#34;不支持的文件格式，仅支持: {\u0026#39;, \u0026#39;.join(ALLOWED_EXTENSIONS)}\u0026#34;) # 4. 确保上传目录存在 UPLOAD_DIR.mkdir(parents=True, exist_ok=True) # 5. 保存文件（如果已存在则覆盖） file_path = UPLOAD_DIR / safe_filename if file_path.exists(): logger.info(f\u0026#34;文件已存在，将覆盖: {file_path}\u0026#34;) file_path.unlink() content = await file.read() if len(content) \u0026gt; MAX_FILE_SIZE: # 最大 10MB raise HTTPException(status_code=400, detail=\u0026#34;文件大小超过限制（最大 10MB）\u0026#34;) file_path.write_bytes(content) logger.info(f\u0026#34;文件上传成功: {file_path}\u0026#34;) # 6. 自动创建向量索引（即使索引失败文件上传依然成功） try: vector_index_service.index_single_file(str(file_path)) logger.info(f\u0026#34;向量索引创建成功: {file_path}\u0026#34;) except Exception as e: logger.error(f\u0026#34;向量索引创建失败: {file_path}, 错误: {e}\u0026#34;) # 7. 返回响应 return JSONResponse(status_code=200, content={ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;filename\u0026#34;: safe_filename, \u0026#34;file_path\u0026#34;: str(file_path), \u0026#34;size\u0026#34;: len(content), }, }) index_single_file 是向量索引的核心，接收文件路径后完成读取 → 删旧 → 分片 → 入库全流程：\ndef index_single_file(self, file_path: str): path = Path(file_path).resolve() # 1. 读取文件内容 content = path.read_text(encoding=\u0026#34;utf-8\u0026#34;) logger.info(f\u0026#34;读取文件: {path}, 内容长度: {len(content)} 字符\u0026#34;) # 2. 删除该文件的旧数据（按 metadata[\u0026#34;_source\u0026#34;] 匹配删除） normalized_path = path.as_posix() vector_store_manager.delete_by_source(normalized_path) # 3. 文档分片（Markdown 按标题两阶段切分 + 合并小片） documents = document_splitter_service.split_document(content, normalized_path) logger.info(f\u0026#34;文档分割完成: {file_path} -\u0026gt; {len(documents)} 个分片\u0026#34;) # 4. 批量向量化并写入 Milvus（LangChain 自动处理，无需手动循环） if documents: vector_store_manager.add_documents(documents) logger.info(f\u0026#34;文件索引完成: {file_path}, 共 {len(documents)} 个分片\u0026#34;) ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9AAPI%E6%8E%A5%E5%8F%A3%E4%B8%8EAgent%E7%9A%84%E6%95%B4%E5%90%88/","summary":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：\u0026lt;mention-doc token=\u0026ldquo;FMMRwPNVZiRiqSkTTF8cQhOanyb\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 小试牛刀：使用goframe框架3分","title":"源码分析：API接口与Agent的整合"},{"content":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。\n核心代码目录：src/main/java/org/example/agent/tool\n当前时间查询工具 按照ai框架的规范用@Tool来表示工具：\n第一个参数是toolName，用于表示工具名\n第二个参数是toolDesc，用于告诉大模型这个工具的功能\n@Component public class DateTimeTools { @Tool(name = \u0026#34;getCurrentDateTime\u0026#34;, description = \u0026#34;Get the current date and time in the user\u0026#39;s timezone\u0026#34;) public String getCurrentDateTime() { return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); } } 腾讯云日志MCP工具 通过 MCP Server 查询日志服务 CLS 中存储的日志数据，以实现大模型平台/工具与日志数据的结合。例如使用自然语言查询日志，降低日志查询复杂度 https://cloud.tencent.com/developer/mcp/server/11710\nMCP配置：[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\nJava代码的MCP使用了Spring AI的能力。（QueryLogsTools，它只是本地模拟工具，不是真实的MCP）\n也就是说：\n业务代码没有 new McpClient(...)\nMCP Client 是由 Spring AI 自动配置创建的\n触发自动配置的是 pom.xml 里的依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.ai\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-ai-starter-mcp-client-webflux\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 实际链路是：\nspring-ai-starter-mcp-client-webflux ↓ Spring Boot AutoConfiguration ↓ 创建 MCP Client / MCP Session / ToolCallbackProvider ↓ 注入到 ChatService.tools ↓ tools.getToolCallbacks() ↓ ReactAgent.builder().tools(...) getToolCallbacks() 这里没有创建 MCP Client。它只是使用 Spring 容器里已经自动创建好的 Bean：\n@Autowired private ToolCallbackProvider tools; /** * 获取工具回调列表，mcp服务提供的工具 */ public ToolCallback[] getToolCallbacks() { return tools.getToolCallbacks(); } ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AD%E7%AB%A0%EF%BD%9CTool%20%E5%92%8C%20MCP%20%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ATool%20%E5%92%8C%20MCP%20%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%98%28Java%29/","summary":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。 核心代码目录：src/main/java/org/example/agent/tool  当前时间查询工具 按照a","title":"源码分析：Tool 和 MCP 代码实战(Java)"},{"content":"大模型应用开发框架选型 大模型应用开发框架是连接业务逻辑与大模型能力的桥梁，直接影响开发效率、系统稳定性和迭代速度。选择合适的框架，能让团队避免重复造轮子，聚焦核心业务创新。反之，框架选型不当可能导致后期维护成本激增、性能瓶颈难突破。\n大模型应用的核心诉求：我们需要什么？ 选择框架前，先明确大模型应用的核心需求——不止是能用，更要好用、可靠、能落地\n开发效率：能否快速搭建复杂流程（如 ReAct Agent、多工具调用），是否有可视化工具辅助调试\n系统稳定性：高并发场景下是否会崩溃，类型错误能否提前发现，长期迭代后代码是否依然清晰\n企业级适配：是否支持链路追踪、性能监控？能否无缝对接内部中间件（如日志系统、配置中心）\n主流大模型应用开发框架类型 当前大模型应用开发框架主要分为两类：Python 生态框架（如 LangChain、LlamaIndex、LangGraph）和** 强类型编译型框架**（如 Go 语言的 Eino）。两类框架各有侧重，没有绝对优劣，只有是否适配场景的区别。\nPython 生态框架：快速上手但长期维护成本高 https://www.bilibili.com/video/BV1ZppNzHEY4\nhttps://www.langchain.com/\nPython 系框架是目前的主流选择，凭借 Python 在 AI 领域的生态优势，快速积累了大量用户。\n优点：\n生态丰富：集成了几乎所有主流大模型、工具和数据处理组件，开箱即用。\n上手门槛低：动态类型+简洁语法，适合快速验证想法，初学者能在几小时内搭建简单 Agent。\n社区活跃：教程、插件、案例丰富，遇到问题容易找到解决方案。\n缺点：\n长期维护困难：动态类型缺乏编译时校验，变量类型需通读上下文确认，大型项目代码可读性差、重构风险高（比如一个参数类型错误可能线上才暴露）。\n性能瓶颈：Python 并发能力很差，在高并发高流量场景下需额外引入多进程/多线程管理，在性能和并发处理上的性能远不如Go\nGo 系框架（Eino）：强类型驱动的企业级选择 https://www.cloudwego.io/zh/docs/eino/\nEino 已成为字节跳动内部大模型应用的首选全代码开发框架，已有包括豆包、抖音、扣子等多条业务线、数百个服务接入使用：\n核心优势：\n内核稳定，API 简单易懂，有明确的上手路径，平滑的学习曲线。\n极致的扩展性，研发工作高度活跃，长期可持续。\n基于强类型语言 Go，代码能看懂，易维护，高可靠。\n背靠字节跳动核心业务线的充分实践经验。\n提供开箱即用的配套工具。\nJava系框架（Spring AI Alibaba）：强类型驱动的企业级选择 https://java2ai.com/docs/overview\nSpring AI Alibaba 是构建 Agent 智能体应用最简单的方式，只需不到 10 行代码就可以构建您的智能体应用。\n核心优势：\n内核稳定，API 简单易懂，有明确的上手路径，平滑的学习曲线。\n极致的扩展性，研发工作高度活跃，长期可持续。\n基于强类型语言 Java，代码能看懂，易维护，高可靠。\n背靠阿里巴巴核心业务线的充分实践经验。\n提供开箱即用的配套工具。\nSpring ai alibaba本质上和eino没有区别，都是强类型语言的大模型应用开发框架\n适用场景分析 选择Eino的场景：\n高性能生产环境：需要处理高并发请求\n强类型要求：需要编译时类型安全\nGo技术栈：团队主要使用Go语言\n微服务架构：需要与其他Go服务深度集成\n选择SpringAIAlibaba的场景：\n高性能生产环境：需要处理高并发请求\n强类型要求：需要编译时类型安全\nJava技术栈：团队主要使用Java语言\n微服务架构：需要与其他Java服务深度集成\n选择LangChain的场景：\n快速原型开发：需要快速验证想法\n研究实验：频繁尝试新的模型和技术\nPython生态：团队熟悉Python和相关库\n社区资源：需要大量的示例和教程\n总结 大模型应用开发框架的选型，本质是**“短期效率”与“长期可靠性”的平衡**。SpringAI/Eino 虽在生态覆盖度上略逊于 Python 系的框架，但其 强类型带来的稳定性、Graph 编排的开发效率、字节实践的可靠性，更适合企业级生产落地。\n如果你需要快速搭建可靠的大模型应用，且团队熟悉 Java/Go 语言，SpringAI/Eino 会是“既能解决当下问题，又能支撑未来迭代”的最优选择。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%8C%E7%AB%A0%20_%20%E9%A1%B9%E7%9B%AE%E5%85%A8%E5%B1%80%E8%AE%A4%E7%9F%A5%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/%E9%80%89%E5%9E%8B%E5%88%86%E6%9E%90%EF%BC%9A%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6/","summary":"大模型应用开发框架选型 大模型应用开发框架是连接业务逻辑与大模型能力的桥梁，直接影响开发效率、系统稳定性和迭代速度。选择合适的框架，能让团队避免重复造轮子，聚焦核心业务创新。反之，框架选型不当可能导致后期维护成本激增、性能瓶颈难突破。  大","title":"选型分析：大模型应用开发框架"},{"content":"[小组件 (type: blk_637dcc698597401c1a8fd711)]\n[!TIP] 如果各位林友用《智能OnCall Agent项目》去面试的时候，遇到了本文档没有的面试题，希望导师扩充的，欢迎登记这个表格：\n项目面试题登记：https://icnaxnmh86kx.feishu.cn/share/base/form/shrcnIrmLbgpaH5gCl5iKXoYKTh 后续导师会根据同学们的反馈新增题目，一起共建题库！ [!NOTE] Agent 八股面试题，可作为额外学习内容\nhttps://xiaolinnote.com/ai/（大模型面试题） https://xiaolinnote.com/agent/（图解Agent） https://xiaolinnote.com/claudecode/（图解Claude Code） 林友们反馈的真实面经记录： [面经分享记录](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/面经分享记录/) 简单介绍一下这个项目 这个项⽬源于我们团队内部的⼀个真实痛点，传统的OnCall依赖⼈⼯值守和排查问题，响应慢且占⽤⼤量研发精⼒。\n就比如之前上游同事天天问同一个问题报错怎么解决，明明文档里写了解决方案还反复问。把时间耗在重复回答上，非常的打杂，所以我就思考怎么样能主动去突破，做一些高价值工作。\n其实这两年AI非常火嘛，我就在想能不能用AI技术，打造⼀个智能化的OnCall助⼿，让它能⾃动回答常见问题，并在故障发⽣时主动进⾏初步排查。\n首先我基于Eino框架设计搭建了RAG知识库来让AI能基于内部⽂档回答问题，然后实现了⼏类Agent，⽐如⽤于对话的对话Agent和⽤于运维排障的运维Agent。\n最终，系统上线后，很多重复性的咨询和告警都能由Agent⾃动处理并给出解决方案，显著减轻了值班的负担。\n[!WARNING] 如果你没有实习，就回答是为了学习Agent做的项目/导师的课题等，千万不要打肿脸充胖子，实习 面试官一般不会太挑战你的项目立意的\n简单说说Eino是什么框架 Eino框架是字节跳动开源的一个大模型应用开发框架，核⼼思想是⽤图Graph来定义Agent的⼯作流。\n在我的理解⾥，图中的每个节点代表⼀个原⼦能⼒，⽐如⼤模型节点、召回使用的Retriever节点等。\n边则代表了这些节点之间的执⾏顺序和数据流向。\n在项⽬中，我⽤Eino来构建不同的Agent。⽐如ChatAgent，把召回、Prompt构建、ReAct定义成节点，然后⽤边把它们连起来。这样，整个Agent的执行过程就变得⾮常清晰和可配置。\n选择Eino主要是看中它这种直观的图编排能⼒和对⼯作流状态的维护，让我们能相对容易地构建和调试复杂的Agent。\n简单说说Spring-AI-Alibaba是什么框架 Spring-AI-Alibaba是阿里开源的一个大模型应用开发框架，面向Java开发者。\n是一个一站式 Agent 平台，支持可视化 Agent 开发、可观测、 MCP 管理等。它还与 Dify 等开源低代码平台集成。\n在项⽬中，我⽤这个框架来构建不同的Agent。⽐如ChatAgent，把召回、Prompt构建、ReAct定义成节点，然后⽤边把它们串联起来。这样，整个Agent的执行过程就变得⾮常清晰和可配置。\n选择Spring-AI-Alibaba主要是看中它这种直观的编排能⼒和对⼯作流状态的维护，让我们能相对容易地构建和调试复杂的Agent。\n为什么选择用Eino，不用langchain 这个其实要从它们适合的场景来分析\n选择Eino的场景：\n高性能生产环境：需要处理高并发请求 强类型要求：需要编译时类型安全 Go技术栈：团队主要使用Go语言 选择LangChain的场景\n快速原型开发：需要快速验证想法 社区资源：需要大量的示例和教程 Python生态：团队熟悉Python和相关库 在实际项目中，技术选型应该基于团队的技术栈、项目需求、性能要求和长期维护考虑。\n对于追求性能和稳定性的生产环境，Eino提供了优秀的Go原生解决方案。\n而对于快速迭代和研究型项目，LangChain的灵活性和生态优势更加明显。\nEino 虽在生态覆盖度上略逊于langchain，但其强类型带来的稳定性、Graph 编排的开发效率、字节实践的可靠性，更适合企业级生产落地。\n为什么选择用Spring-AI-Alibaba，不用langchain 这个其实要从它们适合的场景来分析\n选择Spring-AI-Alibaba的场景：\n高性能生产环境：需要处理高并发请求 强类型要求：需要编译时类型安全 java技术栈：团队主要使用java语言 选择LangChain的场景\n快速原型开发：需要快速验证想法 社区资源：需要大量的示例和教程 Python生态：团队熟悉Python和相关库 在实际项目中，技术选型应该基于团队的技术栈、项目需求、性能要求和长期维护考虑。\n对于追求性能和稳定性的生产环境，Spring-AI-Alibaba提供了优秀的Java原生解决方案。\n而对于快速迭代和研究型项目，LangChain的灵活性和生态优势更加明显。\nSpring-AI-Alibaba虽在生态覆盖度上略逊于langchain，但其强类型带来的稳定性、Graph 编排的开发效率、实践的可靠性，更适合企业级生产落地。\n把Eino的话术换一下就行了，Go和Java都是强类型要求，话术是没区别的\n简单介绍一下你设计的几个Agent 好的，在项目里一共实现了3个Agent，分别是知识库Agent、对话Agent和运维Agent。\n知识库Agent的核心目标是作为团队文档管理和AI应用的基础设施，通过自动化流程，将我们日常积累的文档(告警处理手册、技术方案、错误码文档)，转化为可被AI高效检索的向量，为后续的RAG提供了高质量的向量数据支撑。举个最常见的例子，当我们想根据一个模糊的回忆找文档的时候，可以根据模糊的提问快速检索到对应文档，不再需要再嵌套目录里面一个一个翻了。\n对话Agent本质上是一个基于大模型+知识库构造的智能交互系统。你可以把它看作是一个能够像真人一样理解问题、调用知识库检索并给出精准回答的小助手。它最重要的使命就是帮助团队挡掉高频的重复咨询，加速问题解决，从而提高整体的工作效率。\n运维 Agent 是为了解决值班排查问题的痛点而做的。我们团队维护了多个服务，这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，告警太多了。传统的处理方式往往依赖于人工排查：看告警、查日志、查监控，最终才能判断问题的根源。整个过程重复、耗时，特别是在晚上或节假日，响应效率和准确性更是难以保障，值班的时候告警一多就很痛苦。运维 Agent 可以通过调用各平台的 API，实现跨系统联动，一站式完成排查。例如，它可以自动从告警中提取接口名和时间范围，查询日志、查询监控、查询告警处理手册，将所有信息汇总成一份结构化的故障排查报告。\n知识库的使用场景是什么 知识库的使用场景主要围绕团队知识的高效复用与AI应用召回能力的支撑，比如以下这些场景：\n对话Agent在处理业务方咨询时，比如询问某个API接口怎么接，有哪些字段。那么Agent会通过知识库的相似性检索能力，快速定位文档中的关键内容，如XX服务API接口手册。让大模型根据召回的文档内容，生成精准回答。避免大模型因为缺少这部分的知识而出现幻觉乱回答。\n赋能运维Agent的故障排查场景：运维Agent查询到告警后，会调用知识库召回错误码、告警信息与历史处理经验。比如遇到特定错误码时，知识库能返回文档里面写的错误码对应的错误原因，帮助Agent快速分析根因。\n这些场景的核心是让静态文档转化为动态可调用的东西，既提升大模型输出的准确性，也解放团队人力，再也不需要手动翻文档了。\n对话Agent的使用场景是什么 对话Agent的核心是用 大模型+知识库+ReAct模式 替代人工，处理高频重复的交互场景，帮团队从琐事中解放出来。结合我们团队的落地实践，主要有几个典型场景：\n业务支持场景：上游业务同事经常会问：这个API为什么报错、你们这个接口怎么接；即使文档里有解决方案也总找人工。值班的时候80%时间都在当全职客服。现在把技术文档、告警手册、接口文档上传到知识库后，让对话Agent通过RAG技术快速回复业务方，响应时间从分钟变成秒回。只要我文档里面写的足够清楚，那你就不要再来重复问我了，再问就diss你。解决相同问题被重复询问和文档看都不看直接来问你的痛点。\n值班自救场景：告警群收到告警后，每次翻手册比较麻烦，还容易遗漏步骤。现在把告警信息发给Agent，它能快速检索《告警处理手册》里的标准化方案。并且对话Agent里面还使用到了ReAct模式，我们还可以问它最近5分钟内的XX接口的错误日志是什么，Agent能调用日志查询工具进行查询，并结合知识库里面的文档给出分析。\n工单预处理场景：其实对话Agent还可以对接工单平台。原来新工单进来，研发要逐条查看，80%的简单问题（比如常见报错，他们就是喜欢动不动提工单，而工单还必须要解决）占用大量时间。根据对话Agent的特点，工单进行先召回一遍历史工单记录，如果遇到相同问题直接AI自动回复，提前过滤掉重复工单。只把复杂问题流转给人工，让团队聚焦真正需要人工解决的疑难杂症。\n这些都是我们团队的真实痛点转化来的场景，有了Agent确实帮我们从很多琐事中解放出来。\n你刚才提到RAG，详细说下RAG吧 RAG是构建智能客服、企业知识库、产品问答助手的核心技术，RAG全称是Retrieval-Augmented Generation，也就是检索增强生成。\n当你需要让大模型回答特定领域问题（如内部文档）时，直接将长文本发送给模型，会受限于模型的上下文窗口大小，导致成本高、速度慢、准确率低。RAG通过先检索相关内容再生成答案的方式，完美解决了这些问题，在对话Agent和运维Agent里，都会使用到RAG。\n具体流程是：用户提出问题后，系统先从知识库检索相关的真实信息，再把这些检索到的可靠知识和问题一起输入给生成模型，让模型基于这些有据可依的内容生成回答。这样输出的结果不仅更准确，还能追溯信息来源，也能覆盖模型训练数据之外的新知识\nRAG的核心流程分为两部分\n提问前（离线处理，数据准备）分为3个步骤：分片 -\u0026gt; embedding -\u0026gt; 存储 提问后（在线处理，回答生成）分为3个步骤：召回 -\u0026gt; 重排 -\u0026gt; 生成 问：需要我详细介绍一下每个步骤吗？\n详细步骤见《RAG全流程解析》，这里就不再赘述了。基本上你把每个步骤的概念介绍出来就行了。\n你刚才提到ReAct，详细说下ReAct吧 ReAct的本质是Reasoning推理+ Acting行动。核心是让大模型像人类一样，通过先思考，再行动来解决复杂的问题。传统的大模型只能进行你问我答，无法处理任务，ReAct解决了这个问题，它会先思考问题，然后通过调用工具的处理任务。\n举个例子：问地球和火星质量相加是多少，ReAct会进行多轮循环如下：\n第1次循环： 思考：我需要地球和火星的质量，得先查地球质量。 行动：调用 查星球质量 工具，输入 地球 观察：工具返回 地球质量≈5.97×10²⁴ kg 第2次循环： 思考：有了地球质量，还缺火星的，继续查火星。 行动：调用 查星球质量 工具，输入 火星 观察：工具返回 火星质量≈6.42×10²³ kg 第3次循环： 思考：现在有两个数了，需要相加，得用计算器。 行动：调用 计算器 工具，输入 5.97e24 + 6.42e23 观察：工具返回 ≈6.61×10²⁴ kg 最终输出答案，循环终止。 ReAct就是让大模型执行这个过程：遇到问题不直接瞎猜答案 ，而是先想该查什么，再调用工具，拿到结果后判断够不够回答，不够就继续查，直到能给出最终答案。\nReAct核心优势是让Agent能实时决策、动态调用工具，处理需要外部信息支撑的复杂问题。例如对话Agent中，用户问某个req id的error日志，ReAct能拆分步骤并调用日志工具获取日志信息。\n运维Agent的使用场景是什么 运维 Agent 是为了解决值班排查问题的痛点而做的。我们团队维护了多个服务，这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，告警太多了。\n传统的处理方式往往依赖于人工排查：看告警、查日志、查监控，最终才能判断问题的根源。整个过程重复、耗时，特别是在晚上或节假日，响应效率和准确性更是难以保障，值班的时候告警一多就很痛苦。\n运维 Agent 主要是使用了Plan-Execute-Replan设计模式，自动规划排查步骤，通过调用各平台的 API，实现跨系统联动，一站式完成排查。\n一方面要回答问题，另一方面要引导面试官问一个问题，所以要重点强调Plan-Execute！\n例如，它可以自动从告警中提取接口名和时间范围，查询日志、查询监控、查询告警处理手册，将所有信息汇总成一份结构化的故障排查报告。 如果面试官不追问不要说下面的场景\n主要覆盖以下几个高频实用场景： 实时告警的自动响应与智能排查：面对服务接口失败率大于80%等紧急告警时，Agent能秒级启动结构化流程。它会先规划排查步骤：1. 查最近1小时错误日志 2. 若日志无异常则动态调整计划 3. 若日志有异常，根据错误信息召回处理手册信息。替代人工反复切换系统的操作，即使遇到预设外的情况也能自主推进。 跨系统联动的故障根因分析：针对日志、监控、告警等信息分散的问题，Agent能统一调用多系统工具，聚合数据。比如接口超时告警时，它会结合告警信息，自动查询日志中的context cancel错误，快速定位是否为下游依赖故障，避免人工在多个平台间来回切换。 经验沉淀的自动化闭环：每次处理完告警后，Agent会自动总结排查过程并更新到知识库，形成处理-沉淀-复用的循环。例如某次解决了grpc调用下游超时问题后，Agent会将该案例存入知识库，后续遇到同类告警可直接复用方案，用得越久准确率越高。 你刚才提到的Plan-Execute-Replan是什么意思 Plan-Execute-Replan 是 Agent 的结构化任务执行模式。核心是先规划执行步骤，再按步骤行动，随时校准方向，通过规划，执行，评估，让 Agent 像人类一样拆解复杂任务、稳步推进，还能应对突发变化。\n这个模式靠三个子Agent协同：\nPlanner（规划）：任务拆解者，把模糊目标转化为结构化步骤，关键能力是理解复杂逻辑、生成清晰步骤 Executor（执行）：工具执行者，只专注执行规划出来的第一步，不负责整体规划，核心是准确调用工具 Replanner（重规划）：进度监理，评估执行结果。如果结果有效则推进执行下一步；如果结果异常，则调整计划；如果计划执行完成，则终止并返回结果。 举个例子：服务器凌晨突发CPU使用率100%告警，运维Agent自动排查根因：\nPlanner接到排查CPU突增根因的目标后，结合运维经验生成结构化计划： 步骤1：调用日志工具，查询服务器近1小时error/warn级别日志（重点看进程崩溃、资源争抢记录） 步骤2：调用监控工具，获取CPU使用率突增时段的进程占用排行（定位高耗CPU进程） 步骤3：调用历史工单手册，检索该进程过往CPU异常的处理方案（匹配已知问题） Executor按计划启动第一步，调用日志工具\nReplanner分析执行结果：日志无异常，说明问题可能不在应用错误，需优先定位高耗CPU进程，于是调整计划顺序：\n【更新后计划】 步骤1：调用监控工具，获取CPU使用率突增时段（02:00-02:10）的进程占用排行（优先定位异常进程） 步骤2：调用日志工具，查询步骤1中高耗CPU进程的近1小时详细日志（针对性排查该进程行为） 步骤3：调用历史工单手册，检索该进程过往CPU异常的处理方案 继续执行与动态调整，Executor 执行更新后的步骤1，调用监控工具 CPU突增时段（02:00-02:10），进程「data-sync-service」占用率达95%（正常时段通常\u0026lt;10%） Replanner 再次评估，已定位异常进程，需进一步查该进程日志确认原因，计划无需调整，继续执行步骤2。\nExecutor 调用日志工具，参数更新为进程名=data-sync-service，时间范围=近1小时，返回结果：\n日志显示02:00触发全量数据同步任务，遍历数据库1000万条记录，未做分页处理 Replanner 最终评估已明确根因：全量同步任务未分页导致CPU过载，无需继续执行步骤3（因问题已定位，且历史工单中类似场景解决方案明确），终止任务并返回结论。\n最终输出结果\n故障根因：服务器进程data-sync-service在02:00执行全量数据同步时，未做分页处理，遍历1000万条记录导致CPU使用率突增。 建议方案：优化同步逻辑，添加分页参数（如每次拉取1000条），并设置非高峰时段执行。 核心逻辑是结构化推进+动态校准，流程如下：\n输入目标：排查CPU突增根因 Plan：Planner生成结构化步骤 Execute：Executor执行第一步 Replan：Replanner评估结果，决定继续/调整/终止 循环：直到任务完成，返回最终结论 核心优势：让复杂任务可控又灵活\n结构化拆解：把一团乱麻的复杂任务拆成步步可执行的步骤，降低认知负荷。 动态适应变化：遇到执行问题时，Replan 机制能及时调整计划，避免一条道走到黑。 ReAct和Plan-Execute-Replan有什么区别？ ReAct适合边想边做，没有固定步骤\nPlan-Execute-Replan适合处理复杂任务，先拆解步骤，再执行，根据执行结果判断要不要修改计划\n所以在我的项目中，对话场景用ReAct，因为用户问题灵活，需实时决策。它的职责是处理开放式的，多轮的业务咨询，⽐如回答某个API怎么用，特点是灵活，能根据对话历史动态决定下⼀步做什么，但执⾏链条不会预设得特别长。\n运维场景用Plan-Execute-Replan，⽐如专门处理告警。它的职责是接受⼀个明确⽬标，然后制定⼀个可能包含多步骤的排查计划，并严格按计划调⽤各⼯具执⾏。\n它们底层共享同⼀套⼯具集和知识库，但⼯作模式和⽬标不同。\n我看你提到运维响应时间从小时级降低到分钟级，这个数据是怎么来的？ 这个数据的验证我们主要做了两⽅⾯⼯作。\n⼀是回顾性分析，我们统计了我们组前三个月的工单和平均解决时间，确实在1～4小时之间，这包括等待处理，排查问题，一直到处理结束。\n二是使用Agent后的效果统计，我们写脚本把工单问题让AI进行处理，从触发到生成解决方案平均在2～5分钟之间。\n简历里提到的文档分块大小，和检索topK参数是怎么选择的？ 文档分块策略采用按照一级标题切分，topK选择top3的数据。\n我的验证⽅法是数据驱动的。⾸先我从历史⼯单和常见问题中整理出了⼀套包含⼏⼗个问题的测试集，并为每个问题标注了相关的标准答案⽂档⽚段。\n然后我开始调整两个参数：⽂档分块策略和检索返回的TopK数量。\n我尝试了不同的分块策略，⽐如按⾃然段落分块，以及固定256、512、1024等不同长度进⾏分块。\n对于每个分块⽅案，再搭配不同的TopK值（⽐如3，6，9）进⾏组合测试。\n我主要看两个核⼼指标：⼀是检索到的⽂档块是否包含了标准答案，⼆是排名第⼀的⽂档块是否最相关。\n最终，我找到了⼀个平衡点：采⽤按语义段落为主的分块⽅式，并设置TopK=3。\n当然我觉得这也和我们预先清洗了一遍文档有关，以前大家写文档可能随心所欲的写，语义相关的信息可能比较散。现在我们就规范要求大家，对于同一个问题尽量写在一个段落里面。不要东写一点，西写一点。\n简历里提到的多轮对话和上下文记忆是什么意思？ 多轮对话就是指，和大模型进行多轮沟通，大模型能记得之前沟通的内容。\n那么历史沟通的对话就是上下文，我们设计一个机制保持上下文，在新一次对话的时候，把上下文发送给大模型，这样大模型就仿佛拥有了记忆。\n项目里我设计了分层的记忆管理机制。最简单的最近⼏轮对话，我们直接放在内存里，作为短期记忆，这能保证对话的连贯性。\n当对话轮数增多，我们会把之前的对话内容进⾏处理，比如对前30轮历史对话进⾏总结摘要，作为长期记忆。\n这样，模型就能同时记住最近聊了什么，以及过去聊的关键信息。\n对历史对话做总结的目的，主要是为了避免太多的对话超过大模型的上下文窗口。\n大模型上下文窗口就是一次对话中，能够记住和处理的信息总量的上限。说白了就是输入token+输出token的最大限制。\n你提到通过SSE技术实现对话的流式输出，为什么不用websocket？ SSE和WebSocket都是实现实时通信的技术，但原理和适⽤场景不同。SSE，也就是服务器发送事件，它是基于HTTP协议的。它的⼯作⽅式是，客户端发起⼀个普通的HTTP请求，但服务器不⽴即关闭连接，⽽是保持这个连接打开，并按照特定的格式（text/event-stream）持续地、⼀段⼀段地向客户端推送数据。\n⽽WebSocket是⼀个独⽴的协议，它在初次连接时通过HTTP协议进⾏握⼿，握⼿成功后，连接就升级为WebSocket协议，之后双⽅就可以在这个连接上进⾏全双⼯的双向通信了，服务器可以随时发消息给客户端，客户端也可以随时发消息给服务器。\n在选择时，我会考虑：如果需要双向实时交互，⽐如在线聊天、协同编辑，肯定选WebSocket。\n如果只需要服务器向客户端推送实时数据，⽐如股票⾏情、通知、或者像我项⽬⾥的AI对话流，SSE就⾜够且更简单，因为它基于HTTP，不需要处理新的协议，后端实现和调试也⽅便⼀些。\n选择SSE主要是出于简单和够⽤的考虑，我们的场景主要是服务器向浏览器单向推送AI⽣成的⽂本，不需要双向通信，SSE的协议⽐WebSocket更轻量，实现起来也简单，对于流式⽂本这种场景⾮常适。⽤户体验就像是在看⼀个⼈实时打字。\n你能详细说一下SSE吗，核心是怎么实现的？ SSE基于HTTP协议，不需要特殊的协议支持，使用标准的HTTP连接。在建立连接后，将HTTP头部的Content-Type改成text/event-stream就可以了，后续发送消息要按照SSE数据格式发送。\nSSE的数据格式非常简单，每条消息由多个字段组成，每个字段由字段名、冒号和字段值组成，以换行符分隔。\n一个完整的SSE消息示例：\nid: 1\\n event: message\\n data: {\u0026#34;message\u0026#34;: \u0026#34;Hello, World!\u0026#34;}\\n\\n 其中，双换行符（\\n\\n）表示一条消息的结束。 我看你简历上还写了MCP工具，谈谈你对MCP的理解 我们可以把MCP想象成电脑的USB-C接口\n键盘、U盘、显示器就是不同的MCP Server，它们提供各自独特的功能。\n电脑就是Agent，它作为MCP Client，通过统一的USB-C接口（即MCP协议）来连接和使用所有外设(MCP Server)。\n这样一来，无论你更换电脑还是外设，只要都支持USB-C标准，就能即插即用，非常方便。\nMCP协议正是为AI使用工具带来了这种即插即用的便利性。最直观的感受就是相同的Tool，可以给很多Agent使用，不需要重复写代码。\n你的工具集有哪些Tool，分别有什么功能？ 其实设计tool就是要思考Agent需要哪些功能\n首先想到的是知识库召回工具，从向量数据库中召回最相关的3份片段\n然后是日志查询工具，日志查询是通过腾讯云日志平台的MCP实现的，他们的MCP支持用自然语言查询日志\n还有告警查询工具，对接监控系统的/alerts接口，直接获取当前活跃告警信息\n其实在测试过程中，我还发现大模型不能精准知道当前时刻的时间，所以还编写了一个时间查询工具，为Agent提供实时时间信息\n最后还有一个联网查询工具，集成外部搜索引擎，让大模型也有搜谷歌的能力\n向量数据库为什么使用Milvus 目前市面上有四种类型的向量数据库：\n集成了向量搜索插件的现有关系型或列型数据库，比如PG Vector 支持密集向量索引的传统倒排索引的ElasticSearch 基于向量搜索库构建的轻量级向量数据库，Chroma 专用向量数据库：这类数据库专门为向量搜索而设计的向量数据库 其实选用Milvus完全就是拍脑袋决定的，当时我是在谷歌上搜了向量数据库，返回的第一个就是Milvus的首页\n然后我点进去看了一下，github上有42k的star，说明社区很活跃。并且使用案例里面有很多大公司的Logo，加上Eino框架里面也提供了Milvus的相关sdk，所以就选择了Milvus。\n你的项目怎么部署的 其实我的项目就分为向量数据库，前端，后端。\n整个项目是部署在办公环境开发机里面的，分别编写为dockerfile用docker启动。\n这样我们在办公网就可以直接使用了。\n在开发机里面部署的好处是办公网可以直接使用，不必为现上网络和办公网络打通而烦恼。\n你的项目用的是什么模型？ 向量模型我使用的是阿里的text-embedding-v4\n语言模型我使用的是qwen3-max\n建议说阿里的模型，阿里在国内做的还是比较好\n为什么用这个模型？ 因为是在国内使用，公司使用，所以选择模型最好还是选择国内的。\n搭建RAG时选择合适的embedding模型很重要，Huggingface有一个MTEB（Massive Multilingual Text Embedding Benchmark）评测标准是一个业界比较公认的标准。\n打开MTEB的官网，阿里的模型就排在第三，所以embedding就选择使用阿里的模型了。\n语言模型还是选择阿里主要还是考虑到embedding模型都用阿里的了，就一块用吧。\n而且阿里的模型挺好看，我之前看公众号说qwen3-max横扫榜单，我自己使用下来感觉也挺聪明的。\n你做这个项目遇到过什么困难/挑战？ 其实最大的困难是在于写文档，之前的告警处理手册其实是很多人都会往里面加东西\n很多步骤可能表述的不是很清晰，最蛋疼的是有些超链接，链接到其他文档，写的不完整\n那对于我们做RAG来说，第一步就是要把文档写完整完善，否则会影响召回的质量，以及大模型的判断\n另外我觉得印象比较深刻的点在于，这个项目是我实习到时候偷摸做的，因为实习的时候安排我也值班。\n值班我就发现了这些痛点，太浪费时间了值班。老是要翻日志看监控，回复很多相同的问题。\n说白了就是比较打杂，维护老项目。让我打杂但是我不能真打杂浪费时间啊，所以我偷偷做这个项目，好在最后做出来了，不论是对其他人值班，还是对自己值班，真有帮助。我还觉得挺有成就感的。\n召回与重排的区别是什么？ 召回：快速捞出相关片段\n将用户问题通过Embedding模型转化为向量。 用向量相似度算法计算问题向量与数据库中所有片段向量的相似度，挑出Top N（如10个）最相关的片段。 特点：速度快、成本低，但准确率有限，适合初步筛选 重排：给片段排优先级\n使用专门计算文本对相似度的模型，逐对计算用户问题与每个召回片段的语义相关性。 从10个片段中选出Top K（如3个）最相关的片段。 为什么不直接召回3个？召回用向量相似度（快但准度低），重排用Cross Encoder模型（慢但准度高），二者结合实现 先广撒网再精挑细选 ，效果优于一步到位。\n简历里提到的Agent，Agent是什么意思？ Agent中文解释有很多，代理、智能体之类的，但其实很难精准的描述它的含义。\n先从简单的和大模型对话说起吧，最早的时候我们只能和chatgpt对话，chatgpt其实不能执行任务，所谓的任务就比如说让它帮忙整理电脑的文件\n即使我们给大模型的提示词写得再详细，大模型只能回答问题或者给出建议，实际动手的还是得靠我们自己，那有没有办法让AI自己完成任务呢？\n这就需要在用户和 AI 中间引入一个智能体Agent来帮忙了。智能体Agent就像一个中间人，本质上就是我们写的程序，它负责接收用户的指令，并协调 AI 和实际工具来干活。\n具体来说，我们先给智能体agent准备好一些基本工具，比如查找文件、读取文件、移动文件等工具。\n当用户发出指令，比如帮我读取C盘目录下的hello_world.cpp文件，移动到D盘目录下，最后总结文件内容：\n那么整个流程会按照下面的步骤执行：\n智能体agent会先把这个请求传给 AI ，并附带告诉 AI 它可以使用哪些工具，工具有哪些作用。 AI 经过思考后，会告诉智能体agent：调用读取文件工具，路径是C://hello_world.cpp。 智能体agent收到指示后，就实际操作工具读取文件，然后把读取的内容反馈给 AI。 AI 根据结果决定下一步该做什么，比如可能还需要移动文件，会告诉智能体agent：调用移动文件工具，路径是C://hello_world.cpp 到 D://hello_world.cpp。 智能体agent收到指示后，就实际操作工具移动文件，然后把移动结果反馈给 AI。 AI 收到移动完成的进度后，返回总结内容给智能体。 智能体收到 AI 传来的结果后，向用户报告结果。这样一步步推进，智能体agent全程协调，直到任务完成。 简单来说，智能体agent 让 AI 不再是只动嘴的参谋，而是变成了能动手的实干家，整个过程更自动化、更智能。Agent与Tool定义：\nAgent：在AI、工具、用户间协调的程序 Tool：提供给 AI 调用的函数。 Agent是怎么实现工具调用的？ 在没有function call技术之前，工具描述是放在system prompt中的。\n就是在Agent中我们提到，我们会将工具信息告诉AI。\n这完全依赖于字符串解析，导致大模型经常会出现问题\n比如告诉大模型有一个查询天气的工具，参数是城市和日期\n大模型返回字符串可能是：调用工具：查询天气的工具，输入上海，明天\n然后我们后端解析字符串，解析到了要调用天气工具，但是参数错了！明天不是一个日期！\n再后来，有了function call之后就好多了，function call把工具描述从 system prompt中剥离，用JSON格式统一定义函数名，函数介绍，参数字段，并规范AI调用工具的回复格式。\n开发者不用自己写代码检测AI回复是否正确，若AI回复错误，AI的服务器端可检测并自动重试，降低用户端开发难度和token开销。\n听你说的还是有点抽象，Function Call的实现流程是什么？ 其实function call的本质就是，让我们用json来描述你写的函数的函数名，函数作用，以及参数\n然后通过调用大模型的接口，比如deepseek的对话接口 https://api.deepseek.com/chat/completions\n这个接口有一个字段是 tools，我们调用这个接口之前，给tools字段赋值，按照function call的规范写好赋值就行了\n📷 [图片 token=EDpMbd3sYoX65Ixsp9octCiOnug（未能下载，见飞书原文）]\n那么这个接口返回的消息里面，有一个tool_calls字段，代表大模型选择调用哪个工具，那么我们的程序只需要按照接口规范解析这个字段，就知道要不要调用工具了，而不需要很原始的通过prompt来解析。 📷 [图片 token=FN3ibL67monAblx7gAXcib1wn7g（未能下载，见飞书原文）]\nEmbedding是什么意思？ Embedding就是向量化的意思，或者说一个过程的感觉，我们一般提到embedding\n就是指用专门的Embedding模型将文本片段转化为向量。\n为什么要向量化？语义相近的文本，向量距离更近。 方便后面召回做相似度匹配\n向量是数学中的基础概念，代表有大小、有方向的量，用数组表示（比如 [1, 2, 3] 是三维向量），维度=数组长度。低维向量（1-3维）可画在坐标轴上，高维向量（几百/几千维）虽无法可视化，但高维向量包含的信息更丰富，能更细腻地表达文本特征。 通过Embedding模型将文本片段转化为向量后，就能用数学方法计算语义相似度。比如 小林写python 和 小林写golang 的向量会非常接近。\nEmbedding 简单说就是把文字转成向量的过程，意思相近的句子向量也相近 。 比如 小林写python 和 小林写golang 这两句话意思相近，经过 Embedding 后会变成两个非常相似的向量（比如 [0.8, 0.2, -0.5] 和 [0.78, 0.22, -0.48]），而 牛牛玛特 的向量则会和它们相差很远。大模型本身看不懂文字，只能处理数字。通过 Embedding，文字的语义被转化成向量后，计算机就能通过计算向量之间的相似度来判断两句话是否相关。\n当用户提问时，问题会先转成向量，向量数据库通过计算向量相似度，快速从海量片段中找出最相关的结果（比如从 小林写python 能关联到 小林写golang ）\n召回的相似度算法是什么？ 当用户提问后，第一步是从向量数据库中召回相关片段：\n将用户问题通过Embedding模型转化为向量。\n用向量相似度算法计算问题向量与数据库中所有片段向量的相似度\n我们在召回的时候一般使用余弦相似度\n原理：计算两个向量夹角的余弦值，范围在-1到1之间。夹角越小，余弦值越接近1，相似度越高。 特点：只关注方向，不考虑向量长度，适合文本语义匹配。 其他还有很多相似度算法，比如欧式距离。但是最适合做语义匹配一般都用余弦相似度\n你的对话Agent Prompt是怎么写的？ # 角色：对话小助手 ## 核心能力 - 上下文理解与对话 - 搜索网络获得信息 ## 互动指南 - 在回复前，请确保你： • 完全理解用户的需求和问题，如果有不清楚的地方，要向用户确认 • 考虑最合适的解决方案方法 - 提供帮助时： • 语言清晰简洁 • 适当的时候提供实际例子 • 有帮助时参考文档 • 适用时建议改进或下一步操作 - 如果请求超出了你的能力范围： • 清晰地说明你的局限性，如果可能的话，建议其他方法 - 如果问题是复合或复杂的，你需要一步步思考，避免直接给出质量不高的回答。 ## 输出要求： • 易读，结构良好，必要时换行 • 输出不能包含markdown的语法，输出需要纯文本 ## 上下文信息 - 当前日期：{date} - 相关文档：|- ==== 文档开始 ==== {documents} ==== 文档结束 ==== 你的运维Agent Prompt是怎么写的？ \u0026#34;1. 你是服务告警运维分析助手,首先调用工具query_prometheus_alerts获取所有活跃的告警。\u0026#34; \u0026#34;2. 分别根据告警的名称调用工具query_internal_docs，获取告警名对应的处理方案。\u0026#34; \u0026#34;3. 完全遵循内部文档的指导进行查询和分析,不允许使用文档外的任何信息。\u0026#34; \u0026#34;4. 涉及到时间的参数都需要先通过工具get_current_time获取当前时间,再结合工具的时间要求进行传参。\u0026#34; \u0026#34;5. 涉及到日志的查询,需要先通过日志工具获取相关日志信息，参数必须携带地域和日志主题。\u0026#34; \u0026#34;6. 分别将告警对应查询到的信息进行总结分析,最后只需要告诉我汇总的所有告警和总结分析。\u0026#34; 如果你的Agent召回的答案不准确，大模型会胡说八道吗？ 这里我使用到了相似度阈值这个参数，这是用于筛选向量检索结果的关键参数，用于判断检索到的文档片段与用户查询的相关性。该阈值通常以余弦相似度（范围 0–1）表示，高于此值的文档会被保留并输入大模型生成答案，低于此值的则被过滤。\n在项目里我设置的阈值是0.8。为什么设置这么高呢？是因为我们使用的场景主要是针对告警的处理，所以一定要准确准确再准确。\n那其实还会有另一个问题，如果没有召回任何片段，大模型会有幻觉，乱回答吗？\n实际上不会的，因为我从prompt里面约束了大模型。\n“严格按照文档的内容回答，不允许使用文档外的任何信息。”\n“如果请求超出了你的能力范围，清晰地说明你的局限性”\n针对这种这种场景我还给大模型的回复加一个👎 的功能。\n用户按了这个👎按钮后，我们后端就会把这次对话的log id发送到群里面，然后我人工通过log id，trace整个生命流程的日志，看看为什么会反馈不好。\n通常是因为某个告警是第一次出现，我们的文档里没有对应的解决方案。会导致这种情况出现，那么遇到了这种情况，就需要人工去把文档写好。下一次就不会再出现了。\n你知道Agentic RAG吗？ Agentic 就是让Agent不在回答问题之前检索文档，而是在ReAct逐步推理过程中，由Agent来决定什么时候进行检索，以及如何检索信息。\n说白了，最早的RAG就是先检索信息，再把信息交给大模型来增强生成。\n那么现在的Agentic本质上就是把检索作为一个Tool，交给大模型，让大模型自己选择调用检索。\n我项目里是混合RAG，调用前的检索和推理中检索都实现了。\n混合RAG有什么好处？ Agentic RAG工具检索可以让大模型自主选择使用文档搜索，非常灵活。\n普通RAG在 Agent 开始时查询，只执行一次，避免每次 reasoning 循环都调用，降低成本，提高检索质量。\n这样的灵活组合结合了 Agentic RAG 的灵活性和普通 RAG 的质量控制。非常适合问答系统，具备高准确性。\n你们的项目是什么背景? / 为什么会有这个项目? 这个项⽬源于我们团队内部的⼀个真实痛点，传统的OnCall依赖⼈⼯值守和排查问题，响应慢且占⽤⼤量研发精⼒。\n就比如之前上游同事天天问同一个问题报错怎么解决，明明文档里写了解决方案还反复问。把时间耗在重复回答上，非常的打杂，所以我就思考怎么样能主动去突破，做一些高价值工作。\n其实这两年AI非常火嘛，我就在想能不能用AI技术，打造⼀个智能化的OnCall助⼿，让它能⾃动回答常见问题，并在故障发⽣时主动进⾏初步排查。\n首先我基于Eino框架设计搭建了RAG知识库来让AI能基于内部⽂档回答问题，然后实现了⼏类Agent，⽐如⽤于对话的对话Agent和⽤于运维排障的运维Agent。\n最终，系统上线后，很多重复性的咨询和告警都能由Agent⾃动处理并给出解决方案，显著减轻了值班的负担。\n你这个项目是用来做什么的? 项目里一共实现了3个Agent，分别是知识库Agent、对话Agent和运维Agent。\n知识库Agent的核心目标是作为团队文档管理和AI应用的基础设施，通过自动化流程，将我们日常积累的文档(告警处理手册、技术方案、错误码文档)，转化为可被AI高效检索的向量，为后续的RAG提供了高质量的向量数据支撑。举个最常见的例子，当我们想根据一个模糊的回忆找文档的时候，可以根据模糊的提问快速检索到对应文档，不再需要再嵌套目录里面一个一个翻了。\n对话Agent本质上是一个基于大模型+知识库构造的智能交互系统。你可以把它看作是一个能够像真人一样理解问题、调用知识库检索并给出精准回答的小助手。它最重要的使命就是帮助团队挡掉高频的重复咨询，加速问题解决，从而提高整体的工作效率。\n运维 Agent 是为了解决值班排查问题的痛点而做的。我们团队维护了多个服务，这些告警从服务错误、性能波动，到中间件异常、下游依赖故障，告警太多了。传统的处理方式往往依赖于人工排查：看告警、查日志、查监控，最终才能判断问题的根源。整个过程重复、耗时，特别是在晚上或节假日，响应效率和准确性更是难以保障，值班的时候告警一多就很痛苦。运维 Agent 可以通过调用各平台的 API，实现跨系统联动，一站式完成排查。例如，它可以自动从告警中提取接口名和时间范围，查询日志、查询监控、查询告警处理手册，将所有信息汇总成一份结构化的故障排查报告。\n你在项目里是什么角色? 这个项目完全由我从0到1实现。\n就如我之前说的，之前上游同事天天问同一个问题报错怎么解决，明明文档里写了解决方案还反复问。\n把时间耗在重复回答上，非常的打杂，所以我就思考怎么样能主动去突破，做一些高价值工作。\n把值班OnCall的痛点给解决了，所以我在工作之外，自己从0到1偷偷搞的一个项目。\n你们项目的核心指标? 其实我这个项目是完全内对实用的，所以评判的标准是回答是否合理准确。\n对于对话Agent，问答这种场景来说，我给大模型的回复加一个👎 的功能。\n同事按了这个👎按钮后，后端就会把这次对话的log id发送到群里面，然后我人工通过log id，trace整个生命流程的日志，以及去询问一下对应的同事，看看为什么会反馈不好。\n通常是因为某个告警是第一次出现，我们的文档里没有对应的解决方案。会导致这种情况出现，那么遇到了这种情况，就需要人工去把文档写好。下一次就不会再出现了。\n项目的难点是什么? 你遇到的最大的困难? 其实最大的困难是在于写文档，之前的告警处理手册其实是很多人都会往里面加东西\n很多步骤可能表述的不是很清晰，最蛋疼的是有些超链接，链接到其他文档，写的不完整\n那对于我们做RAG来说，第一步就是要把文档写完整完善，否则会影响召回的质量，以及大模型的判断\n另外我觉得印象比较深刻的点在于，这个项目是我实习到时候偷摸做的，因为实习的时候安排我也值班。\n值班我就发现了这些痛点，太浪费时间了值班。老是要翻日志看监控，回复很多相同的问题。\n说白了就是比较打杂，维护老项目。让我打杂但是我不能真打杂浪费时间啊，所以我偷偷做这个项目，好在最后做出来了，不论是对其他人值班，还是对自己值班，真有帮助。我还觉得挺有成就感的。\n你觉得做的最好的点是什么? 做的最好的点当然是解决了值班的痛点啦，哈哈哈。\n以前值班⼈⼯值守和排查问题，响应慢且占⽤⼤量研发精⼒。太累了\n系统上线后，很多重复性的咨询和告警都能由Agent⾃动处理并给出解决方案，显著减轻了值班的负担。\n果然懒人改变世界，哈哈哈。\n你觉得项目有哪些不足?如何改进? 其实这个项目上线至今反馈都还挺好的。\n不足和改进呢，其实我希望能把这个项目一直维护，更新下去。\n从我1个人，到其他同事也加入进来共建。那么以后可能就不止我们组内部使用。\n希望能维护迭代下去，形成一个司内的公共中台服务，不同业务组都可以搭建使用。\n你知道 Agent Skills 吗？ 2025年10月16日，Anthropic推出Agent Skills，起初用于提升Claude在某些任务的表现。\n但是由于这个东西比较好用，VS Code、Codex、Cursor等工具迅速跟进支持。\n在这样的背景下，2025年12月18日，Anthropic将其发布为开放标准，支持跨平台、跨产品复用。\nSkills的核心设计是渐进式披露结构，主要分为\n元数据层、包含所有skill的名称、描述。这是始终加载的。 指令层，对应skill.md之间的内容，这部分是按需加载，作用是提供具体任务处理规则。 资源层，包含Reference文件和Script文件，按需中的按需加载，提供补充信息或执行自动化操作。 Skill和MCP的区别：\n特性 Agent Skill MCP 核心功能 教会模型如何处理数据（规则定义） 为模型供给数据（数据连接） 本质 结构化说明文档 独立运行程序 代码执行能力 适合轻量脚本、简单逻辑 安全性和稳定性更优，适合复杂任务 典型应用 格式标准化、规则校验、轻量自动化 数据查询、系统集成、复杂业务逻辑 数据加载方式 分层按需加载，节省token 持续连接，实时数据获取 最佳实践 可与MCP结合使用，实现数据供给+规则处理的完整流程 - 关于Skills的功能使用，请看：https://www.bilibili.com/video/BV1cGigBQE6n\n你的对话Agent如何处理超出上下文窗口的长对话？ 这个问题在实际使用中确实会遇到，我设计了一个分层记忆管理机制：\n第一层是滑动窗口记忆，保留最近5轮对话的完整内容，这部分会直接放入prompt中，保证对话的连贯性。\n第二层是摘要记忆，当对话轮数超过5轮时，会把第5轮之前的对话进行摘要压缩。摘要会保留关键信息，比如用户提到的核心问题、重要参数、已经解决的问题等，把10轮对话压缩成2-3句话。\n第三层是向量记忆，所有历史对话都会向量化存储在向量数据库中。当用户提到\u0026quot;之前说的那个问题\u0026quot;时，可以通过向量检索找回历史对话内容。\n这三层记忆相互配合，既保证了短期对话的连贯性，又支持长期对话的信息检索，还避免了上下文窗口溢出的问题。\n在实现上，我会在每次调用大模型前检查token数量，如果超过阈值（比如上下文窗口的80%），就触发摘要压缩逻辑。这样可以确保系统稳定运行。\n你的RAG系统如何处理文档更新？增量索引是怎么做的？ 文档更新是RAG系统的常见场景，我设计了一套增量索引机制来处理：\n首先在数据库中维护了一张文档元数据表，记录每个文档的路径、最后修改时间、索引状态等信息。\n当有新文档上传或文档更新时，系统会比对最后修改时间，识别出需要重新索引的文档。\n对于需要重新索引的文档，会先从向量数据库中删除该文档的旧的向量数据，然后重新执行加载、分块、向量化、存储的流程。\n这个过程是异步的，不会影响正常的查询服务。用户上传文档后会立即返回，后台会有一个定时任务或消息队列来处理索引任务。\n整个增量索引的流程大概需要几秒到几十秒，取决于文档大小。这样既保证了数据的时效性，又不影响系统的可用性。\n你提到使用了Multi-Agent，具体是怎么实现多Agent协作的？ Multi-Agent在我的项目中主要体现在Plan-Execute-Replan模式中，通过Planner、Executor、Replanner三个子Agent协作完成复杂任务。\n协作机制主要是通过共享上下文来实现的：\n首先有一个全局的Context，里面包含了任务目标、执行计划、当前步骤、历史结果等信息 Planner读取任务目标，生成结构化的执行计划，更新到Context中 Executor读取Context中的当前步骤，调用对应工具，将结果写回Context Replanner读取执行结果，评估是否需要调整计划，更新Context中的计划和步骤 这样循环往复，直到任务完成 这种设计的好处是每个Agent职责单一，Planner只负责规划不执行，Executor只负责执行不规划，Replanner只负责评估不具体干活。\n另外，这三个Agent底层可以使用不同的大模型，比如Planner用推理能力强的模型，Executor用速度快的模型，这样可以在性能和成本之间做权衡。\n你的系统如何保证大模型不会调用错误的工具或传递错误的参数？ 这个问题很关键，我主要从三个层面来保障：\n第一层是工具描述的精确性。在定义function call的时候，我会非常详细地描述每个工具的功能、适用场景、参数格式、参数约束。比如日志查询工具，我会明确说明时间参数必须是RFC3339格式，地域参数必须是指定的枚举值。\n第二层是参数校验机制。在工具实际执行前，我会对大模型传递的参数进行严格校验，包括类型检查、范围检查、格式检查。如果参数不合法，会返回详细的错误信息给大模型，让它重新调用。\n第三层是容错重试机制。如果大模型调用工具失败，Replanner会分析失败原因，并在下一次规划中修正参数。通过这种自我纠错机制，即使第一次调用失败，后续也能成功。\n实践中，通过这三层保障，工具调用的成功率能达到95%以上，除了网络问题导致的报错，我几乎没看到过其他的报错日志，大大提升了系统的稳定性。\n你的系统如何避免大模型产生幻觉？ 大模型幻觉是AI应用中最需要关注的问题，我主要从四个方面来控制：\n第一是强约束的Prompt设计，在系统Prompt中明确要求：\u0026ldquo;严格按照文档内容回答，不允许使用文档外的任何信息\u0026rdquo;、\u0026ldquo;如果不知道答案，明确说不知道，不要编造\u0026rdquo;。这样可以从源头上约束大模型的输出。\n第二是RAG增强，通过检索相关文档片段，给大模型提供可靠的事实依据。在对话Agent中，我会把召回的文档片段明确标记为\u0026quot;参考文档\u0026quot;，让大模型基于这些文档来回答。\n第三是相似度阈值过滤，我设置了0.8的高阈值。如果召回的文档相似度低于这个值，说明知识库中没有相关信息，这时会直接告诉用户\u0026quot;知识库中暂无相关信息\u0026quot;，而不是让大模型凭空猜测。\n第四是人工反馈闭环，我在系统中加入了👎功能。用户如果觉得回答不准确，可以点击反馈，后台会记录log id并通知到群里。我会人工分析原因，如果是文档缺失就补充文档，如果是prompt问题就优化prompt。\n通过这四层防护，大部分情况下都能给出可靠的答案。\n如果让你重新设计这个系统，你会做哪些改进？或者说你的系统还可以怎么迭代？ 经过一段时间的实践，我确实有一些改进的想法：\n第一是增强多模态能力，目前只支持文本，但实际场景中经常需要分析监控图表、日志截图等。可以接入多模态大模型，让Agent能够理解和分析图片。\n第二是构建评测基准，建立一套标准化的评测数据集和评测指标，定期评估系统的效果。目前主要靠人工反馈，不够系统化。打算参考LLM评测领域的一些做法，比如自动化的准确率、召回率、F1-score等指标。\n第三是支持多租户，目前是单一团队使用，如果要推广到其他团队，需要做多租户隔离，包括知识库隔离、权限隔离、资源隔离等。\n最后还需要增强可观测性，目前的日志和监控还比较基础。可以引入更细粒度的trace，比如每个工具调用的耗时、每次召回的相似度分布、大模型的token消耗等，方便性能分析和问题排查。\n怎么解决幻觉？ 引入外部知识库和外部工具（api搜索）等，多维度查询真实数据；\n特定微调，只回答指定类型问题，成本较高，需要准备大量高质量数据，使用rag时，可以选择使用一些特定垂直类行业微调后的模型使用，降低微调成本；\n多模型混用，用多个模型同时回答同一个问题做比较，也可以用一个回答另一个专门来做检查验证；\n后处理规则，对既知的逻辑可以直接用程序对大模型结果做一些验证，跟上面说的多模型交叉验证差不多\n人工干预和反馈，人工对结果评价，事后纠正，也可以人工直接干预在关键步骤需要人工来处理，人工加入工作流（比如让人工去选择）；\n优化提示词：\n明确指令和上下文：提示词越清晰，推理方向越明确 引入假设约束：通过提示词明确输入输出大概是怎么样的，可以减少不符合条件的答案 提供背景信息：模型通过上下文做推理，背景信息可以帮助模型理解问题 让模型给出来源或参考：督促模型生成内容更谨慎 要求分步骤推理：涉及复杂问题，要求模型分步推理 限制模型输出：通过严格的输出范围或者约束条件，减少内容的多样性 鼓励模型承认不确定性：如果你不知道就说不知道 反向提示：主动写出来排除一些内容 温度：限制模型的创造力，温度值越低越严谨，温度越高越开放\n怎么评价你的prompt写的好坏，如何评测？ 其实刚开始做这个项目写prompt的时候，写的还是挺随意的，导致回答不准，有幻觉\n然后我就去看了一些prompt工程的文档，prompt和解决幻觉一般用 xxx（参考51题 解决幻觉[项目面试题](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/项目面试题/)）这些方案\n修改了之后，跑出来的项目就挺好的。至于测评我还没有开始做\n因为我本身不是专业搞这个的，更多的精力还是放在工作上，这个项目是为了提升自己的效率才做的（不要打肿脸充胖子，说没做没什么的）\n怎么实现用户的意图识别？ 我们这个场景比较简单，其实不需要意图识别，因为就是针对告警来的\n如果你想回答高大上一点，可以学习这个视频 https://www.bilibili.com/video/BV19r5yzPENP\n但是我还是推荐不要回答，比较难说的清楚，不如不给自己挖坑\n没做多租户吗？为什么？ 其实我们会对每个新打开对话框的网页分配一个随机的session id\n但是没有去做RBAC权限控制，因为本来就是内部自己几个人使用\n在服务里通过session id来区分不同用户的历史记录。\n如果让你去设计一个Agent去辅助增效，你有什么想法吗 我觉得首先是挖掘痛点，凭空想象一个增效没有意义\n挖掘到痛点，比如值班很痛苦之后，就可以思考，值班这件事情能不能让AI代替\n很明显是可以的。再举一个例子，打电话，以前有电销，而现在很多都是AI打电话了。所以首先要挖掘痛点，再针对性的分析\n面试官你那边是否有什么痛点，我们可以聊聊\n压力面-你们这个项目的TPM是多少？ 内心：面试官你太专业了，我都没听过TPM是什么东西，直接投降\n面试官你说的这个TPM我没听过，这个项目是我为了提效做的，不是像豆包那样toc对接很多人的agent项目\n所以这种比较专业的测评之类的，我还没开始做，后面我去了解一下\n压力面-你的项目看起来很简单，是很常见的方案？这个项目你有没有做的比较深入的技术点？ 内心：你装你m呢，大家不都是用这些？你造出一个新的来？skill怎么不是你发明的\n如果遇到了这种问题，就是压力面，遇到压力面就投降，顺着面试官说好话，不要跟他犟，也不要心态崩。压力面看的就是你能不能抗压\n面试官您是资深或者专家级别的，看我做的肯定很简单。但是对于我这个初级程序员来说，确实还是有一些挑战。虽然方案都是比较常见的，但实际做起来的时候，细节还是比较多的，比如怎么解决幻觉\n这个时候，先投降，然后把问题引导到幻觉上面，幻觉的回答，参考51题（[项目面试题](/oncall/智能 OnCall Agent 项目/第九章 _ 面试求职全攻略/项目面试题/)）\nAgent执行异常/失败怎么处理？ agent执行异常，一般是无限调用Tool，循环。\n一般我们会设置一个最大Step，超过Step就强制中断了。\n第二个就是检查你的prompt，为什么会出现这个情况，是prompt写的有问题，还是模型选的太垃圾\n有了skill之后，你的项目那里可以改进 优化点就是RAG去掉，改成skill\n不同的问题就是不同的skill\n本身我们RAG就是想把怎么处理告警的SOP拿出来，现在有了skill后，天然的可以替换掉RAG了\n日志为什么在腾讯云上？是怎么上传的？ 日志要么使用云厂商的能力，要么就是自建ELK。现在用云上能力比较多\n在服务器上按照一个日志采集器，配置采集路径，即可实现自动采集\n采集器是腾讯云CLS提供的，一行命令按照以下就好了\nhttps://cloud.tencent.com/document/product/614/17414?from=console_document_search\n你的项目测评是怎么做的？ 对话Agent，从设计上看（告警怎么处理），一个问题必定可以有一个正确答案，所以可以测评：\n构造测评集，即把问题和答案构造出来，比如100条\n把问题拉出来调用给对话Agent，记录Agent的回答\n把回答和测评集答案，用其他模型充当评委，判断回答是否正确（如果回答是确定性的，则直接用运算符等于来判断，不确定的用大模型来判断）\n统计这一批测评的正确率\n运维Agent，从设计上看，一个问题可能有很多种决策，是随机的的，不适合做测评\n硬要做测评，那也是上面那一种测评方式\n调用日志服务是如何保证上下文不会爆的 首先会根据trace id去搜索，一个服务里面通过trace id串联的日志不会非常多，如果很多说明打日志的地方设计有问题\n并且现在模型的上下文很长，像DeepSeek-V4模型上下文有1M，支持百万字超长上下文。几乎不可能出现上下文窗口打满的情况\n当然我们也有作兜底，即如果现在窗口用了70%（这个随便说一个数即可），那么就会会之前的上下文做一个压缩总结，这个总结是让大模型自己总结。\n如果兜底也没有用还是超过上下文窗口，那么大概率是大模型推理出问题了，按理说不应该搜这么多日志。所以这个时候就要转人工了，这个问题大模型就不需要继续处理了。\n你的prompt中有三部分，system prompt，历史对话的摘要和最近几轮对话，他们的顺序是怎样的，历史对话摘要和最近对话的顺序对效果和性能有什么影响 顺序就是：system prompt、摘要（可能没有）、按照时间顺序的对话\n这里的顺序其实是固定的，和性能没啥关系\n效果是有影响的：大模型有注意力机制，即开头和结尾的注意力是最集中的，中间的内容和细节可能会遗忘。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/%E9%A1%B9%E7%9B%AE%E9%9D%A2%E8%AF%95%E9%A2%98/","summary":"小组件 (type: blk_637dcc698597401c1a8fd711)   !TIP  如果各位林友用《智能OnCall Agent项目》去面试的时候，遇到了本文档没有的面试题，希望导师扩充的，欢迎登记这个表格：  - 项目面试题","title":"项目面试题"},{"content":"OncallAgent 通过 OpenAI-compatible 协议接入 Qwen，而不是让业务代码直接依赖厂商私有 SDK。聊天模型与 embedding 使用 langchain-openai 的 ChatOpenAI 和 OpenAIEmbeddings，rerank 则被封装为独立的异步协议和 HTTP 客户端。这样做的重点不是隐藏模型名称，而是让聊天、索引、检索和 readiness 依赖稳定的 provider 抽象。\n📷 [图片 token=KgZGbxgcZo0YYTxVicxcVgf6ngh（未能下载，见飞书原文）]\n模型配置来自仓库根目录的两个本地 JSON：config/project.json 提供基础设置，config/user.project.json 只覆盖个人字段。两者均被 .gitignore 忽略，运行时递归合并对象；数组、字符串、数字和布尔值整体替换。应用不会从 .env 或本机环境变量补齐项目配置，这使实际生效值可以从一条确定的 JSON 合并链解释。\n📷 [图片 token=Q5ZIbfCLbo8yKFxyRyucV7dlnof（未能下载，见飞书原文）]\n“本地安全配置”不等于把密钥写进源码。模型 API key、CLS 凭据和真实资源 ID 只能留在被忽略的本地文件中，并且不能出现在日志、readiness、配置检查或异常正文中。本篇只讨论字段与调用关系，不展示任何本机真实凭据。\n📷 [图片 token=YiR4bs7QFooMQjxCxljcxiQknJS（未能下载，见飞书原文）]\n学习目标 理解基础 JSON 与用户 JSON 的递归合并规则及默认路径。\n追踪配置如何变成 LlmProviderConfig，再创建聊天、embedding 与 rerank 能力。\n理解 provider protocol 如何让业务服务和测试脱离真实网络。\n掌握 embedding 每批十条、显式维度、rerank 校验与有限重试的实现边界。\n区分 liveness、模型 readiness、配置有效性和真实业务调用成功。\n📷 [图片 token=JwtIbNZZbozroPxoPPGcMSVCnTc（未能下载，见飞书原文）]\n功能入口与完整调用链 配置入口是 apps/backend/src/super_ai/project_config.py 的 load_project_config。默认基础路径由 DEFAULT_PROJECT_CONFIG_PATH 指向根目录 config/project.json，默认用户路径指向 config/user.project.json。_read_json_object 要求文件可读且顶层为 JSON object；用户文件存在时，_deep_merge 对两边都是 object 的字段递归处理，否则以用户值整体替换基础值。\n📷 [图片 token=GzVabIHGfoMnhQxEoxscTSbKnXb（未能下载，见飞书原文）]\napps/backend/src/super_ai/llm/config.py 的 load_llm_provider_config 读取 llm section，要求 provider、apiKey、baseUrl、chatModel、embeddingModel、embeddingDimensions、rerankModel、rerankUrl、temperature、timeoutSeconds 和 maxRetries。它还以当前 chatModel 为键读取 modelCapabilities，取得 contextWindowTokens；没有匹配 profile 会安全失败。api key 字段在 dataclass 中标记 repr=False，减少对象调试输出意外带出密钥的风险。\n📷 [图片 token=Mqpubxss1oGe0axCjKnco7AtnDg（未能下载，见飞书原文）]\nbuild_default_llm_provider 把类型化配置交给 QwenOpenAIProvider。聊天链路调用 create_chat_model，得到配置过的 ChatOpenAI；文档索引调用 create_embedding_model，得到 OpenAIEmbeddings；混合检索精排调用 create_rerank_model，得到 QwenVlRerankModel。业务模块只依赖 ChatModel、EmbeddingModel、RerankModel 或 LlmProvider 协议。\n📷 [图片 token=VjeGbVITqoqJFoxi4VGcrrNVnHg（未能下载，见飞书原文）]\nconfig/project.json + 递归覆盖 config/user.project.json ↓ load_project_config ↓ load_llm_provider_config ↓ QwenOpenAIProvider ├─ ChatOpenAI：聊天与 Agent ├─ OpenAIEmbeddings：知识索引与查询向量 └─ QwenVlRerankModel：候选精排 📷 [图片 token=Hp8HbfFwJovImKxvjhicYfgInze（未能下载，见飞书原文）]\n当前本地配置与测试固定的非敏感模型上下文包括 OpenAI-compatible Qwen provider、qwen3.7-max 聊天模型、text-embedding-v4 embedding、1024 维向量和 qwen3-vl-rerank。基础 URL、温度、超时、重试次数和 model capability 都来自合并配置，不应在业务代码散落硬编码。模型名称是配置事实，不代表外部服务在任意时刻都可用。\n📷 [图片 token=EdzEbQRctoMPa6xbp0rcGU6fnZg（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 .gitignore config/project.json、config/user.project.json 规则 阻止本地运行配置和个人覆盖进入版本控制。 apps/backend/src/super_ai/project_config.py load_project_config、_deep_merge、project_config_section 加载、校验并递归合并两个本地 JSON。 apps/backend/src/super_ai/llm/config.py LlmProviderConfig、load_llm_provider_config 把弱类型 JSON 转成完整模型运行配置并验证 capability profile。 apps/backend/src/super_ai/llm/provider.py LlmProvider、QwenOpenAIProvider、QWEN_EMBEDDING_BATCH_SIZE 创建聊天、embedding、rerank 客户端并执行安全 readiness。 apps/backend/src/super_ai/llm/rerank.py RerankModel、QwenVlRerankModel、LlmRerankError 封装异步 rerank HTTP、重试、响应验证和安全错误。 apps/backend/src/super_ai/api/app.py _llm_provider、_embedding_model、_rerank_model、_llm_readiness_payload 按需装配模型能力，并将 readiness 转换为安全 API 数据。 apps/backend/src/super_ai/documents/indexing.py DocumentIndexingService 把文档 chunks 交给 embedding protocol，并验证向量数量后写 Milvus。 apps/backend/src/super_ai/retrieval/tool.py KnowledgeRetrievalTool 生成查询向量、融合候选并通过 RerankModel 精排。 apps/backend/tests/test_llm_provider.py FakeChatModel、FakeEmbeddingModel 在无网络环境验证配置、工厂、批处理和 readiness 脱敏。 📷 [图片 token=Fei3bh74IoAGPQxqBwhcTS4xn5c（未能下载，见飞书原文）]\n代码调用流程图 模型接入从两份本地 JSON 开始，经过递归合并和类型校验后，分别装配聊天、embedding 与 rerank 三条调用边界。\n📷 [图片 token=JZ8ebn4lwos2BtxaPMDcqtxrnJc（未能下载，见飞书原文）]\n关键实现拆解 确定的 JSON 合并，而不是隐式环境注入 _deep_merge 只在 base 与 override 同一键的值都是 dict 时递归。例如用户文件仅覆盖 llm.apiKey 和 llm.chatModel，基础文件中的 baseUrl 与 timeout 仍保留；若覆盖一个数组，则整个数组替换。用户文件缺失时使用基础配置。没有任何 os.environ 兜底，因此排查配置时只需检查两个 JSON 和显式传入的测试路径。\n📷 [图片 token=DsfjbzUijoxByjxovQMcauRen9v（未能下载，见飞书原文）]\nrequired_str 拒绝空字符串，required_int 与 required_float 检查类型，required_dict 约束简单参数对象。底层 ProjectConfigurationError 会被 LlmConfigurationError 包装，但消息只指出缺失或无效字段。缺 apiKey 时错误包含字段名，不包含其他 secret。\n📷 [图片 token=VnMXbrE5poOhBZxAaj3cD6Oun4d（未能下载，见飞书原文）]\n看什么：配置加载器只确定两个 JSON 路径；用户文件存在时递归合并，否则直接返回基础对象。\n📷 [图片 token=FT1Gb1ao7oDAXMxQUcictyKOnJb（未能下载，见飞书原文）]\ndef load_project_config( config_path: Path | str | None = None, *, user_config_path: Path | str | None = None, ) -\u0026gt; Mapping[str, Any]: \u0026#34;\u0026#34;\u0026#34;Load the repository-level JSON configuration file with user overrides.\u0026#34;\u0026#34;\u0026#34; # 1. 基础配置路径来自显式参数或仓库内固定位置。 path = Path(config_path) if config_path is not None else DEFAULT_PROJECT_CONFIG_PATH config = _read_json_object(path) override_path = _default_user_config_path(path, user_config_path) # 2. 用户文件存在时递归覆盖，不从环境变量补值。 if override_path.exists(): override = _read_json_object(override_path) return _deep_merge(config, override) return config 📷 [图片 token=U3pfbx1luodg8lxHaZgcXuYSnib（未能下载，见飞书原文）]\n代码证明配置来源是确定的文件合并，而不是 shell 当前状态；测试也可以传入隔离路径。失败边界包括文件不可读、非法 JSON、顶层不是 object 和必填字段为空，错误只描述字段与路径，不应把相邻 secret 一并打印。\n📷 [图片 token=C01FbggPbo8Xqvxp6jrc3EyznNe（未能下载，见飞书原文）]\n看什么：递归合并只发生在两侧都是 dict 的同一键，数组、数字、布尔和字符串会整体替换。\n📷 [图片 token=LIy9bdijeo7zbUxHT9FczGCmnEe（未能下载，见飞书原文）]\n图中虚线表示明确不存在的配置补值路径。用户文件缺失时基础值保留；若基础模板中的必填个人字段仍为空，类型化配置创建会失败，而不是静默从环境取一个可能过期的值。\n📷 [图片 token=WDrDbq1SnoAWtExqKUpcm5B2ngd（未能下载，见飞书原文）]\n聊天与 embedding 走 OpenAI-compatible 客户端 _create_chat_openai_model 把 api key 作为回调传给 ChatOpenAI，同时设置 base URL、model、temperature、timeout 和 max retries。Provider 协议只要求异步 ainvoke，因此聊天服务、标题生成、记忆压缩或诊断规划可以注入 fake model，而不直接构造厂商客户端。\n📷 [图片 token=OszWb7p8SomttwxD7BRc2rw8n6b（未能下载，见飞书原文）]\n_create_openai_embedding_model 明确设置 model、dimensions、timeout 和 max retries。它把 chunk_size 设为常量 10，并关闭客户端的 token-ID 上下文切分，让原始字符串或字符串数组送往 Qwen-compatible embedding 接口。超过十个文档 chunk 时，客户端分批请求并按输入顺序汇总；DocumentIndexingService 还验证返回向量数必须等于 chunk 数。\n📷 [图片 token=Yuzobe4UeoWVOaxz6MlcVNOVn9e（未能下载，见飞书原文）]\nembedding dimension 必须与 Milvus vectorDimension 一致。Provider 只负责生成配置维度的向量，MilvusVectorStore._chunk_to_entity 在写入前再次比较实际向量长度与 collection 设置。这是跨配置边界的防线：模型切换若只改一侧，会明确失败而不是写入不可搜索的数据。\n📷 [图片 token=U951bWuqvorUUPxIzyMcymW1n1d（未能下载，见飞书原文）]\n看什么：embedding 工厂把模型、维度和每批十条文本的兼容约束一次性传给 LangChain 客户端。\n📷 [图片 token=RustbKRRNoSnuYxzDiDcXLLlnJb（未能下载，见飞书原文）]\ndef _create_openai_embedding_model(config: LlmProviderConfig) -\u0026gt; EmbeddingModel: # 1. 继续依赖 OpenAI-compatible 抽象，而非厂商 SDK。 return cast( EmbeddingModel, OpenAIEmbeddings( api_key=lambda: config.api_key, base_url=config.base_url, model=config.embedding_model, dimensions=config.embedding_dimensions, # 2. 这是 provider 批次上限，不是文档文本 chunk 大小。 chunk_size=QWEN_EMBEDDING_BATCH_SIZE, check_embedding_ctx_length=False, timeout=config.timeout_seconds, max_retries=config.max_retries, ), ) 📷 [图片 token=YNPcbqnc0o2xMgxJl3ic8akBnSn（未能下载，见飞书原文）]\n片段证明 embedding 保留原始字符串输入，并由客户端按十条分批；当前测试用十一条输入验证顺序与两次请求。它不保证向量可写入任意 collection，Milvus 仍会检查实际长度与 vectorDimension，不匹配时索引任务必须失败并保留重试状态。\n📷 [图片 token=GhLjblOFEosXJDxy9ZQc5irqncd（未能下载，见飞书原文）]\n看什么：聊天和 embedding 虽共享 provider 配置与 base URL，却创建不同客户端、服务于不同业务调用。\n📷 [图片 token=RsjfbcEAhobQyFx19dCcqdB4nub（未能下载，见飞书原文）]\n共享配置不等于共享成功状态：聊天模型可用时 embedding 仍可能因模型权限、批次或维度失败。排障和 readiness 需要分别理解能力边界，不能用一次对话成功证明知识索引完整。\n📷 [图片 token=ZEw6bjwQvoZVihxbrGtctuLKnng（未能下载，见飞书原文）]\nRerank 是独立协议与直接 HTTP 边界 QwenVlRerankModel.arerank 接收 query、documents 和 top_n；空 query 或空 documents 返回空列表，不发送网络请求。合法请求组装模型、文本和 top_n，通过 bearer header 调用配置 endpoint。429 和服务端错误在重试额度内指数退避，传输或超时也可重试；最终失败统一抛出 LlmRerankError，不附上上游响应正文。\n📷 [图片 token=F2XmbOD9Do718ixnbVwcfsgInAd（未能下载，见飞书原文）]\n_parse_rerank_results 要求 output.results 是列表，每个结果必须有范围内且不重复的 index，以及 0 到 1 之间的有限 relevance_score。结果按分数降序并截取 top_n。这个验证很关键：外部模型响应属于不可信输入，错误索引可能把一个文档的分数错误关联到另一个文档。\n📷 [图片 token=Xs2nbWWotoecK8xMHS9cU6OCnLb（未能下载，见飞书原文）]\n看什么：rerank 请求使用独立 endpoint，空输入短路，合法请求只发送模型、查询、候选文本和 top_n。\n📷 [图片 token=SjxFbI1s5oVo1zxgLCCcyJmhneb（未能下载，见飞书原文）]\nasync def arerank( self, *, query: str, documents: Sequence[str], top_n: int, ) -\u0026gt; list[RerankResult]: normalized_query = query.strip() # 1. 空查询或空候选不发起外部 HTTP 请求。 if not normalized_query or not documents: return [] if top_n \u0026lt; 1 or top_n \u0026gt; len(documents): raise LlmRerankError(\u0026#34;Rerank request is invalid.\u0026#34;) # 2. 响应中的 index 必须继续映射回这里的候选顺序。 payload = { \u0026#34;model\u0026#34;: self._model, \u0026#34;input\u0026#34;: { \u0026#34;query\u0026#34;: {\u0026#34;text\u0026#34;: normalized_query}, \u0026#34;documents\u0026#34;: [{\u0026#34;text\u0026#34;: document} for document in documents], }, \u0026#34;parameters\u0026#34;: {\u0026#34;return_documents\u0026#34;: False, \u0026#34;top_n\u0026#34;: top_n}, } response = await self._post_with_retry(payload) 📷 [图片 token=O09SbPnWMohzvuxT26pc6pisnOf（未能下载，见飞书原文）]\n代码证明 rerank 没有被伪装成 OpenAI chat 调用，也没有在空候选时制造分数。外部响应随后必须验证 index、重复、有限分数和范围；超时、429、5xx 或坏 JSON 最终都会成为安全 LlmRerankError，检索层不得退化为把 RRF 分数冒充最终精排分。\n📷 [图片 token=O7HybCsTLolMQhxfh07cJMtonoh（未能下载，见飞书原文）]\n看什么：从两路粗召回到精排结果，关键的一致性条件是 rerank index 始终引用同一候选数组。\n📷 [图片 token=VKAhbb2SDoQym7xSdVtcKcZ2ndc（未能下载，见飞书原文）]\n任一粗召回失败时当前实现返回系统不可用，不静默单路降级；rerank 失败也不会产生伪造终分。只有通过校验的 index 才能把分数写回对应 chunk 与引用。\n📷 [图片 token=C7jGbrpqaoW4oixWoOucOBNjnYe（未能下载，见飞书原文）]\nReadiness 不是一次业务保证 QwenOpenAIProvider.check_readiness 创建 chat model 并执行最小提示，返回 ok、provider、model、base URL、延迟和可选错误。_safe_error_message 会把异常字符串中出现的 api key 替换为 [redacted]。API 层的 _llm_readiness_payload 进一步把失败归一为“LLM provider is unavailable”，所以 /ready 不返回 provider 原始异常。\n📷 [图片 token=NPPQbdQ9OoyLjxxeLWxcoHyBnhd（未能下载，见飞书原文）]\n/health 完全不探测模型；/ready 实际调用模型并与 SQLite、Milvus、MCP 结果聚合；/config/check 还检查 llm section 能否构造成类型化配置。即使 readiness 成功，后续真实对话仍可能因限流、超时、网络变化或输入规模失败，因此业务流必须保留自己的 error 事件和任务失败状态。\n📷 [图片 token=DeVNbjDOJo8dX1x7LUvcUto6nEc（未能下载，见飞书原文）]\n看什么：provider readiness 的最小提示只返回安全配置上下文和延迟，异常中的 key 会在结果构造前替换。\n📷 [图片 token=NxyRbLwxaobbG2xE8dTc1eu3nKe（未能下载，见飞书原文）]\nasync def check_readiness(self) -\u0026gt; LlmReadinessResult: started_at = monotonic() try: model = self.create_chat_model() # 1. 最小请求验证当前聊天模型可达。 await model.ainvoke(\u0026#34;Return exactly: ready\u0026#34;) except Exception as exc: return LlmReadinessResult( ok=False, provider=self._config.provider, model=self._config.chat_model, base_url=self._config.base_url, latency_ms=_elapsed_ms(started_at), # 2. readiness 结果不保留可识别的 API key。 error=_safe_error_message(exc, self._config.api_key), ) return LlmReadinessResult( ok=True, provider=self._config.provider, model=self._config.chat_model, base_url=self._config.base_url, latency_ms=_elapsed_ms(started_at), ) 📷 [图片 token=VH0KbcdDmojhtaxJF5ic1fd7n8c（未能下载，见飞书原文）]\n片段证明 readiness 是一次真实的最小 chat 调用，不只是检查字段非空；API 层还会把失败错误归一化。它不探测 embedding 与 rerank 的完整业务输入，也不能预言后续限流、长上下文或网络变化，所以索引、检索和聊天仍要分别保存失败。\n📷 [图片 token=HR6PbhIkForZkyxDxabcKD7Fn0d（未能下载，见飞书原文）]\n看什么：三种健康入口回答的问题不同，不能把 liveness、配置可解析和依赖可用混为一谈。\n📷 [图片 token=OPjabTbxko5SeVxjlAVcacPinYb（未能下载，见飞书原文）]\n/health 即使模型离线仍可成功；/ready 的 503 仍携带成功 envelope 中的分组件诊断；具体业务可能在 readiness 后失败。恢复动作必须针对所在层，而不是看到 503 就盲目修改密钥。\n📷 [图片 token=TvRdbNbqfoOKgsxIMP7cWFdqnTg（未能下载，见飞书原文）]\n模型切换需要成组修改 切换 chat model 不能只替换一个字符串。新的 chatModel 必须在 modelCapabilities 中有同名 profile，否则 load_llm_provider_config 明确失败；contextWindowTokens 又会影响会话占用率和自动压缩判断。若同时切换 embedding model，还必须确认 embeddingDimensions 与 vectorStore.vectorDimension 一致，并考虑已有 collection 中向量是否需要重建。rerank model 与 rerankUrl 也应作为一组检查。\n📷 [图片 token=TQWsbjoLRocofOxzmcZcK7ewnPd（未能下载，见飞书原文）]\n温度、超时和重试不是越大越安全。聊天重试可能增加等待时间，embedding 批次在大文档中会放大总耗时，rerank 的重试还包含指数退避。配置变更后至少要分别验证最小模型调用、超过十个 chunk 的 embedding、Milvus 维度写入以及 rerank 响应校验，不能用一次聊天成功概括全部模型能力。\n📷 [图片 token=Hu7JbxRn4oty2sxDOPecwcDynRf（未能下载，见飞书原文）]\n看什么：模型切换是三个耦合组而不是一个名称替换，图中每组都标出当前代码实际消费该字段的边界。\n📷 [图片 token=IHOGbKXQUoNpElxc7mjcMt4knpA（未能下载，见飞书原文）]\n任何一组只改一半都会明确失败或产生不可搜索的数据风险。切换后需要受控重启重新装配 provider，并分别验证 chat、十一条以上 embedding、Milvus 写入和 rerank；旧后台任务还要避免在一次执行中跨越两套配置。\n📷 [图片 token=HrhWbn3V2oaM9Ax3Lwlcx4R2nIb（未能下载，见飞书原文）]\n依赖注入让失败可以被确定地测试 QwenOpenAIProvider 构造函数接受 model_factory、embedding_factory 和 rerank_factory。create_app 也允许注入 llm_provider、embedding_model、rerank_model 和 chat_agent_runner。这些入口使测试能记录输入、返回固定向量或主动抛错，而无需访问外部服务。可测试性不是额外便利，它证明业务层真正依赖协议，而没有在深处偷偷创建网络客户端。\n📷 [图片 token=X5yxb817WorojNxzvuFczWFenXb（未能下载，见飞书原文）]\n例如 readiness 测试注入 FakeChatModel，可以确认最小提示确实调用模型，并检查结果 repr 不含 api key；索引测试注入 embedding fake 与 vector store fake，可以确认 chunk 顺序、向量数量和状态更新；聊天测试注入 Agent runner，可以确认越权时 runner 调用次数保持为零。这些断言比 mock 一个 HTTP 状态更接近安全边界。\n📷 [图片 token=UuJ8bVeKdo5PhRxh5uMczUMunec（未能下载，见飞书原文）]\n看什么：provider 构造函数保留三个工厂插槽，并在未注入时才选择真实客户端工厂。\n📷 [图片 token=IyoWbOmUMobpiGxj8TbcM12bnaf（未能下载，见飞书原文）]\ndef __init__( self, config: LlmProviderConfig, model_factory: ModelFactory | None = None, embedding_factory: EmbeddingFactory | None = None, rerank_factory: RerankFactory | None = None, ) -\u0026gt; None: self._config = config # 1. 测试可注入只记录输入、不访问网络的 chat factory。 self._model_factory = model_factory or _create_chat_openai_model # 2. embedding 与 rerank 也有独立替换点。 self._embedding_factory = embedding_factory or _create_openai_embedding_model self._rerank_factory = rerank_factory or _create_qwen_rerank_model 📷 [图片 token=AIwibHmO0oaLLhxueUicX9MDnie（未能下载，见飞书原文）]\n代码证明业务层可以依赖稳定协议，并让不同能力独立失败；测试无需 monkeypatch 厂商 SDK。边界是 fake 只能证明本地编排和错误处理，不能替代真实 endpoint 的集成验证；线上凭据、限流和响应兼容仍需安全的 readiness 与显式业务测试。\n📷 [图片 token=HkLebSfG5oHXVQxOpV3c9TJKnVe（未能下载，见飞书原文）]\n配置排障的分层路径 遇到模型不可用时，先判断是文件、字段、连接还是业务输入。文件层由 load_project_config 报告无法读取、非法 JSON 或顶层非 object；字段层由 load_llm_provider_config 报告缺失、空值、类型错误或 capability 不匹配；连接层由 /ready 的 llm 组件给出降级；业务层则由聊天 SSE、索引任务或检索工具记录具体失败状态。按层排查可以避免因为一次 503 就反复改动密钥。\n📷 [图片 token=GufZb6GRXoiLcHxXYPVcj1v7nid（未能下载，见飞书原文）]\n/config/check 会同时返回 configuration 与 dependencies。configuration.llm 有效只表示必要字段能被解析，并会暴露安全的 provider、model 与 base URL；dependencies.llm 才表示最小请求结果。若前者失败，应先修 JSON；若前者成功而后者失败，再检查 endpoint、网络、模型权限、限流或服务状态。响应不会提供原始上游正文，进一步诊断应在不泄密的本地环境进行。\n📷 [图片 token=KLjybaMsCo0FoRx5Kw1czHqtnkf（未能下载，见飞书原文）]\n看什么：排障树先使用无网络证据，再逐层进入真实调用，避免在无法读取 JSON 时就怀疑远端模型。\n📷 [图片 token=Xkf7bOfRUoFq1CxB3YIc4XDjnTo（未能下载，见飞书原文）]\n每个节点都对应不同证据与恢复动作。安全诊断只返回 provider、模型、base URL 和组件状态，不返回 key 或上游正文；需要更深排查时也应保持本地日志脱敏，不能用扩大响应内容换取便利。\n📷 [图片 token=XHixbxM20oNPCsx5moYcpErgnjc（未能下载，见飞书原文）]\n本地文件与外部服务的责任分离 项目配置不读取环境变量，但宿主机启动器会从同一 JSON 合并结果为外部 CLS MCP 进程导出它所需的进程变量。这不改变后端 LLM 的读取规则：聊天、embedding 和 rerank 仍从 apps/backend/src/super_ai/project_config.py 读取 JSON。区分“应用项目配置”和“外部进程启动接口”可以避免误以为在 shell 中设置一个模型 key 就会覆盖后端配置。\n📷 [图片 token=OnwSbSS5yoxf7KxqofjcngAIned（未能下载，见飞书原文）]\n本地 JSON 也不是远端密钥管理系统。它解决的是单机开发中的明确来源、覆盖和版本控制隔离；多人共享、集中轮换、最小权限和审计仍需要组织层面的密钥管理流程。当前代码能保证不主动从环境补值和不在安全诊断中返回 key，但不能阻止拥有本机文件读取权限的人查看文件。\n📷 [图片 token=ZLbrbpqgLoFZdnx9lW1cmEFonzc（未能下载，见飞书原文）]\n配置文件发生修改后，已经创建的 provider 对象不会自动热更新。应用在按需构建默认 provider 时读取当时的合并结果，长期运行的对象持有不可变 LlmProviderConfig。因此切换模型或轮换 key 后，应通过受控重启重新装配，并再次执行配置检查和 readiness；不要假定编辑 JSON 会改变正在进行的聊天或索引任务。重启前还应等待或取消重要后台任务，避免同一任务的不同阶段使用不同模型配置。\n📷 [图片 token=XWOQbCylropZj5xbFIccyFvnn0n（未能下载，见飞书原文）]\n看什么：责任图把本地 JSON、宿主机应用对象和远端能力分开，并标出编辑文件后不会自动更新已缓存 provider。\n📷 [图片 token=HryHbqG2xogfWxxLQ9dcUxQ9n0g（未能下载，见飞书原文）]\n本地文件负责明确来源和版本控制隔离，远端服务负责实际模型能力，两者之间没有热更新保证。拥有本机文件权限的人仍可读取 secret；配置轮换后还需重启、重新 readiness，并妥善处理正在运行的 durable job。\n📷 [图片 token=WoVdblJvIocHS9xuWQ2cbmtfnQf（未能下载，见飞书原文）]\n数据、契约与状态 LlmProviderConfig 是不可变 dataclass，字段覆盖 provider、api key、base URL、三个模型、embedding 维度、rerank endpoint、上下文窗口、温度、超时和重试。api key 的 repr=False 只是降低误输出风险，不意味着可以记录整个配置映射；原始 JSON 同样不得进入日志或 HTTP。\n📷 [图片 token=OQZJbFB1noca6UxZaWIcLcXanzb（未能下载，见飞书原文）]\n三类模型能力承担不同数据语义：ChatModel 输入对话或提示并输出消息；EmbeddingModel 输入有序文本列表并输出同序向量；RerankModel 输入 query 与候选正文，输出引用候选下标的相关性。它们共享一个 provider 配置，但不能互换。Milvus 只保存 embedding 结果和知识 chunk，不保存聊天响应或 rerank 调用记录。\n📷 [图片 token=Qsa8bYSePouXRkxoE3LccnHzn5b（未能下载，见飞书原文）]\n当前聊天 model capability 的 contextWindowTokens 被聊天记忆服务用于上下文占用计算和压缩策略。它来自用户配置中的 modelCapabilities，且必须与当前 chatModel 匹配。这个字段描述模型能力边界，不是某次请求已经消耗的 token 数；会话自己的 context_tokens 持久化在 SQLite。\n📷 [图片 token=T3PGb6WtmoB8t7xeDR0cIm6rnuf（未能下载，见飞书原文）]\n权限、安全与失败边界 本地 JSON 文件虽然被忽略，仍是明文敏感文件，应依赖操作系统文件权限和工作目录安全。不要把它们粘贴进 issue、日志、截图或测试 fixture。.gitignore 只能阻止正常未跟踪添加，不能清除已经进入 Git 历史的秘密，因此密钥一旦泄露仍需轮换。\n📷 [图片 token=KLfIbADSJo7tsJx044UcpxCGnBh（未能下载，见飞书原文）]\n日志、/ready 和 /config/check 只允许 provider、model、base URL、延迟和安全错误。API key 不应进入 LlmReadinessResult，rerank 的 Authorization header 与上游响应正文不进入 LlmRerankError。业务层也不得捕获外部异常后直接串行化到 SSE。\n📷 [图片 token=Q2BZbeh34o0GtexHLRWcEqp4nMF（未能下载，见飞书原文）]\n外部模型不可用时必须如实失败：聊天流发送结构化 error 且不保存部分 assistant 消息；embedding 失败使索引任务和文档索引状态变为 failed，可手动重试；rerank 失败不能伪造分数。配置缺字段或 capability 不匹配时应在构建 provider 阶段报配置错误，而不是切换到未声明的模型或环境变量。\n📷 [图片 token=EGKzb0mHpomAZhxwXdvcIJ2KnqG（未能下载，见飞书原文）]\n阅读顺序与小结 先读 apps/backend/src/super_ai/project_config.py，准确理解路径选择和深度合并。\n再读 apps/backend/src/super_ai/llm/config.py，列出从 JSON 到类型化配置的所有必需字段。\n阅读 apps/backend/src/super_ai/llm/provider.py，区分聊天、embedding、rerank 工厂和 readiness。\n深入 apps/backend/src/super_ai/llm/rerank.py 与 apps/backend/src/super_ai/documents/indexing.py，核对外部响应验证和失败状态。\n最后沿 provider、readiness 与大文档索引调用链，确认外部访问时机和脱敏边界。\nQwen 接入的工程价值在于“可替换且可失败”：业务依赖协议，具体客户端集中创建；配置来源单一可解释，密钥不进入公共状态；embedding 和 rerank 对供应商限制与不可信响应做显式处理。模型能力越强，越需要这种窄而清晰的 provider 边界来保证权限、恢复和可观测性仍由应用掌控。\n📷 [图片 token=W5VqbEY10oCYOAxCCSjcD6donVc（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/04.%20Qwen%20%E6%A8%A1%E5%9E%8B%E6%8E%A5%E5%85%A5%E4%B8%8E%E6%9C%AC%E5%9C%B0%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE/","summary":"OncallAgent 通过 OpenAI-compatible 协议接入 Qwen，而不是让业务代码直接依赖厂商私有 SDK。聊天模型与 embedding 使用  langchain-openai  的  ChatOpenAI  和","title":"04. Qwen 模型接入与本地安全配置"},{"content":"今天咱们来聊一个很多朋友都在问的话题：做 Agent 开发，到底该选什么技术栈？\n如果你之前已经跟着我们学了 RAG、ReAct、Plan-Execute 这些 Agent 的核心架构设计，那你心里多半已经有一个疑问了：\u0026ldquo;道理我都懂了，那具体用什么语言、什么框架来落地呢？\u0026rdquo;\n这篇文章就是来帮你回答这个问题的。我会把 Java、Go、Python 三个语言版本的 Agent 开发技术栈全部拆开给你看，每个语言用什么框架、框架能帮你做什么、底层是怎么串起来的，一次性讲明白。\n不管你是 Java 出身的后端同学，还是写 Go 的基础架构选手，又或者是 Python 起步的 AI 爱好者，看完这篇你都能找到适合自己的那条路。\n先搞清楚：Agent 技术栈到底需要哪些「积木」？ 📷 [图片 token=KYA4b8deeomwBmx1O8EcKwAHnGh（未能下载，见飞书原文）]\n在聊具体框架之前，我们得先搞清楚一个问题：开发一个 Agent，到底需要哪些技术能力？\n你可以把 Agent 想象成一个「会思考、会干活的助手」。那要造一个助手出来，你至少需要这些「积木块」：\n第一块：Web 框架（助手的身体）\nAgent 总得有个入口，让用户能跟它对话吧？不管是通过 HTTP 接口、WebSocket 还是聊天窗口，你需要一个 Web 框架来承载这些网络通信。它就是 Agent 的「身体」，负责接收请求、返回响应。\n第二块：AI 编排框架（助手的大脑）\n这是 Agent 技术栈里最核心的部分。所谓「编排」，你可以理解为「导演」。\n一个 Agent 在工作的时候，要做很多事情：调用大模型去思考、去知识库里检索文档、调用外部工具获取数据、把多个步骤串成一条工作流……这些事情的先后顺序、怎么衔接、出错了怎么办，都需要有个「导演」来统筹安排。\nAI 编排框架就是那个导演。没有它，你就得自己手写所有的流程控制逻辑，想想都累。\n第三块：大模型接入（助手的智商）\nAgent 的「聪明程度」取决于背后接的是哪个大模型。通义千问、GPT、DeepSeek……不同的模型能力不同、调用方式不同，编排框架需要帮你屏蔽这些差异，让你可以方便地切换模型。\n第四块：工具和知识库（助手的技能包）\n做 RAG 要接向量数据库，做 ReAct 要注册工具函数，做 Plan-Execute 要编排多步骤任务……这些能力都需要框架提供现成的支持，让你专注于业务逻辑而不是底层对接。\n好了，积木块清楚了。那三个语言分别怎么搭这套积木呢？咱们一个一个来看。\nPython 版：LangChain + LangGraph Python 在 AI 领域的地位，怎么说呢，就像 Java 在企业后端的地位一样，属于「当仁不让的主角」。AI 领域绝大多数的论文、模型、工具，第一个版本基本都是 Python 的。所以 Python 的 Agent 生态也是三个语言里最成熟的。\nPython 版的技术栈组合是：LangChain（AI 编排框架）+ LangGraph（工作流引擎）。\n📷 [图片 token=AO5FbWE39oBDscxiXeVcLO0On8d（未能下载，见飞书原文）]\n2.1 LangChain 是什么？ 你可能听说过 LangChain 这个名字，它在 AI 开发圈子里几乎是「绕不过去」的存在。但很多同学对它的印象停留在「好像是个 AI 框架」这个层面，具体它干了什么、解决了什么问题，并不太清楚。\n咱们从一个实际痛点说起。假设你要用 Python 开发一个简单的 RAG 对话助手，不借助任何框架，你需要做什么？\n自己写代码调用大模型的 API（还得处理不同模型的调用差异）\n自己对接向量数据库（Milvus、Chroma 各有各的 SDK）\n自己写文档切分逻辑（按段落切？按 Token 数切？）\n自己写 Embedding 调用逻辑\n自己写检索、重排、拼接 Prompt 的流程\n自己做对话历史管理（多轮对话要记住上下文）\n……\n光列出来就知道有多繁琐了吧？而且这些逻辑，每做一个新项目你可能都要重写一遍。\nLangChain 做的事情，就是把这些重复性的「脏活累活」全部封装好，让你用几行代码就能搞定。\n你可以把 LangChain 理解为一个「AI 开发的工具箱」。它帮你做了三件关键的事情：\n第一件：统一模型接口\n不管你用的是通义千问、GPT-4、还是 DeepSeek，LangChain 都帮你封装成了统一的接口。你要切换模型？改一行配置就行，业务代码一行都不用动。\n这就好比你家里的各种充电器，虽然手机品牌不同，但都用 Type-C 接口。LangChain 就是那个 Type-C 标准，让你不用关心底层差异。\n第二件：封装常用组件\n文档加载器、文本切分器、Embedding 模型、向量数据库、检索器、对话记忆……这些 AI 开发中常用的组件，LangChain 都帮你封装好了，而且还提供了几十上百种实现。\n比如向量数据库，你要用 Milvus 就导入 Milvus 的组件，要换 Chroma 就换个导入，接口是一样的。\n第三件：链式编排\nLangChain 名字里的「Chain」就是「链」的意思。它允许你把多个组件像搭积木一样串成一条「链」。比如一条 RAG 链可能是：用户问题 → 检索器 → Prompt 模板 → 大模型 → 输出解析器。\n你只需要定义好每个环节用什么组件，LangChain 帮你把数据在组件之间自动流转。\n说白了，LangChain 就是 AI 开发的「Spring」。Spring 帮 Java 开发者屏蔽了 Web 开发的底层复杂度，LangChain 帮 Python 开发者屏蔽了 AI 开发的底层复杂度。\n2.2 那 LangGraph 又是什么？ 你可能会想：LangChain 已经能做链式编排了，为什么还需要一个 LangGraph？\n好问题。这就要说到 LangChain 的一个局限了。\nLangChain 的「链」本质上是一条直线：A → B → C → D，数据从头流到尾，中间没有分叉、没有循环。对于简单的 RAG 场景，这完全够用了。\n但 Agent 的工作方式可不是一条直线。\n回想一下我们之前讲的 ReAct 模式：思考 → 行动 → 观察 → 再思考 → 再行动……这是一个循环。还有 Plan-Execute 模式：先制定计划 → 执行第一步 → 检查结果 → 可能要修改计划 → 再执行……这里面既有循环，又有分支判断。\n用做饭来类比的话：LangChain 的「链」适合做一道简单菜，比如煮面条，步骤就是烧水 → 下面 → 捞出来 → 加调料，一条线走到底。但如果你要做一桌满汉全席，有些菜要同时开做，有些菜要根据前面的结果决定调料放多少，有些菜做砸了要重来，那你就需要一个更高级的「调度系统」。\nLangGraph 就是这个调度系统。\nLangGraph 的核心概念是图（Graph）。它把 Agent 的工作流程建模成一张图，图里有：\n节点（Node）：每个节点代表一个「工作步骤」，比如「调用大模型思考」「执行工具」「检查结果」\n边（Edge）：节点之间的连线，定义了流程的走向。而且边可以是有条件的，比如「如果模型说要调用工具，就走到工具节点；如果模型说可以回答了，就走到结束节点」\n而且 LangGraph 还有一个很厉害的特性：状态管理。\n在复杂的多步骤 Agent 中，你需要记住「当前执行到哪一步了」「中间产生了什么结果」「计划列表还剩哪些没做」这些信息。LangGraph 内置了一套状态管理机制，每个节点执行完都可以更新全局状态，下一个节点可以读取到最新的状态。\n这就好比一个项目经理手里的看板：每完成一个任务就更新看板状态，所有团队成员都能看到最新进展。\n2.3 它们怎么配合工作？ 搞清楚了两个框架各自的职责，你可能想问：它们是怎么配合的？\n其实很简单。LangGraph 负责控制「流程怎么走」，LangChain 提供「每一步用什么工具」。\n举个例子，你要做一个 RAG 对话 Agent：\nLangGraph 定义了整体工作流：接收问题 → 判断是否需要检索 → 检索知识库 → 生成回答 → 检查质量 → 输出\nLangChain 提供了每个节点需要的组件：检索器用 LangChain 封装好的向量检索器，大模型调用用 LangChain 封装好的模型接口，Prompt 拼接用 LangChain 的模板\n两者搭配，LangGraph 是骨架，LangChain 是血肉。\n2.4 Python 版技术栈总览 最后来一张全景图：\n角色 技术选型 干什么用的 AI 编排框架 LangChain 封装模型调用、组件对接、链式编排，屏蔽底层差异 工作流引擎 LangGraph 实现带循环、带条件分支的复杂 Agent 工作流 大模型 通义千问 / GPT / DeepSeek 等 通过 LangChain 统一接口接入 向量数据库 Milvus / Chroma 等 通过 LangChain 组件对接 Web 框架 FastAPI（可选） 提供 HTTP 接口，承载 Agent 服务 Python 这套方案的最大优势是生态成熟。遇到问题网上搜一搜，大概率能找到答案。社区活跃，更新快，新功能通常第一时间支持。\n不过也有不足：Python 本身的性能和类型安全在生产环境里是短板，适合快速原型验证和 AI 研究场景，但如果你们团队的主力语言不是 Python，纯粹为了做 Agent 而引入 Python 技术栈，后期维护成本可能会比较高。\nJava 版：SpringBoot + Spring AI Alibaba 如果你的团队是 Java 技术栈（国内大量企业都是），那恭喜你，Java 生态里现在也有了非常趁手的 Agent 开发方案：SpringBoot + Spring AI Alibaba。\n为什么把 Java 放在第二个讲？因为 Java 版技术栈的设计思路和 Python 版是相通的，理解了 Python 版的逻辑，Java 版你很快就能理解。\n📷 [图片 token=RWHebMqCsoAJsixZjLGctxqmnze（未能下载，见飞书原文）]\n3.1 为什么是 SpringBoot？ 这个问题可能你都不用我回答。对于 Java 开发者来说，SpringBoot 几乎等于「呼吸一样自然」的存在。\n但还是简单说一下，SpringBoot 在 Agent 技术栈里扮演的角色就是前面说的「身体」：\n提供 HTTP/WebSocket 接口，让用户能和 Agent 对话\n管理依赖注入、配置管理，让代码结构清晰\n提供流式返回（SSE）的能力，实现「逐字蹦出」的回答效果\n提供监控、日志、健康检查等生产级基础设施\n如果你已经有 SpringBoot 的项目底座，那 Agent 开发就是在现有基础上「加一层 AI 能力」，不需要从零搭一套新架构。\n3.2 Spring AI Alibaba 是什么？ 这才是 Java 版技术栈的重头戏。\nSpring AI Alibaba 是阿里巴巴基于 Spring AI 框架做的增强版本。那问题来了，Spring AI 又是什么？\nSpring AI 是 Spring 官方推出的 AI 应用开发框架。就像 Spring Data 帮你简化了数据库操作、Spring Security 帮你简化了安全认证一样，Spring AI 的目的是帮你简化 AI 应用的开发。\n它的核心理念和 LangChain 非常像：统一接口、屏蔽差异、简化开发。\n但 Spring AI 有个问题：它最初主要对接的是 OpenAI 等国外大模型，对国内的通义千问、文心一言等模型的支持不够好，对国内常用的向量数据库的适配也不够完善。\nSpring AI Alibaba 就是来补这个缺的。\n它在 Spring AI 的基础上，增强了对国内 AI 生态的支持，尤其是对阿里云的通义系列模型和相关云服务做了深度集成。\n用一句话概括：Spring AI Alibaba = Spring AI 的国内增强版，专门为国内开发者的使用场景做了优化。\n3.3 Spring AI Alibaba 提供了什么能力？ 咱们还是用「积木块」的思路来拆解，看看它怎么帮你搭 Agent。\n能力一：统一的模型调用接口\n和 LangChain 一样，Spring AI Alibaba 帮你把不同大模型的调用方式统一了。它提供了一个 ChatClient 接口，不管背后是通义千问还是 DeepSeek，你写的代码都是一样的。\n这意味着什么呢？意味着你的 Agent 代码和具体的大模型是解耦的。今天用通义千问，明天老板说试试 DeepSeek，你只需要改一下配置文件里的模型名，代码一行不用动。\n对于经历过「甲方今天要换这个模型、明天要换那个模型」的同学来说，这简直是救命的特性。\n能力二：开箱即用的 RAG 支持\n做知识库 Agent 需要的 RAG 能力，Spring AI Alibaba 都帮你封装好了：\n文档加载：支持加载 PDF、Markdown、HTML 等多种格式的文档\n文本切分：内置了多种切分策略，帮你把长文档拆成适合检索的片段\nEmbedding：对接 Embedding 模型，把文本转成向量\n向量存储和检索：对接向量数据库，存储向量并支持相似度搜索\n这一套下来，一条 RAG 链路就搭好了。你不需要自己去研究怎么调 Milvus 的 SDK、怎么处理不同文档格式的解析差异，框架都帮你包好了。\n能力三：Function Call 支持\n还记得我们之前讲 ReAct 时提到的 Function Call 吗？Spring AI Alibaba 对这个能力的支持非常优雅。\n你只需要写一个普通的 Java 方法，加上注解和描述，框架就会自动把它注册为一个 AI 可以调用的「工具」。大模型在思考的时候，会自动判断需不需要调用你的工具，如果需要，框架会自动帮你完成调用并把结果回传给模型。\n这个过程对你来说几乎是透明的。你只需要关心「工具本身的业务逻辑怎么写」，调用时机、参数解析、结果回传这些事情框架全包了。\n能力四：工作流编排\n这是 Spring AI Alibaba 里非常重要的一个能力，对标的就是 Python 里的 LangGraph。\nSpring AI Alibaba 提供了一套 Graph（图） 编排能力，让你可以用图的方式定义 Agent 的工作流。节点、边、条件分支、循环……这些 LangGraph 能做的事情，在 Java 里一样能做。\n它还内置了对几种常见 Agent 模式的支持：\nReAct 模式：思考 → 行动 → 观察的循环\nPlan-Execute 模式：先制定计划，再逐步执行，执行中可以修改计划\n这些模式你不需要从零实现，框架提供了现成的「蓝图」，你在蓝图基础上配置自己的工具和提示词就行。\n3.4 和 Python 版的对应关系 看到这里，你应该已经发现了：Java 版和 Python 版的架构思路是高度一致的。我给你画一个对照表：\n能力 Python 版 Java 版 AI 编排 + 组件封装 LangChain Spring AI Alibaba 工作流（图）编排 LangGraph Spring AI Alibaba 内置的 Graph 能力 Web 框架 FastAPI SpringBoot 统一模型接口 LangChain ChatModel Spring AI ChatClient RAG 能力 LangChain Retriever 等组件 Spring AI Alibaba 内置 RAG 组件 Function Call LangChain Tool 注解 Spring AI Alibaba Function 注解 看到了吧？殊途同归。只是换了个语言、换了个框架的名字，核心思路完全一样。\n所以如果你已经理解了 Python 版的架构，转到 Java 版几乎是零成本的。反过来也一样。\n3.5 Java 版技术栈总览 角色 技术选型 干什么用的 Web 框架 SpringBoot HTTP 接口、SSE 流式返回、依赖管理等基础设施 AI 编排框架 Spring AI Alibaba 统一模型接口、RAG 组件、Function Call、工作流编排 大模型 通义千问 / DeepSeek 等 通过 Spring AI 统一接口接入 向量数据库 Milvus / Elasticsearch 等 通过 Spring AI 组件对接 Java 版方案的最大优势是企业级友好。如果你们团队已经是 Spring 技术栈，引入 Spring AI Alibaba 几乎没有学习门槛，而且 Spring 生态的成熟度、稳定性、监控能力在生产环境里都是有保障的。\n另外一个优势是和阿里云生态的深度集成。如果你们公司用的是阿里云的基础设施（通义千问、向量检索服务等），Spring AI Alibaba 在对接上会非常丝滑。\nGo 版：GoFrame + Eino 最后来说说 Go 版。Go 语言近几年在云原生、基础架构领域大放异彩，但在 AI 应用开发领域，它的起步确实比 Python 和 Java 要晚一些。\n不过别着急，后发不等于落后。Go 版的 Agent 技术栈虽然年轻，但设计上是站在前人肩膀上的，很多地方的思路反而更清晰。\nGo 版的技术栈组合是：GoFrame（Web 框架）+ Eino（AI 编排框架）。\n📷 [图片 token=SVO0bSongoMeUWxyr0PcXb6enHd（未能下载，见飞书原文）]\n4.1 GoFrame 是什么？ GoFrame 是 Go 语言生态里一个非常成熟的企业级开发框架，你可以把它理解为 Go 版的 SpringBoot。\n为什么这么说？因为它们解决的问题非常像：\nWeb 服务：提供 HTTP 服务、路由管理、中间件支持\n配置管理：统一管理各种配置文件\n日志系统：结构化日志、日志分级\n数据库 ORM：简化数据库操作\n开发工具链：代码生成、项目脚手架等\n如果你是写 Go 的同学，大概率听说过甚至用过 GoFrame。它在国内 Go 社区的影响力很大，文档也是中文的，上手门槛低。\n在 Agent 技术栈里，GoFrame 扮演的角色和 SpringBoot 一样，就是提供 Web 服务的底座。Agent 的 HTTP 接口、SSE 流式返回、配置管理等基础设施，都由 GoFrame 来搞定。\n4.2 Eino 是什么？ Eino（读音类似「诶诺」）是字节跳动开源的一套 Go 语言 AI 应用开发框架。\n如果说 LangChain 是 Python 世界的 AI 编排框架、Spring AI Alibaba 是 Java 世界的 AI 编排框架，那 Eino 就是 Go 世界的 AI 编排框架。\n你可能会好奇：为什么字节要自己做一个 Go 版的 AI 框架？\n原因很直接。字节跳动内部大量使用 Go 语言，他们在做内部 AI 应用的时候发现：Go 生态里缺一个好用的 AI 编排框架。现有的 AI 框架几乎都是 Python 的，Go 开发者想做 AI 应用，要么硬着头皮写 Python，要么自己从零造轮子。\n于是字节就把自己内部沉淀的 AI 应用开发能力抽象出来，做成了 Eino 这个开源框架。它在字节内部已经支撑了大量的 AI 应用，包括大家熟悉的豆包等产品背后的部分能力。\n4.3 Eino 提供了什么能力？ Eino 的能力和 LangChain、Spring AI Alibaba 在大方向上是对齐的，但在具体设计上有自己的特色。咱们还是一个一个来拆解。\n能力一：组件化的 AI 能力\nEino 把 AI 应用开发中常用的能力抽象成了一个个标准化的「组件」。\n比如：\nChatModel 组件：负责和大模型对话，支持通义千问、DeepSeek 等主流模型\nRetriever 组件：负责从知识库中检索相关文档\nEmbedding 组件：负责把文本转成向量\nTool 组件：负责封装外部工具，让 AI 可以调用\nDocument Loader 组件：负责加载各种格式的文档\n每个组件都定义了标准的接口（这是 Go 语言的强项，接口设计天然清晰），你可以随时替换具体的实现。比如 Retriever 组件，今天底层用 Milvus，明天换 Elasticsearch，上层代码不需要任何改动。\n这种设计和 LangChain 的思路是一脉相承的。Go 语言本身「面向接口编程」的理念在这里发挥得淋漓尽致。\n能力二：流式编排\n这是 Eino 在设计上比较有特色的一点。\n在 AI 应用里，「流式」是一个非常重要的概念。用户和 Agent 对话时，不希望等半天才看到一个完整回答，而是希望回答像打字一样一个一个字蹦出来。这就要求从大模型到最终输出的整条链路，都要支持「流式处理」。\nEino 在编排层面就把「流式」作为一等公民来对待。它的每个组件都天然支持流式输入和输出，编排的时候不需要你额外处理流式逻辑。\n打个比方：你在做一条流水线，有的工人干活快、有的干活慢。传统的做法是等一个工人全部做完，再把半成品交给下一个工人。但 Eino 的做法是：前一个工人做出一点成果就立刻传给下一个，大家「流水线式」地并行工作。这样整体的响应速度就快多了。\n能力三：图编排（Graph）\n和 LangGraph、Spring AI Alibaba 的 Graph 能力一样，Eino 也支持用「图」的方式来编排复杂的 Agent 工作流。\n节点、边、条件分支、循环，这些核心能力一个不少。你可以用 Eino 的 Graph 来实现 ReAct、Plan-Execute 等各种 Agent 模式。\n而且 Eino 的 Graph 设计充分利用了 Go 语言的并发优势。多个不相互依赖的节点可以自动并行执行，不需要你手动管理协程。这在处理复杂工作流的时候，性能优势是比较明显的。\n能力四：回调和可观测性\n开发 Agent 有一个很现实的问题：调试太难了。\n一个 Agent 的工作流可能有十几个步骤，中间调了好几次大模型、好几个工具。如果最终输出不对，你得知道到底是哪一步出了问题。\nEino 提供了完善的回调机制。你可以在每个组件、每个节点上挂回调函数，记录输入输出、执行耗时、错误信息等。这些信息对于调试和优化 Agent 来说是非常宝贵的。\n而且 Eino 还支持和 OpenTelemetry 等可观测性标准集成。也就是说，Agent 每一步的执行轨迹都可以被追踪和可视化，就像一条链路追踪（Trace）一样清晰。\n4.4 和另外两个版本的对应关系 同样，来一张三方对照表：\n能力 Python 版 Java 版 Go 版 AI 编排 + 组件封装 LangChain Spring AI Alibaba Eino 工作流（图）编排 LangGraph Spring AI Alibaba Graph Eino Graph Web 框架 FastAPI SpringBoot GoFrame 统一模型接口 ChatModel ChatClient ChatModel 组件 RAG 能力 Retriever 等 内置 RAG 组件 Retriever/Embedding 组件 Function Call Tool 注解 Function 注解 Tool 组件 流式支持 支持 支持 原生流式编排（更强调） 看到了吧？三个版本的架构思路是完全一致的。不管你用哪个语言，要做的事情是一样的，只是实现的框架不同。\n4.5 Go 版技术栈总览 角色 技术选型 干什么用的 Web 框架 GoFrame HTTP 接口、SSE 流式返回、配置管理等基础设施 AI 编排框架 Eino 组件化 AI 能力、流式编排、图编排、回调和可观测性 大模型 通义千问 / DeepSeek 等 通过 Eino ChatModel 组件接入 向量数据库 Milvus 等 通过 Eino Retriever 组件对接 Go 版方案的最大优势是性能和并发。Go 语言天生的协程模型和高性能特性，在需要处理大量并发请求的生产环境里非常有优势。如果你们团队的技术栈是 Go（尤其是云原生方向），Eino 是目前最合适的选择。\n另外值得一提的是，Eino 背后是字节跳动，框架本身经过了大规模生产验证，稳定性和实用性是有保障的。\n三个版本怎么选？ 📷 [图片 token=Su9CbekUKoqtRtxLDAbc6Si1nNf（未能下载，见飞书原文）]\n讲完了三个语言版本的技术栈，最后来聊聊怎么选的问题。\n先说结论：三个版本在架构设计上是等价的，选择的核心依据是你团队的技术栈，而不是框架本身的优劣。\n为什么这么说？因为从前面的分析你应该已经看到了，不管是 Python 的 LangChain + LangGraph，还是 Java 的 SpringBoot + Spring AI Alibaba，还是 Go 的 GoFrame + Eino，它们解决的问题是一样的，提供的能力是对等的，只是语法和 API 风格不同。\n所以选择标准非常明确：\n你团队主力写 Python？ 选 LangChain + LangGraph。生态最成熟，社区资源最丰富，遇到问题最容易找到解决方案。适合 AI 研究团队、快速原型验证、数据科学背景的团队。\n你团队主力写 Java？ 选 SpringBoot + Spring AI Alibaba。和现有 Spring 项目无缝集成，学习成本最低，企业级特性最完善。适合传统互联网公司、金融等对稳定性要求高的场景。\n你团队主力写 Go？ 选 GoFrame + Eino。性能最强，并发处理最好，和云原生生态契合度最高。适合基础架构团队、对性能有要求的场景。\n换句话说，不要为了用某个 AI 框架而去学一门新语言。AI 框架只是工具，你的业务逻辑、团队协作效率、代码可维护性才是最重要的。用你最熟悉的语言，选那个语言里最成熟的 AI 框架，这就是最优解。\n当然，如果你是个人学习者，想从零开始学 Agent 开发，我的建议是先从 Python 入手。原因很简单：Python 版的教程最多、社区最活跃、上手最快，你可以用最短的时间跑通一个 Agent 原型，建立起对整个体系的认知。等你理解了核心概念之后，再切换到 Java 或 Go 版，就是换个框架的事儿。\n总结 最后，让我们来做一个总复盘。\n今天我们把 Agent 开发的三个语言版本的技术栈全部拆解了一遍。核心要记住的就是这几点：\nAgent 技术栈需要哪些积木？\nWeb 框架（身体）+ AI 编排框架（大脑）+ 大模型接入（智商）+ 工具和知识库（技能包） Python 版怎么搭？\nLangChain 做组件封装和链式编排 + LangGraph 做复杂工作流的图编排，生态最成熟 Java 版怎么搭？\nSpringBoot 做 Web 底座 + Spring AI Alibaba 做 AI 编排（统一模型接口、RAG、Function Call、Graph 工作流全包了），企业级最友好 Go 版怎么搭？\nGoFrame 做 Web 底座 + Eino 做 AI 编排（组件化设计、原生流式编排、Graph 工作流、可观测性），性能最强 怎么选？\n团队用什么语言就选什么版本，不要为框架换语言。核心架构思路是完全一致的，换语言只是换了一层皮。 好了，三个语言版本的技术栈全景图就给大家画完了。有了技术栈的认知基础，接下来我们就可以进入具体的实战环节，一步步把 Agent 搭起来。\n我们下篇见！\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%8C%E7%AB%A0%20_%20%E9%A1%B9%E7%9B%AE%E5%85%A8%E5%B1%80%E8%AE%A4%E7%9F%A5%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/Java%E3%80%81Go%E3%80%81Python%20%E4%B8%89%E7%89%88%E6%9C%AC%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%A0%88%E5%85%A8%E8%A7%A3%E6%9E%90/","summary":"今天咱们来聊一个很多朋友都在问的话题： 做 Agent 开发，到底该选什么技术栈？ 如果你之前已经跟着我们学了 RAG、ReAct、Plan-Execute 这些 Agent 的核心架构设计，那你心里多半已经有一个疑问了：\u0026ldquo;道理我都懂了，那","title":"Java、Go、Python 三版本核心技术栈全解析"},{"content":".openspec.yaml 是 change 目录中的元数据文件。它很小，但作用非常明确：告诉 OpenSpec 这个 change 采用什么工作流 schema，以及创建时间等生命周期信息。它不是需求文档，也不是运行时配置，更不能存放 API Key、数据库地址或产品参数。\n📷 [图片 token=RZi8blpxJooS5AxMKG6c9SEEnDg（未能下载，见飞书原文）]\n在 OncallAgent 中长什么样 主案例 2026-07-11-limit-qwen-embedding-batch-size 的内容只有两行：\nschema: spec-driven created: 2026-07-11 schema: spec-driven 表示该变更使用规范驱动的产物图。工具不会只凭文件名猜流程，而会按 schema 解释有哪些 artifact、它们怎样依赖、什么时候达到 apply-ready。created 保存创建日期，归档后文件仍随整个目录保留，为历史追溯提供时间信息。\n📷 [图片 token=FwnpbPacioYm2Mx62fRc71konZd（未能下载，见飞书原文）]\nschema 到底决定了什么 默认 spec-driven schema 的核心依赖可以理解为：\nproposal 无前置依赖 specs 依赖 proposal design 依赖 proposal tasks 依赖 specs + design 因此工具先知道 proposal 可创建；proposal 完成后，specs 与 design 都解锁；两者完成后，tasks 才解锁。这个依赖图比“固定执行四条命令”更灵活，因为 OpenSpec 可以支持其他 schema。自定义 schema 可能增加研究、测试计划、迁移计划等产物，也可能针对小型维护任务缩短流程。\n📷 [图片 token=VqL5bFNLooMJgjxuIvQcY13fnsg（未能下载，见飞书原文）]\nOncallAgent 当前仓库中的 OpenSpec Skills 会先运行 openspec status --change \u0026lt;name\u0026gt; --json，读取 schemaName、artifacts、applyRequires、artifactPaths 和 actionContext，再决定下一步，而不是把路径和顺序硬编码在业务逻辑中。\n📷 [图片 token=KjnDbPdQComSNlxp0WFcvn8YndD（未能下载，见飞书原文）]\n为什么元数据必须与业务内容分开 业务内容会被人审查和修改，例如 proposal 的范围可能缩小，design 的决策可能调整，tasks 可能重新拆分。schema 与创建时间属于工具解释 change 所需的控制信息。如果把二者混在 proposal 中，工具需要从自然语言猜流程；如果把需求写进 YAML，又会让审查者在多个格式间来回寻找。\n📷 [图片 token=OVgbbW4RWoujEpxABHDc7ABHnRH（未能下载，见飞书原文）]\n这种分离与 OncallAgent 自身的分层思想一致：共享契约描述协议，业务代码实现行为，项目 JSON 配置负责运行参数，OpenSpec 元数据负责变更工作流。每个文件只承担一种稳定职责，边界越清晰，Codex 越不容易在错误位置修改内容。\n📷 [图片 token=NLDCbPbOYoaHW2xcuv0cFIy2nke（未能下载，见飞书原文）]\n创建、继续和归档时怎样使用 创建 change 时，openspec new change \u0026quot;\u0026lt;name\u0026gt;\u0026quot; 会建立脚手架和元数据。随后 openspec status 根据 schema 告诉 Agent 哪个 artifact ready、哪个 blocked。openspec instructions \u0026lt;artifact-id\u0026gt; 再提供该 artifact 的模板、规则、依赖和目标路径。\n归档时，整个 change 目录被移动到带日期的 archive 路径，.openspec.yaml 一起保留。它不会被 OncallAgent 的 VitePress wiki-sync 聚合页 include，因为面向读者的页面重点是 proposal、design、tasks 和 delta specs；但元数据仍是归档审计的一部分。\n📷 [图片 token=QIrbbWnkpo8rFHxaZACcvrQlnzc（未能下载，见飞书原文）]\n常见错误 **把 YAML 当项目配置。**OpenSpec schema 不控制 FastAPI、Vue、Milvus、模型或 MCP 的运行参数。OncallAgent 的运行配置来自被 Git 忽略的项目 JSON，而不是这个文件。\n**手工改 schema 却不检查 artifact。**改变 schema 可能改变产物图和模板，不能只改一行名字。应先查看可用 schema 和状态，再决定是否迁移。\n**删除归档中的元数据。**即使 VitePress 页面不展示它，也应与其他 artifact 一起保留，避免历史 change 失去工作流上下文。\n📷 [图片 token=SpUebanTdoQaZJxvREpcmpgMntg（未能下载，见飞书原文）]\n面试表达 .openspec.yaml 不是需求，它是变更的工作流元数据。我们用它固定 schema 和创建信息，OpenSpec 再根据 schema 计算 artifact 的依赖和 ready 状态。业务内容放在 Markdown，控制信息放在 YAML，归档时两者一起保留，所以既方便人审查，也方便工具确定性执行。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/openspec.yaml%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":".openspec.yaml  是 change 目录中的元数据文件。它很小，但作用非常明确：告诉 OpenSpec 这个 change 采用什么工作流 schema，以及创建时间等生命周期信息。它不是需求文档，也不是运行时配置，更不能存放","title":"openspec.yaml文件作用介绍"},{"content":"我们从最基础的大模型、Prompt，一路学到了 Agent、Function Calling、MCP，前面这些技术在咱们之前的「智能OnCall项目」中都有用到。\n不过，从 26 年初开始，AI 圈子又出了个叫 Skills（技能） 的新东西。\n当时我们在做智能 OnCall 项目时，还没有 Skills 这个概念，自然项目也就没有用到。不过今天，我还是得专门带大家来学习一下 Skills 这个新玩意，因为它现在真的太火了，现在的 AI 岗位面试几乎逢考必问！\n很多同学在看各种 AI 框架源码时，经常被这几个词绕晕：「它们到底是个啥？谁先出谁后出？有啥区别？」\n别慌，今天咱们就泡杯茶，用最接地气的话，按照技术演进的真实时间线，把这三个概念从头到尾盘一盘。\n第一阶段：AI 的「破壁人」 ， Function Calling（函数调用） 咱们先回想一下早期的 ChatGPT。\n那时的它就像是一个被关在小黑屋里的超级学霸：虽然上知天文下知地理，但他没网（没法查实时数据），也没手（没法帮你真正去执行操作，比如发邮件、查数据库）。\n为了让大模型能连接外部世界，Function Calling 诞生了。\n用白话说，这就是大模型和外部世界打交道的一种「底层暗号」。\n📷 [图片 token=OgsJbzjN2o7GbMx4YzfcyJqvnnb（未能下载，见飞书原文）]\n你想让 AI 查今天深圳的天气，它自己查不到，但你可以给它提供一个 get_weather 的函数说明书。\nAI 看到你的需求和说明书后，不会直接回答天气，而是输出一段 JSON 代码：「我决定调用 get_weather，参数是 深圳」。 然后你的代码拿着这个参数去真正调用 API，查到结果后再喂给 AI，AI 最后总结成人类语言回复你。\n划重点： Function Calling 是最基础的机制，它赋予了 AI 使用工具的「潜能」。\n第二阶段：大一统的「Type-C 接口」 ， MCP (模型上下文协议) 有了 Function Calling，大家都很兴奋，开始给 AI 疯狂写各种工具（查天气、查 MySQL、读 Github 等等）。\n但很快，新的灾难来了：接口完全不兼容。\n张三用 Python 写了个查数据库的工具，李四用 Java 搞了个读本地文件的工具。由于没有统一的标准，你的 AI 应用想要接入这些现成的工具，就得挨个去适配它们千奇百怪的数据格式和通信逻辑。这就像你买了个新手机，却发现大家用的充电线有圆头、扁头、方头，简直让人崩溃。\n这时候，MCP（Model Context Protocol，模型上下文协议） 闪亮登场。\n📷 [图片 token=RJjabe09goCik2xyfHxczJu5n8y（未能下载，见飞书原文）]\n用大白话说，MCP 就是 AI 世界里的 「Type-C 接口标准」。\n它是一套通用的开源协议。只要外部工具（数据源、数据库、代码仓库）按照 MCP 的标准开发成 MCP Server，那么无论前端是哪个大模型（MCP Client），都可以无缝、零代码修改地直接插上去用。MCP 把 Function Calling 的能力彻底标准化了。\n第三阶段：终极进化 ， Skills（技能）为什么会出现？ 好，现在 AI 有了底层机制（Function Calling），也有了标准化的工具箱（MCP）。是不是就天下太平了？\n并没有！随着咱们让 AI 干的活越来越复杂，一个极其痛点的问题暴露了出来：每次让 AI 干活，沟通成本太高了！\n举个例子，假设你要让 AI 帮你写一份项目日报。你手里虽然已经有了 MCP 提供的「读取数据库」和「读取 Jira」的标准化工具，但你每次跟 AI 聊天，还是得像个唐僧一样反复叮嘱。\n咱们来看看没有 Skills 之前的现状：\n现状：每次对话都要重新描述相同的工作流程 用户：\u0026ldquo;帮我按XX格式生成报告\u0026rdquo; 用户：\u0026ldquo;生成报告前，先去调用数据库工具获取今天的数据\u0026rdquo; 用户：\u0026ldquo;记得要包含数据分析和总结部分\u0026rdquo; 用户：\u0026ldquo;别忘了图表的排版细节\u0026hellip;\u0026rdquo; （每次生成报告，都要重复这段痛苦的念经过程）\n发现问题了吗？ 工具虽然标准化了，但「使用工具的流程和大脑的思考方式（Prompt）」并没有被沉淀下来。\n为了解决这个痛点，Skills（技能） 应运而生！\n什么是 Skill？ Skill 就像是给 AI 定制的一份 「标准作业程序 (SOP)」。它把解决特定问题所需的 背景设定 (Prompt)、执行步骤 和 以调用的资源（脚本、模板，或者通过 MCP 对接的外部工具），打包成了一个干净利落的代码文件。\n📷 [图片 token=PdDIb8VTMowWZEx0zd4c7FcLnk5（未能下载，见飞书原文）]\n咱们来看看用了 Skills 之后的方案：\nSkills 方案：把经验沉淀为可复用的配置\n--- name: report-generator description: 按照公司标准格式自动收集数据并生成报告 tools: - mcp-jira-reader # 挂载所需的 MCP 工具 - mcp-database-query --- # 报告生成流程 1. 包含封面页（模板见 templates/cover.md） 2. 执行数据分析（自动调用 mcp-database-query 获取今日核心指标） 3. 提取任务进度（自动调用 mcp-jira-reader 获取任务状态） 4. 生成图表和摘要 ... 只要有了这个 Skill 文件，你下次只需要对 AI 说一句：「帮我生成今天的日报」。 AI 就会自动加载 report-generator 这个技能，自动按顺序调用 MCP 工具，按规定格式输出。一步到位，神清气爽！\n拆解：一个 Skill 的内部结构长什么样？ 从上面的对比图咱们能看出来，一个优秀的 Skill，结构是非常清晰的，通常分为两部分：\n头部配置（Frontmatter）： 通常用 YAML 语法写在最前面（被 --- 包裹）。这里最关键的两个字段是 name（技能叫啥）和 description（技能干啥用的）。这俩字段后面你会看到，是 Agent 判断「要不要激活这个技能」的唯一依据，写得好不好，直接决定技能能不能被正确触发。如果技能执行过程中需要特定的外部工具（比如某个 MCP Server），也可以在这里额外声明。\n技能主体（Body）： 这里写的是具体的指令、工作流（Workflow）、规则限制和模板。这里的大白话指令，就是在教大模型」拿到工具后，第一步干啥，第二步干啥」。\nSkills 最大的亮点：渐进式披露 了解完 Skill 的内部结构，你可能会产生一个新疑问：Agent 启动的时候，是不是要把所有 Skill 的内容一股脑全塞进去？\n咱们来算一笔账。假设你的 Agent 装了 50 个 Skill，每个 Skill 的完整指令大概 2000 个 token。如果全部塞进 System Prompt：\n50 个 Skill × 2000 token = 10 万 token 这会带来三个灾难：\n贵：大模型按 token 收费，每次对话还没开始干活，光加载技能就烧掉 10 万 token 的钱\n慢：上下文越长，模型处理速度越慢，响应延迟直线上升\n蠢：这是最致命的，用户可能只是想让 AI 帮忙压缩一张图片，结果模型脑子里同时塞着「写日报」「查数据库」「发邮件」等 49 个完全无关的技能指令。无关信息太多，模型的注意力被严重稀释，反而干不好眼前这件事\n结论：一股脑全塞进去，既浪费钱又影响质量。\n那怎么办？这就引出了 Skills 架构里最核心的设计思想，渐进式披露（Progressive Disclosure）。\n什么是渐进式披露？ 用一句话概括：Agent 启动时只看「菜单」，需要时才「点菜」。\n这个概念其实借鉴自用户界面设计领域的经典原则：只展示当前任务需要的信息，其余全部隐藏。你打开手机设置，首页只显示「Wi-Fi」「蓝牙」「通知」几个大类，不会一上来就把所有子选项全摊开，那就是渐进式披露。\n放到 Skills 里，意思就是：不要把整本百科全书一次性翻开，而是像查字典一样，先看目录，再翻到需要的那一页。\n三层加载架构 Skills 的渐进式披露把信息分成了三个层级，每一层只在被需要时才加载：\n第一层：元数据（Metadata），只看菜单\nAgent 启动时，只把每个 Skill 的 name（名称）和 description（描述）加载到 System Prompt 里。\n还记得咱们前面讲的 Skill 头部配置吗？就是那段 YAML：\n--- name: report-generator description: 按照公司标准格式自动收集数据并生成报告 --- 每个 Skill 的元数据大约只占 100 个 token。50 个 Skill 全部加载元数据 = 5000 token。\n打个比方，这就像去餐厅吃饭。菜单上只列了菜名和一句话简介：「宫保鸡丁，花生米配鸡丁，微辣」。你靠这些信息就能决定今天想吃什么，不需要把每道菜的详细菜谱全贴在墙上。\nAgent 看到这份「菜单」之后，就知道自己有哪些技能可以用。当用户提出需求时，Agent 扫一眼菜单，判断哪个 Skill 跟当前任务相关。\n第二层：完整指令（SKILL.md 正文），下单点菜\n当 Agent 判断某个 Skill 和当前任务相关时，才去读取这个 Skill 的完整 SKILL.md 内容，把详细的工作流指令加载到上下文里。\n继续餐厅的比方：你看完菜单决定点「宫保鸡丁」，服务员这才去后厨把这道菜的详细菜谱拿出来，放多少油、炒几分钟、什么时候放花生米，全部按步骤来。\n这一层建议控制在 5000 token 以内，保证加载后不会让上下文太臃肿。\n第三层：附属资源（脚本/模板/参考文档），按需取食材\nSKILL.md 里可能会引用一些额外的文件：Python 脚本、参考文档、模板文件等。这些附属资源只在 Agent 执行任务的过程中，真正需要用到的时候才去读取。\n还是餐厅的比方：厨师按菜谱做到第三步，发现需要松子，这才去仓库拿。不是一开始就把仓库里所有食材全搬到灶台上。\n这一层的好处是：附属资源可以很大（几千行的脚本、整份参考手册），因为它们不会在 Agent 启动时占用上下文，只在真正执行时按需访问。\n走一遍完整的加载过程 光说不练假把式，咱们用一个具体场景走一遍三层加载的完整流程。\n假设你的 Agent 装了 30 个 Skill，其中有一个叫 pdf-processor，专门处理 PDF 文件。现在用户问了一句：」帮我把这个 PDF 表单填了。」\n第一层启动： Agent 启动时，30 个 Skill 的名称和描述已经在 System Prompt 里了（大约 3000 token）。Agent 扫一圈菜单，看到 pdf-processor 的描述是」处理 PDF 文件，支持解析、填表、格式转换」，跟用户的需求对上了。\n第二层加载： Agent 决定启用 pdf-processor，于是读取 pdf-processor/SKILL.md 的完整内容，获得详细的工作流指令。\n第三层按需访问： SKILL.md 里写着」填写表单时，请参考 forms.md 获取表单字段规则」。Agent 在执行到填表这一步时，才去读取 forms.md。\n整个过程，Agent 只加载了 1 个 Skill 的完整内容，其余 29 个 Skill 自始至终只花了名称和描述的 token。\n效果有多夸张？用数字说话 yNIMac 全量加载 渐进式披露 启动时加载量 50 × 2000 = 10 万 token 50 × 100 = 5000 token 执行任务时追加 0（已经全部加载了） 1 × 2000 = 2000 token 总消耗 10 万 token 7000 token 节省比例 — 节省约 93% 省了 93% 的 token，而且因为上下文里只有跟当前任务相关的信息，模型的注意力更集中，响应质量反而更高。\n这就是为什么渐进式披露被认为是 Skills 架构最核心的创新，它让 Agent 可以拥有几十上百个技能，却不用为此付出巨额的上下文成本。技能装得越多，渐进式披露的优势就越明显。\n灵魂拷问：Skills 不就是高级一点的 Prompt 吗？ 可能看到这里，有同学心里犯嘀咕了：「我看你这 SKILL.md 里面，主体部分不还是写了一堆自然语言的步骤吗？这不就是个长一点的 Prompt（提示词）吗？跟我在对话框里敲字有什么区别？」\n这个问题问得太好了！其实很多人刚接触时都会有这个错觉。咱们来掰扯掰扯它俩到底是什么关系、有什么区别：\n**从关系上看：包含与被包含。 **Prompt 是 Skill 的「灵魂核心」，但不是全部。一个完整的 Skill = Prompt（指令与流程） + 挂载的工具列表（MCP） + 附属资源（脚本 / 模板 / 参考文档）。\n从能力上看：动嘴与动手。 Prompt 只能控制大模型的「嘴」。你写一万字的 Prompt 教它怎么查数据库，它也只能给你输出一段怎么查的文本。而 Skill 给 AI 赋予了「手」。通过配置文件里绑定的 tools，AI 在阅读 Prompt 的同时，是真的能去后台调用代码、拉取数据的。\n从工程上看：临时工与标准资产。 你在对话框里敲的 Prompt 是「临时工」，上下文一长它就忘了，下次还得重敲。而 Skill 是一份写在项目目录里的配置文件（通常是 YAML 或 Markdown），它是可以提交到 Git 仓库里的代码资产。它把个人经验变成了整个团队都可以直接复用的 标准作业程序（SOP）。\n打个粗俗点的比方：\nPrompt 是你站在厨房门口喊：「去给我炒个鱼香肉丝，先放葱姜蒜，再放肉丝！」\nSkill 是你把《鱼香肉丝菜谱》写好，连带厨房里自动炒菜机的【启动钥匙】，一起装进了一个信封里。以后谁饿了，拿着这个信封扫一下，菜就自动炒出来了。\n实战加餐：以最近爆火的 Claude Code 为例，看看 Skills 到底有多强？ 光说不练假把式。咱们就以它为例，看看真实项目里是怎么玩 Skills 的。\n假设你在开发一个前端网站，经常需要把设计稿里的大图片压缩、转换成 WebP 格式，然后再挪到 public 文件夹里。如果每次都让大模型现写一段 Python 脚本来跑，既费时间又容易出错。\n有了 Skills 之后，你可以直接在项目目录下建一个专门干这活的「图片优化技能」。\n目录结构长这样： your-web-project/ ├── src/ ├── .claude/ # Claude Code 的专属配置目录 │ └── skills/ # 💡 这里就是技能大本营 │ └── image-optimizer/ # 我们自定义的「图片优化技能」 │ ├── SKILL.md # 技能的说明书和工作流 │ └── optimize.py # 真正干活的 Python 压缩脚本 SKILL.md 里面写了啥？ 咱们刚才说了，一个优秀的 Skill 分为「头部配置」和「技能主体」。在 Claude Code 里，它是这么写的： --- name: image-optimizer description: 专门用于将大尺寸图片压缩并转换为 webp 格式，输出到 public/assets 目录。 tools: - mcp-python-runner # 声明需要用到运行 Python 代码的工具能力 --- # 任务流程 当你收到优化图片的指令时，请严格按照以下步骤执行： 1. 读取用户指定的原图片文件。 2. 调用同目录下的 `optimize.py` 脚本，将原图转换为高质量的 webp 格式。 3. 将转换后的图片移动到项目的 `public/assets/` 目录下。 4. 在终端打印出压缩前后的文件大小对比。 怎么使用？ 配置好之后，你在 Claude Code 的终端里，只需要像聊天一样发一句话（或者直接输入内置指令 /image-optimizer）： 用户： \u0026ldquo;把这张刚生成的测试图，用你的 image-optimizer 技能处理一下。\u0026rdquo;\nClaude 会自动扫描 skills/ 目录，读取 SKILL.md 理解整个工作流，然后默默在后台调用脚本、压缩图片、移动文件，最后在终端里回复你：「搞定了！图片大小从 631KB 压缩到了 56KB，已经放在 public 目录啦！」\n发现了吗？这种目录结构最大的好处是「热插拔」。有了 Skill，你实际上是在给 AI 「教流程」、「写 SOP」。\n以后再遇到类似的脏活累活，AI 就能像个熟练的老员工一样，直接照着这个目录里的 SOP 帮你把活干得漂漂亮亮！\n总结：理清它们的三角关系 最后，我再用一句话帮大家总结一下今天聊的这三个核心概念：\n📷 [图片 token=Mu4ZbhmcxoqufOxP2tZcPUsOnEl（未能下载，见飞书原文）]\nFunction Calling（函数调用）： 是底层技术，让大模型拥有了「伸手」调取外部工具的能力。\nMCP（模型上下文协议）： 是接口标准，统一了全网工具的接入规格，让 AI 拥有了「Type-C」通用插口。\nSkills（技能）： 是上层的业务封装，它将 Prompt 指令和 MCP 工具组合在一起，按清晰的目录结构管理起来，变成了 AI 可以随拿随用、彻底告别重复劳动的「标准工作流」。\n技术的发展永远是这样，从「能干活」（Function Calling），到「统一标准」（MCP），再到「工程化沉淀和管理」（Skills）。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%20Skills%EF%BC%9F/","summary":"我们从最基础的大模型、Prompt，一路学到了 Agent、Function Calling、MCP，前面这些技术在咱们之前的「智能OnCall项目」中都有用到。 不过，从 26 年初开始，AI 圈子又出了个叫  Skills（技能）  的","title":"什么是 Skills？"},{"content":"为什么要做这个名词扫盲？ 随着 AI 从聊天机器人进化到能干活的智能体，中间出现了很多新概念（比如system prompt、function calling、AI Agent）。这些术语就像拼图碎片，单独看可能模糊，但拼起来才能理解AI如何从被动对话进化为主动完成任务的完整逻辑。\nuser prompt（用户提示词） GPT发布初期，用户通过聊天框发送消息（user prompt）与大模型交互，但大模型缺乏人设，回复通用且仅能聊天，无法执行任务（比如上传PDF让它解析成中文再返回给你）。\n📷 [图片 token=U6bqbxpssouJDaxoIzJcuNDKnkd（未能下载，见飞书原文）]\nsystem prompt（系统提示词） 为给大模型加上人设，将人设信息从user prompt中单独拎出形成system prompt。用于描述大模型的角色、性格等非用户直接表达的内容。\n📷 [图片 token=Ie0IbtNsko7T9Sxot1Jc38VWnRK（未能下载，见飞书原文）]\n每次用户发送 user prompt，系统自动将 system prompt 一起发给AI模型，使对话更自然\n📷 [图片 token=KlkSbtYxhoZ120xVlusctQh9nmc（未能下载，见飞书原文）]\nAI Agent 与 Tool（智能体和工具） 即使我们给 AI 的提示词写得再详细，比如让它帮忙整理电脑里的文件，AI 只能回答问题或者给出建议，实际动手的还是得靠我们自己，那有没有办法让AI自己完成任务呢？这就需要在用户和 AI 中间引入一个智能体agent来帮忙了。\n智能体agent就像一个中间人(本质上就是我们写的程序)，它负责接收用户的指令，并协调 AI 和实际工具来干活。具体来说，我们先给智能体agent准备好一些基本工具，比如查找文件、读取文件、移动文件等工具。\n当用户发出指令，比如帮我读取C盘目录下的hello_world.cpp文件，移动到D盘目录下，最后总结文件内容：\n智能体agent会先把这个请求传给 AI ，并附带告诉 AI 它可以使用哪些工具，工具有哪些作用。\nAI 经过思考后，会告诉智能体agent：调用读取文件工具，路径是C://hello_world.cpp。\n智能体agent收到指示后，就实际操作工具读取文件，然后把读取的内容反馈给 AI。\nAI 根据结果决定下一步该做什么，比如可能还需要移动文件，会告诉智能体agent：调用移动文件工具，路径是C://hello_world.cpp 到 D://hello_world.cpp。\n智能体agent收到指示后，就实际操作工具移动文件，然后把移动结果反馈给 AI。\nAI 收到移动完成的进度后，返回总结内容给智能体。\n智能体收到 AI 传来的结果后，向用户报告结果。这样一步步推进，智能体agent全程协调，直到任务完成。\n📷 [图片 token=Chj2bUEA6ozNSExta22cxtXMngd（未能下载，见飞书原文）]\n简单来说，智能体agent 让 AI 不再是只动嘴的参谋，而是变成了能动手的实干家，整个过程更自动化、更智能。Agent与Tool定义：\nAgent：在AI、工具、用户间协调的程序\nTool：提供给 AI 调用的函数。\nfunction calling（函数调用） 在没有function calling技术之前，工具描述是放在system prompt中的。就是在AI Agent中我们提到，我们会将工具信息告诉AI(传统模式)。\n传统模式的 AI 有多笨？让 AI 查下上海明天天气，传统流程：\nAI 回复：需要调用天气工具，输入[明天，上海]\n而天气工具的实际参数：第一个参数是城市，第二个参数是日期\n问题：AI 每次都要猜怎么调用工具，还可能传错参数（首先参数顺序错了，其次 明天不是一个准确的日期）\nFunction Calling：把工具描述从 system prompt中剥离，用JSON格式统一定义函数名，函数介绍，参数字段，并规范AI调用工具的回复格式。这就是Function Calling的核心：用标准化格式让AI理解怎么调用工具，而不是猜。\n工具描述（Tool Definition）：\n{ \u0026#34;name\u0026#34;: \u0026#34;check_weather\u0026#34;, // 工具唯一名称（AI 调用时会用这个名字） \u0026#34;description\u0026#34;: \u0026#34;获取指定城市的当天天气情况，包括温度、天气状况（晴/雨/多云等）和风力\u0026#34;, // 工具功能说明（AI 靠这个判断是否需要调用） \u0026#34;parameters\u0026#34;: { // 调用工具必须传递的参数（类似必填项） \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: { // 参数1：城市名 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;需要查询天气的城市名称，例如：北京、上海、广州\u0026#34; // 参数说明（AI 会根据这个问用户要信息） }, \u0026#34;date\u0026#34;: { // 参数2：日期（可选参数，默认当天） \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;YYYY-MM-DD\u0026#34;, // 参数格式约束 \u0026#34;description\u0026#34;: \u0026#34;查询的日期，格式为年-月-日，例如：2025-11-13，默认查询当天\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;] // 必须传递的参数（这里 city 是必填，date 可选） } } AI 调用格式（Function Call）\n{ \u0026#34;function_call\u0026#34;: { // 固定字段，表示这是一个工具调用请求 \u0026#34;name\u0026#34;: \u0026#34;check_weather\u0026#34;, // 工具名（必须和上面定义的 name 一致） \u0026#34;parameters\u0026#34;: { // 参数值（严格对应工具定义的 parameters） \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;, // 必填参数：城市名 \u0026#34;date\u0026#34;: \u0026#34;2025-11-14\u0026#34; // 可选参数：日期（用户指定了，所以带上） } } } 工具返回结果（Tool Response）\n{ \u0026#34;temperature\u0026#34;: 18, // 温度（单位：℃） \u0026#34;condition\u0026#34;: \u0026#34;多云转晴\u0026#34;, // 天气状况 \u0026#34;wind\u0026#34;: \u0026#34;3级西北风\u0026#34;, // 风力 \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;, \u0026#34;date\u0026#34;: \u0026#34;2025-11-14\u0026#34; } Function Calling的好处：\n告别猜谜语：以前靠System Prompt用自然语言描述工具，AI可能听不懂；现在用JSON格式，AI一看就会。\n降低开发难度：开发者不用自己写代码检测AI回复是否正确，若AI回复错误，AI的服务器端可检测并自动重试，降低用户端开发难度和token开销。\n跨场景通用：无论是ChatGPT还是开源模型，只要支持Function Calling，就能用同一套工具。\n对比项 System Prompt（传统方式） Function Calling（标准化方式） 工具描述 自然语言随意写（如你可以用查天气工具） JSON格式强制规范（必须包含name/parameters） 调用格式 靠AI猜（可能返回散文式回复） 固定JSON结构（如{\u0026ldquo;function_name\u0026rdquo;: \u0026ldquo;\u0026hellip;\u0026quot;}） 错误处理 开发者自己写代码重试 大模型服务器自动重试 拓展阅读：deepseek的function calling定义 https://api-docs.deepseek.com/zh-cn/guides/function_calling\nMCP（Model Context Protocol） 上文提到的Agent和Tool是怎么进行交互的？最简单的做法就是把Agent和Tool写在同一个程序里面，直接通过函数调用来完成，这也是现在大多数agent的做法。\n但其实有些tool的功能其实挺通用的，可能多个agent都需要，但总不能在每个agent里面都拷贝一份相同的代码吧。\n我们把tool变成服务，统一的托管，让所有的agent都来调用，这就是mcp server。mcp是一个通信协议，专门用来规范agent和tool服务之间是怎么交互的。运行tool的服务叫做mcp server，调用它的agent叫做mcp client。mcp规定了mcp server如何和mcp client通信，以及mcp server有哪些接口。\nmcp server既可以和agent跑在同一台机器上，通过标准输入输出进行通信。也可以被部署在网络上，通过http进行通信。虽然mcp是为了ai而定制出来的标准，但实际上mcp本身却和ai模型没有关系，他并不关心agent用的是哪个模型，mcp只负责帮agent托管工具、资源。\n你可以把 MCP 想象成电脑的 USB-C 接口\n各种外设（如键盘、U盘、显示器）就是不同的 MCP Server，它们提供各自独特的功能。\n电脑就是 AI Agent，它作为 MCP Client，通过统一的 USB-C 接口（即 MCP 协议）来连接和使用所有外设(MCP Server)。\n这样一来，无论你更换电脑还是外设，只要都支持 USB-C 标准，就能即插即用，非常方便。MCP 协议正是为 AI 世界带来了这种即插即用的便利性。\n完整协作流程：\n用户问Agent：女朋友肚子疼怎么办？\nAgent通过MCP协议从MCP Server获取工具信息（如网页浏览工具）\nAgent将工具信息转为Function Calling格式，与User Prompt一起发给AI模型\nAI模型选择调用\u0026quot;web browser\u0026quot;工具搜索答案\nAgent通过MCP调用网页浏览服务，获取结果后返回模型\n模型生成最终建议：多喝热水\n📷 [图片 token=B5aqb68u0oXawXxKFXycLyDcn4c（未能下载，见飞书原文）]\n大模型的上下文窗口 大模型的上下文窗口（Context Window）就是模型在每一次对话中，能够记住和处理的信息总量的上限。\n把上下文窗口想象一块小黑板。\n黑板的作用： 你跟大模型说的每一句话，它都会立刻抄在黑板上，这样它才能接得上你的话。\n黑板的大小：\n如果黑板很大，你们聊了很久、甚至讲完一个长故事，它都能看见黑板上的字，记得清清楚楚。 如果黑板很小，写几句就写满了。 写满了怎么办？ 为了写下你现在说的话，大模型只能把黑板最上面（最早） 的字擦掉。 简单说： 上下文窗口就是这块黑板的大小。黑板越大，大模型的记性就越好；黑板太小，它就变成金鱼记忆，聊着聊着就忘了刚开始说了啥。\nRAG（检索、增强、生成） RAG简单说就是先从资料库找答案，再让AI基于找到的内容生成回答 的这个过程。RAG是目前最火的AI问答方案，很多企业知识助手、智能客服背后使用的技术都是RAG。\n为什么直接喂文档给大模型不行？\n举个例子：如果你的产品手册有几百上千页，直接发给AI模型会出大问题！首先模型有上下文窗口限制，可能读了后面忘前面；其次输入越多推理成本越高，钱包会哭；最后输入量大了回答速度也会变慢，用户等不及。\nRAG如何解决这些痛点？\nRAG的聪明之处在于：只把和问题相关的片段发给模型！比如用户问产品保修政策，RAG会从几百页手册里精准揪出3个相关段落发给模型，大大提升效率和准确性。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E5%BF%85%E7%9C%8B%EF%BC%9AAI%E5%90%8D%E8%AF%8D%E6%89%AB%E7%9B%B2/","summary":"为什么要做这个名词扫盲？ 随着 AI 从聊天机器人进化到能干活的智能体，中间出现了很多新概念（比如system prompt、function calling、AI Agent）。这些术语就像拼图碎片，单独看可能模糊，但拼起来才能理解AI如","title":"必看：AI名词扫盲"},{"content":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：[小试牛刀：使用goframe框架3分钟实现一个http接口](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/Go 语言入门小实战/使用goframe框架3分钟实现一个http接口（Go）/)\nAI运维接口 AI运维接口，调用后会自动查询现在活跃的告警，并判断根因\n请求方法: POST /api/ai_ops\n请求字段:\n字段名 类型 描述 响应字段:\n字段名 类型 描述 Result string 结果 Detail []string 详细信息列表 示例：\ncurl -X POST http://localhost:6872/api/ai_ops \\ -H \u0026#34;Content-Type: application/json\u0026#34; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;result\u0026#34;: \u0026#34;汇总的分析结果...\u0026#34;, \u0026#34;detail\u0026#34;: [ \u0026#34;执行步骤1...\u0026#34;, \u0026#34;执行步骤2...\u0026#34;, \u0026#34;...\u0026#34; ] } } AI运维接口核心实现(Go) 代码路径：SuperBizAgent/internal/controller/chat/chat_v1_ai_ops.go\n因为我们这个Agent比较特殊，有Replan功能，所以我们可以直接让Agent主动查询活跃的告警，如果有告警则让它查询内部文档，自己去规划执行步骤。\n所以重点就在我们的prompt设计上，我们需要稍微的指导一下大模型该怎么制定计划。\n至于如果有告警该怎么执行，那就看你上传的告警处理手册中写的步骤，写的怎么处理，就会怎么执行。\nfunc (c *ControllerV1) AIOps(ctx context.Context, req *v1.AIOpsReq) (res *v1.AIOpsRes, err error) { query := ` \u0026#34;1. 你是一个智能的服务告警运维分析助手,首先调用工具query_prometheus_alerts获取所有活跃的告警。\u0026#34; \u0026#34;2. 分别根据告警的名称调用工具query_internal_docs，获取告警名对应的处理方案。\u0026#34; \u0026#34;3. 完全遵循内部文档的内容进行查询和分析,不允许使用文档外的任何信息。\u0026#34; \u0026#34;4. 涉及到时间的参数都需要先通过工具get_current_time获取当前时间,再结合用户的时间要求进行传参。\u0026#34; \u0026#34;5. 涉及到日志的查询,需要先通过日志工具获取相关日志信息，参数必须携带地域和日志主题。\u0026#34; \u0026#34;6. 分别将告警对应查询到的信息进行总结分析,最后汇总所有告警和总结。\u0026#34;` resp, detail, err := plan_execute_replan.BuildPlanAgent(ctx, query) if err != nil { return nil, err } if resp == \u0026#34;\u0026#34; { return nil, errors.New(\u0026#34;内部错误\u0026#34;) } res = \u0026amp;v1.AIOpsRes{ Result: resp, Detail: detail, } return res, nil } AI运维接口核心实现(Java) 前面我们讲解了SupervisorAgent的作用以及他们的prompt\n所以在API接口使用层面，build出来后直接调用\n然后把输出返回出去即可\n/** * AI 智能运维接口（SSE 流式模式）- 自动分析告警并生成运维报告 * 无需用户输入，自动执行告警分析流程 */ @PostMapping(value = \u0026#34;/ai_ops\u0026#34;, produces = \u0026#34;text/event-stream;charset=UTF-8\u0026#34;) public SseEmitter aiOps() { SseEmitter emitter = new SseEmitter(600000L); // 10分钟超时（告警分析可能较慢） executor.execute(() -\u0026gt; { try { // 调用 AiOpsService 执行分析流程 Optional\u0026lt;OverAllState\u0026gt; overAllStateOptional = aiOpsService.executeAiOpsAnalysis(chatModel, toolCallbacks); OverAllState state = overAllStateOptional.get(); logger.info(\u0026#34;AI Ops 编排完成，开始提取最终报告...\u0026#34;); // 提取最终报告 Optional\u0026lt;String\u0026gt; finalReportOptional = aiOpsService.extractFinalReport(state); // 输出最终报告 if (finalReportOptional.isPresent()) { // 发送 } } }); return emitter; } public Optional\u0026lt;OverAllState\u0026gt; executeAiOpsAnalysis(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) throws GraphRunnerException { logger.info(\u0026#34;开始执行 AI Ops 多 Agent 协作流程\u0026#34;); // 构建 Planner 和 Executor Agent ReactAgent plannerAgent = buildPlannerAgent(chatModel, toolCallbacks); ReactAgent executorAgent = buildExecutorAgent(chatModel, toolCallbacks); // 构建 Supervisor Agent SupervisorAgent supervisorAgent = SupervisorAgent.builder() .name(\u0026#34;ai_ops_supervisor\u0026#34;) .description(\u0026#34;负责调度 Planner 与 Executor 的多 Agent 控制器\u0026#34;) .model(chatModel) .systemPrompt(buildSupervisorSystemPrompt()) .subAgents(List.of(plannerAgent, executorAgent)) .build(); String taskPrompt = \u0026#34;你是企业级 SRE，接到了自动化告警排查任务。请结合工具调用，执行**规划→执行→再规划**的闭环，并最终按照固定模板输出《告警分析报告》。禁止编造虚假数据，如连续多次查询失败需诚实反馈无法完成的原因。\u0026#34;; return supervisorAgent.invoke(taskPrompt); } AI运维接口核心实现(Python) 代码路径：app/api/aiops.py 和 app/services/aiops_service.py\n接口无需用户传入具体问题，Agent 自己主动查询活跃告警，prompt 中已嵌入了完整的诊断任务描述\n接口调用 aiops_service.diagnose，内部使用固定的 AIOps 任务描述启动 Plan-Execute-Replan 工作流\n工作流通过 graph.astream 流式执行，每个节点完成后立即通过 SSE 推送事件给前端\n接收到 complete 或 error 事件后，关闭 SSE 流\n@router.post(\u0026#34;/aiops\u0026#34;) async def diagnose_stream(request: AIOpsRequest): session_id = request.session_id or \u0026#34;default\u0026#34; logger.info(f\u0026#34;[会话 {session_id}] 收到 AIOps 诊断请求（流式）\u0026#34;) async def event_generator(): try: async for event in aiops_service.diagnose(session_id=session_id): # 将每个节点产生的事件序列化后推送给前端 yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps(event, ensure_ascii=False) } # complete 或 error 时结束流 if event.get(\u0026#34;type\u0026#34;) in [\u0026#34;complete\u0026#34;, \u0026#34;error\u0026#34;]: break except Exception as e: yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({ \u0026#34;type\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;stage\u0026#34;: \u0026#34;exception\u0026#34;, \u0026#34;message\u0026#34;: f\u0026#34;诊断异常: {str(e)}\u0026#34; }, ensure_ascii=False) } return EventSourceResponse(event_generator()) `diagnose` 方法内部构造固定的 AIOps 任务描述，将其传入通用的 `execute` 方法启动工作流。任务描述中已明确告知 Agent 该做什么，无需用户填写——这就是这个接口不需要请求参数的原因：\nasync def diagnose(self, session_id: str) -\u0026gt; AsyncGenerator: aiops_task = \u0026#34;\u0026#34;\u0026#34;诊断当前系统是否存在告警，如果存在告警请详细分析告警原因并生成诊断报告，诊断报告输出格式要求： # 告警分析报告 ## 📋 活跃告警清单 | 告警名称 | 级别 | 目标服务 | 首次触发时间 | 最新触发时间 | 状态 | ... ## 🔍 告警根因分析N - [告警名称] ... ## 🛠️ 处理方案执行N - [告警名称] ... ## 📊 结论 ... 重要提醒：所有内容必须基于工具查询的真实数据，严禁编造\u0026#34;\u0026#34;\u0026#34; async for event in self.execute(aiops_task, session_id): # 将 complete 事件转换为包含 diagnosis 字段的格式 if event.get(\u0026#34;type\u0026#34;) == \u0026#34;complete\u0026#34;: yield { \u0026#34;type\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;stage\u0026#34;: \u0026#34;diagnosis_complete\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;诊断流程完成\u0026#34;, \u0026#34;diagnosis\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;completed\u0026#34;, \u0026#34;report\u0026#34;: event.get(\u0026#34;response\u0026#34;, \u0026#34;\u0026#34;) } } else: yield event `execute` 方法以固定的初始状态启动 LangGraph 工作流，通过 `stream_mode=\u0026ldquo;updates\u0026rdquo;` 实时推送各节点的执行结果：\nasync def execute(self, user_input: str, session_id: str) -\u0026gt; AsyncGenerator: initial_state: PlanExecuteState = { \u0026#34;input\u0026#34;: user_input, \u0026#34;plan\u0026#34;: [], \u0026#34;past_steps\u0026#34;: [], \u0026#34;response\u0026#34;: \u0026#34;\u0026#34; } async for event in self.graph.astream( input=initial_state, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, stream_mode=\u0026#34;updates\u0026#34; ): for node_name, node_output in event.items(): if node_name == \u0026#34;planner\u0026#34;: yield self._format_planner_event(node_output) # type=plan elif node_name == \u0026#34;executor\u0026#34;: yield self._format_executor_event(node_output) # type=step_complete elif node_name == \u0026#34;replanner\u0026#34;: yield self._format_replanner_event(node_output) # type=report/status # 流程结束，推送最终报告 final_state = self.graph.get_state({\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}) final_response = final_state.values.get(\u0026#34;response\u0026#34;, \u0026#34;\u0026#34;) if final_state else \u0026#34;\u0026#34; yield {\u0026#34;type\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;stage\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;response\u0026#34;: final_response} ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9AAPI%E6%8E%A5%E5%8F%A3%E4%B8%8EAgent%E7%9A%84%E6%95%B4%E5%90%88/","summary":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：\u0026lt;mention-doc token=\u0026ldquo;FMMRwPNVZiRiqSkTTF8cQhOanyb\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 小试牛刀：使用goframe框架3分","title":"源码分析：API接口与Agent的整合"},{"content":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：[小试牛刀：使用goframe框架3分钟实现一个http接口](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/Go 语言入门小实战/使用goframe框架3分钟实现一个http接口（Go）/)\n对话接口的定义 快速对话接口 与大模型对话，相同Id的对话带有上下文记忆功能\n请求方法: POST /api/chat\n请求字段:\n字段名 类型 描述 Id string 对话的唯一标识 Question string 用户提问 响应字段:\n字段名 类型 描述 Answer string 系统回答 示例：\n# 示例：快速对话 curl -X POST http://localhost:6872/api/chat \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;Id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;Question\u0026#34;: \u0026#34;什么是人工智能？\u0026#34; }\u0026#39; # 响应 { \u0026#34;message\u0026#34;: \u0026#34;OK\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;answer\u0026#34;: \u0026#34;AI 的回答内容...\u0026#34; } } 流式对话接口 与大模型对话，相同Id的对话带有上下文记忆功能，通过SSE实现流式输出回答\n请求方法: POST /api/chat_stream\n请求字段:\n字段名 类型 描述 Id string 对话的唯一标识 Question string 用户提问 响应字段:\n字段名 类型 描述 SSE响应格式：\nevent类型 含义 connected 代表连接建立成功 message 回复的文本片段，会多次发送 error 连接异常，断开连接 done 消息推送完毕，断开连接 示例：\n# 示例：流式对话 curl -X POST http://localhost:6872/api/chat_stream \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;Id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;Question\u0026#34;: \u0026#34;什么是人工智能？\u0026#34; }\u0026#39; # 响应 id: \u0026lt;timestamp\u0026gt; event: connected data: {\u0026#34;status\u0026#34;: \u0026#34;connected\u0026#34;, \u0026#34;client_id\u0026#34;: \u0026#34;session-001\u0026#34;} id: \u0026lt;timestamp\u0026gt; event: message data: 人工智能（AI） id: \u0026lt;timestamp\u0026gt; event: message data: 的发展历史 id: \u0026lt;timestamp\u0026gt; event: message data: 可以追溯到... id: \u0026lt;timestamp\u0026gt; event: done data: Stream completed 快速对话接口的核心实现(Go) 代码路径：SuperBizAgent/internal/controller/chat/chat_v1_chat.go\n根据用户id查询历史对话，并构造用户消息结构体\n创建对话Agent的执行器\n执行对话Agent，获得大模型的返回消息\n将本轮对话的问题和答案存入记忆系统中\n构造结构体，返回消息\nfunc (c *ControllerV1) Chat(ctx context.Context, req *v1.ChatReq) (res *v1.ChatRes, err error) { id := req.Id msg := req.Question // 1. 构造结构体 userMessage := \u0026amp;chat_pipeline.UserMessage{ ID: id, Query: msg, History: mem.GetSimpleMemory(id).GetMessages(), } // 2. 创建对话Agent的执行器 runner, err := chat_pipeline.BuildChatAgent(ctx) if err != nil { return nil, err } // 3. 执行 out, err := runner.Invoke(ctx, userMessage, compose.WithCallbacks(log_call_back.LogCallback(nil))) if err != nil { return nil, err } // 4. 将本轮对话存入系统 mem.GetSimpleMemory(id).SetMessages(schema.UserMessage(msg)) mem.GetSimpleMemory(id).SetMessages(schema.SystemMessage(out.Content)) // 5. 返回消息 res = \u0026amp;v1.ChatRes{ Answer: out.Content, } return res, nil } func (c *SimpleMemory) SetMessages(msg *schema.Message) { c.mu.Lock() defer c.mu.Unlock() c.Messages = append(c.Messages, msg) // TODO 这里可以考虑对前面的对话进行总结，压缩 if len(c.Messages) \u0026gt; c.MaxWindowSize { // 只保留最近的，把前面的丢掉 c.Messages = c.Messages[len(c.Messages)-c.MaxWindowSize-1:] } } func (c *SimpleMemory) GetMessages() []*schema.Message { c.mu.Lock() defer c.mu.Unlock() return c.Messages } 流式对话接口的核心实现(Go) sse返回的消息event类型：\nevent类型 含义 connected 代表连接建立成功 message 回复的文本片段，会多次发送 error 连接异常，断开连接 done 消息推送完毕，断开连接 流式对话的核心是sse，首先我们创建sse客户端\n然后agent我们使用流式输出模式\n最后每次我们从流中读到内容，就通过see发送给用户\nfunc (c *ControllerV1) ChatStream(ctx context.Context, req *v1.ChatStreamReq) (res *v1.ChatStreamRes, err error) { id := req.Id msg := req.Question ctx = context.WithValue(ctx, \u0026#34;client_id\u0026#34;, req.Id) // 1. 创建流式对话客户端 client, err := c.service.Create(ctx, g.RequestFromCtx(ctx)) if err != nil { return nil, err } userMessage := \u0026amp;chat_pipeline.UserMessage{ ID: id, Query: msg, History: mem.GetSimpleMemory(id).GetMessages(), } runner, err := chat_pipeline.BuildChatAgent(ctx) // 2. 使用stream流式输出模式 sr, err := runner.Stream(ctx, userMessage, compose.WithCallbacks(log_call_back.LogCallback(nil))) if err != nil { client.SendToClient(\u0026#34;error\u0026#34;, err.Error()) return nil, err } defer sr.Close() for { // 从流中读取消息 chunk, err := sr.Recv() if errors.Is(err, io.EOF) { client.SendToClient(\u0026#34;done\u0026#34;, \u0026#34;Stream completed\u0026#34;) return \u0026amp;v1.ChatStreamRes{}, nil } if err != nil { client.SendToClient(\u0026#34;error\u0026#34;, err.Error()) return \u0026amp;v1.ChatStreamRes{}, nil } // 发送消息 client.SendToClient(\u0026#34;message\u0026#34;, chunk.Content) } } SSE客户端创建也很简单，就是按照SSE协议的要求，修改HTTP头部字段即可\n// Create 创建SSE连接 func (s *Service) Create(ctx context.Context, r *ghttp.Request) (*Client, error) { // 设置SSE必要的HTTP头 r.Response.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;text/event-stream\u0026#34;) r.Response.Header().Set(\u0026#34;Cache-Control\u0026#34;, \u0026#34;no-cache\u0026#34;) r.Response.Header().Set(\u0026#34;Connection\u0026#34;, \u0026#34;keep-alive\u0026#34;) r.Response.Header().Set(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;*\u0026#34;) // 创建新客户端 clientId := r.Get(\u0026#34;client_id\u0026#34;, guid.S()).String() client := \u0026amp;Client{ Id: clientId, Request: r, messageChan: make(chan string, 100), } // 发送连接成功消息 r.Response.Writefln(\u0026#34;id: %s\u0026#34;, clientId) r.Response.Writefln(\u0026#34;event: connected\u0026#34;) r.Response.Writefln(\u0026#34;data: {\\\u0026#34;status\\\u0026#34;: \\\u0026#34;connected\\\u0026#34;, \\\u0026#34;client_id\\\u0026#34;: \\\u0026#34;%s\\\u0026#34;}\\n\u0026#34;, clientId) r.Response.Flush() return client, nil } 快速对话接口的核心实现(Java) 根据用户id查询历史对话\n构造prompt\n创建ReActAgent并执行\n将本轮的问答存入历史对话中\n/** * 普通对话接口（支持工具调用） */ @PostMapping(\u0026#34;/chat\u0026#34;) public ResponseEntity\u0026lt;ApiResponse\u0026lt;ChatResponse\u0026gt;\u0026gt; chat(@RequestBody ChatRequest request) { try { // 1.1获取或创建会话 SessionInfo session = getOrCreateSession(request.getId()); // 1.2获取历史消息 List\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; history = session.getHistory(); logger.info(\u0026#34;会话历史消息对数: {}\u0026#34;, history.size() / 2); // 创建 DashScope API 和 ChatModel DashScopeApi dashScopeApi = chatService.createDashScopeApi(); DashScopeChatModel chatModel = chatService.createStandardChatModel(dashScopeApi); // 记录可用工具 chatService.logAvailableTools(); // 2. 构建系统提示词（包含历史消息） String systemPrompt = chatService.buildSystemPrompt(history); // 3. 创建 ReactAgent ReactAgent agent = chatService.createReactAgent(chatModel, systemPrompt); // 执行对话 String fullAnswer = chatService.executeChat(agent, request.getQuestion()); // 4. 更新会话历史 session.addMessage(request.getQuestion(), fullAnswer); logger.info(\u0026#34;已更新会话历史 - SessionId: {}, 当前消息对数: {}\u0026#34;, request.getId(), session.getMessagePairCount()); return ResponseEntity.ok(ApiResponse.success(ChatResponse.success(fullAnswer))); } } 流式对话接口的核心实现(Java) sse返回的消息event类型：\nevent类型 含义 connected 代表连接建立成功 message 回复的文本片段，会多次发送 error 连接异常，断开连接 done 消息推送完毕，断开连接 流式对话的核心是SSE，首先我们创建sse客户端\n然后agent我们使用流式输出模式\n最后每次从流中读到内容，就通过sse发送给用户\n/** * ReactAgent 对话接口（SSE 流式模式，支持多轮对话，支持自动工具调用，例如获取当前时间，查询日志，告警等） * 支持 session 管理，保留对话历史 */ @PostMapping(value = \u0026#34;/chat_stream\u0026#34;, produces = \u0026#34;text/event-stream;charset=UTF-8\u0026#34;) public SseEmitter chatStream(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(300000L); // 5分钟超时 executor.execute(() -\u0026gt; { try { // 获取或创建会话 // 获取历史消息 // 创建 DashScope API 和 ChatModel // 记录可用工具 // 构建系统提示词（包含历史消息） // 创建 ReactAgent ReactAgent agent = chatService.createReactAgent(chatModel, systemPrompt); // 用于累积完整答案 StringBuilder fullAnswerBuilder = new StringBuilder(); // 使用 agent.stream() 进行流式对话 Flux\u0026lt;NodeOutput\u0026gt; stream = agent.stream(request.getQuestion()); stream.subscribe( output -\u0026gt; { try { // 检查是否为 StreamingOutput 类型 if (output instanceof StreamingOutput streamingOutput) { OutputType type = streamingOutput.getOutputType(); // 处理模型推理的流式输出 if (type == OutputType.AGENT_MODEL_STREAMING) { // 流式增量内容，逐步显示 String chunk = streamingOutput.message().getText(); if (chunk != null \u0026amp;\u0026amp; !chunk.isEmpty()) { fullAnswerBuilder.append(chunk); // 实时发送到前端 emitter.send(SseEmitter.event() .name(\u0026#34;message\u0026#34;) .data(SseMessage.content(chunk), MediaType.APPLICATION_JSON)); logger.info(\u0026#34;发送流式内容: {}\u0026#34;, chunk); } } } } }, error -\u0026gt; { // 错误处理 logger.error(\u0026#34;ReactAgent 流式对话失败\u0026#34;, error); try { emitter.send(SseEmitter.event() .name(\u0026#34;message\u0026#34;) .data(SseMessage.error(error.getMessage()), MediaType.APPLICATION_JSON)); } catch (IOException ex) { logger.error(\u0026#34;发送错误消息失败\u0026#34;, ex); } emitter.completeWithError(error); }, () -\u0026gt; { // 完成处理 try { String fullAnswer = fullAnswerBuilder.toString(); logger.info(\u0026#34;ReactAgent 流式对话完成 - SessionId: {}, 答案长度: {}\u0026#34;, request.getId(), fullAnswer.length()); // 更新会话历史 session.addMessage(request.getQuestion(), fullAnswer); logger.info(\u0026#34;已更新会话历史 - SessionId: {}, 当前消息对数: {}\u0026#34;, request.getId(), session.getMessagePairCount()); // 发送完成标记 emitter.send(SseEmitter.event() .name(\u0026#34;message\u0026#34;) .data(SseMessage.done(), MediaType.APPLICATION_JSON)); emitter.complete(); } catch (IOException e) { logger.error(\u0026#34;发送完成消息失败\u0026#34;, e); emitter.completeWithError(e); } } ); } }); return emitter; } 快速对话接口的核心实现(Python) 代码路径：app/api/chat.py 和 app/services/rag_agent_service.py\n接收请求，取出 id（session_id）和 question\n调用 rag_agent_service.query 执行 Agent 推理，thread_id 即 session_id\nLangGraph MemorySaver 自动完成历史消息的读取与写入，无需手动管理\n返回答案\n@router.post(\u0026#34;/chat\u0026#34;) async def chat(request: ChatRequest): logger.info(f\u0026#34;[会话 {request.id}] 收到快速对话请求: {request.question}\u0026#34;) # 直接调用 Agent，thread_id 决定会话隔离，历史消息由 MemorySaver 自动维护 answer = await rag_agent_service.query( request.question, session_id=request.id ) return { \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;success\u0026#34;: True, \u0026#34;answer\u0026#34;: answer, \u0026#34;errorMessage\u0026#34;: None } } query 方法内部将系统提示 + 用户问题包装成消息列表，通过 agent.ainvoke 执行完整的 ReAct 推理链，并从最后一条消息中取出答案。thread_id 与 MemorySaver 配合，让相同 id 的请求自动携带历史上下文：\nasync def query(self, question: str, session_id: str) -\u0026gt; str: await self._initialize_agent() messages = [ SystemMessage(content=self.system_prompt), HumanMessage(content=question) ] # thread_id 相同则自动读取 MemorySaver 中的历史消息 result = await self.agent.ainvoke( input={\u0026#34;messages\u0026#34;: messages}, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, ) # 取最后一条消息作为最终答案 last_message = result[\u0026#34;messages\u0026#34;][-1] return last_message.content 会话历史的消息裁剪由 trim_messages_middleware 节点负责，策略是保留第一条系统消息 + 最近 6 条消息（约 3 轮对话），防止多轮对话超出大模型的上下文窗口：\ndef trim_messages_middleware(state: AgentState): messages = state[\u0026#34;messages\u0026#34;] if len(messages) \u0026lt;= 7: return None # 消息较少，无需裁剪 first_msg = messages[0] # 保留系统消息 recent_messages = messages[-6:] if len(messages) % 2 == 0 else messages[-7:] return { \u0026#34;messages\u0026#34;: [ RemoveMessage(id=REMOVE_ALL_MESSAGES), # 清空所有旧消息 *([first_msg] + list(recent_messages)) # 写入保留的消息 ] } 流式对话接口的核心实现(Python) SSE 返回的消息 event 类型：\nevent 类型 含义 message (type=content) 回复的文本片段，会多次发送 message (type=tool_call) 工具调用状态通知 message (type=done) 消息推送完毕 message (type=error) 发生异常 流式对话的核心是 SSE，FastAPI 通过 EventSourceResponse 实现，无需手动设置 HTTP 头\nAgent 使用 agent.astream 的 stream_mode=\u0026quot;messages\u0026quot; 模式，逐 token 产生输出\n每次从流中读到文本内容，就通过 SSE 发送给客户端\n@router.post(\u0026#34;/chat_stream\u0026#34;) async def chat_stream(request: ChatRequest): async def event_generator(): async for chunk in rag_agent_service.query_stream( request.question, session_id=request.id ): chunk_type = chunk.get(\u0026#34;type\u0026#34;) if chunk_type == \u0026#34;content\u0026#34;: # 逐 token 文本片段，实时推送 yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps( {\u0026#34;type\u0026#34;: \u0026#34;content\u0026#34;, \u0026#34;data\u0026#34;: chunk[\u0026#34;data\u0026#34;]}, ensure_ascii=False ) } elif chunk_type == \u0026#34;tool_call\u0026#34;: # 工具调用状态（前端可展示\u0026#34;正在检索知识库...\u0026#34;等提示） yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps( {\u0026#34;type\u0026#34;: \u0026#34;tool_call\u0026#34;, \u0026#34;data\u0026#34;: chunk.get(\u0026#34;data\u0026#34;)}, ensure_ascii=False ) } elif chunk_type == \u0026#34;complete\u0026#34;: # 推送完成信号 yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({\u0026#34;type\u0026#34;: \u0026#34;done\u0026#34;, \u0026#34;data\u0026#34;: None}, ensure_ascii=False) } elif chunk_type == \u0026#34;error\u0026#34;: yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps( {\u0026#34;type\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;data\u0026#34;: str(chunk.get(\u0026#34;data\u0026#34;))}, ensure_ascii=False ) } # EventSourceResponse 自动处理 SSE 协议头和连接管理 return EventSourceResponse(event_generator()) query_stream 方法使用 agent.astream 的 stream_mode=\u0026quot;messages\u0026quot; 模式，每个 token 触发一次回调，从 content_blocks 中提取文本块后 yield 给上层：\nasync def query_stream(self, question: str, session_id: str) -\u0026gt; AsyncGenerator: await self._initialize_agent() messages = [ SystemMessage(content=self.system_prompt), HumanMessage(content=question) ] async for token, metadata in self.agent.astream( input={\u0026#34;messages\u0026#34;: messages}, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, stream_mode=\u0026#34;messages\u0026#34;, # 逐 token 输出模式 ): if type(token).__name__ in (\u0026#34;AIMessage\u0026#34;, \u0026#34;AIMessageChunk\u0026#34;): content_blocks = getattr(token, \u0026#39;content_blocks\u0026#39;, None) if content_blocks: for block in content_blocks: if isinstance(block, dict) and block.get(\u0026#39;type\u0026#39;) == \u0026#39;text\u0026#39;: text = block.get(\u0026#39;text\u0026#39;, \u0026#39;\u0026#39;) if text: yield {\u0026#34;type\u0026#34;: \u0026#34;content\u0026#34;, \u0026#34;data\u0026#34;: text} yield {\u0026#34;type\u0026#34;: \u0026#34;complete\u0026#34;} TODO挑战 流式对话代码里预留的两个Todo给有能力的同学实现。只要你把下面两个Todo实现了，说明这个项目你已经完成搞明白了：\nGo语言代码熟悉：流式对话接口里面没有增加记忆功能，可以参考对话接口的记忆功能来实现，代码是可复用的。\n总结Agent实战：现在的记忆设计是放到先进先出的队列里面。其实我们可以考虑对前面的对话进行总结，压缩(避免多轮对话导致超过大模型的上下文窗口)。你可以尝试实现一个“总结对话Agent”，输入就是历史对话，输出就是大模型的总结内容，那么在对话超过5轮的时候，就调用总结对话Agent，把前5轮的历史对话替换成总结内容。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9AAPI%E6%8E%A5%E5%8F%A3%E4%B8%8EAgent%E7%9A%84%E6%95%B4%E5%90%88/","summary":"项目使用goframe作为web框架，如果想了解API定义到提供服务的流程，先看：\u0026lt;mention-doc token=\u0026ldquo;FMMRwPNVZiRiqSkTTF8cQhOanyb\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 小试牛刀：使用goframe框架3分","title":"源码分析：API接口与Agent的整合"},{"content":" 📷 [图片 token=CGsjby7n5oDwEDx7iQBcVvXsnub（未能下载，见飞书原文）]\n📷 [图片 token=CXOWbrBrvodSkRx98VkcDqCFnQi（未能下载，见飞书原文）]\n前言 本节我们来实现知识库Agent的上半部分，即将文件向量化后存储到数据库中。\n这部分代码在：SuperBizAgent/src/main/java/org/example/service/VectorIndexService.java\n📷 [图片 token=Ss9ebtZFQolf6VxPUEscnUihnGf（未能下载，见飞书原文）]\n流程梳理 我们的目标是将文件向量化后存储到数据库中，这里面具体步骤：\n读取文件\n切分文件\n索引（Embedding和存储）\n读取文件 我们直接传入文件路径path，调用Files.readString读取文件内容到内存\n// 读取文件 String content = Files.readString(path); /** * 索引单个文件 * * @param filePath 文件路径 * @throws Exception 索引失败时抛出异常 */ public void indexSingleFile(String filePath) throws Exception { // 1. 读取文件内容 String content = Files.readString(path); logger.info(\u0026#34;读取文件: {}, 内容长度: {} 字符\u0026#34;, path, content.length()); // 2. 删除该文件的旧数据（如果存在） deleteExistingData(path.toString()); // 3. 文档分片 List\u0026lt;DocumentChunk\u0026gt; chunks = chunkService.chunkDocument(content, path.toString()); logger.info(\u0026#34;文档分片完成: {} -\u0026gt; {} 个分片\u0026#34;, filePath, chunks.size()); // 4. 为每个分片生成向量并插入 Milvus for (int i = 0; i \u0026lt; chunks.size(); i++) { DocumentChunk chunk = chunks.get(i); try { // 生成向量 List\u0026lt;Float\u0026gt; vector = embeddingService.generateEmbedding(chunk.getContent()); // 构建元数据（包含文件信息） Map\u0026lt;String, Object\u0026gt; metadata = buildMetadata(path.toString(), chunk, chunks.size()); // 插入到 Milvus insertToMilvus(chunk.getContent(), vector, metadata, chunk.getChunkIndex()); } } logger.info(\u0026#34;文件索引完成: {}, 共 {} 个分片\u0026#34;, filePath, chunks.size()); } 文件分块 第一层按照Markdown的标题#切分，将文档按照标题分割成多个章节Section\n第二层对每个章节进行分配，如果章节小于MaxSize，则直接将这个章节作为一个分配。\n如果章节大于MaxSize，则对段落边界进行切分\n对于对段落边界进行切分的地方，还会根据Overlap，实现段落间内容重叠，来保持段落之间的上下文语义连贯\n// 文档分片 List\u0026lt;DocumentChunk\u0026gt; chunks = chunkService.chunkDocument(content, path.toString()); logger.info(\u0026#34;文档分片完成: {} -\u0026gt; {} 个分片\u0026#34;, filePath, chunks.size()); // 核心实现 public List\u0026lt;DocumentChunk\u0026gt; chunkDocument(String content, String filePath) { List\u0026lt;DocumentChunk\u0026gt; chunks = new ArrayList\u0026lt;\u0026gt;(); // 1. 首先尝试按标题分割（Markdown格式） List\u0026lt;Section\u0026gt; sections = splitByHeadings(content); // 2. 对每个章节进行进一步分片 int globalChunkIndex = 0; for (Section section : sections) { List\u0026lt;DocumentChunk\u0026gt; sectionChunks = chunkSection(section, globalChunkIndex); chunks.addAll(sectionChunks); globalChunkIndex += sectionChunks.size(); } logger.info(\u0026#34;文档分片完成: {} -\u0026gt; {} 个分片\u0026#34;, filePath, chunks.size()); return chunks; } // 对单个章节进行分片 private List\u0026lt;DocumentChunk\u0026gt; chunkSection(Section section, int startChunkIndex) { List\u0026lt;DocumentChunk\u0026gt; chunks = new ArrayList\u0026lt;\u0026gt;(); String content = section.content; String title = section.title; // 如果章节内容小于最大尺寸，直接作为一个分片 if (content.length() \u0026lt;= chunkConfig.getMaxSize()) { // } // 章节内容较长，需要进一步分片 // 优先在段落边界分割 List\u0026lt;String\u0026gt; paragraphs = splitByParagraphs(content); StringBuilder currentChunk = new StringBuilder(); int currentStartIndex = section.startIndex; int chunkIndex = startChunkIndex; for (String paragraph : paragraphs) { // 如果当前分片加上新段落超过最大尺寸 if (currentChunk.length() \u0026gt; 0 \u0026amp;\u0026amp; currentChunk.length() + paragraph.length() \u0026gt; chunkConfig.getMaxSize()) { // 保存当前分片 // 开始新分片，包含重叠部分 } currentChunk.append(paragraph).append(\u0026#34;\\n\\n\u0026#34;); } return chunks; } 文件索引(向量化和存储到数据库) 首先对所有分片进行向量化，获取向量数组\n构造符合milvus表记录的结构体。id、content、vector、metadata\n构造完记录后，插入到数据库中\n// 4. 为每个分片生成向量并插入 Milvus for (int i = 0; i \u0026lt; chunks.size(); i++) { DocumentChunk chunk = chunks.get(i); try { // 生成向量 List\u0026lt;Float\u0026gt; vector = embeddingService.generateEmbedding(chunk.getContent()); // 构建元数据（包含文件信息） Map\u0026lt;String, Object\u0026gt; metadata = buildMetadata(path.toString(), chunk, chunks.size()); // 插入到 Milvus insertToMilvus(chunk.getContent(), vector, metadata, chunk.getChunkIndex()); logger.info(\u0026#34;✓ 分片 {}/{} 索引成功\u0026#34;, i + 1, chunks.size()); } } /** * 生成向量嵌入 * 调用阿里云 DashScope Text Embedding API * * @param content 文本内容 * @return 向量嵌入（浮点数列表） */ public List\u0026lt;Float\u0026gt; generateEmbedding(String content) { try { // 构建请求参数 TextEmbeddingParam param = TextEmbeddingParam .builder() .model(model) .texts(Collections.singletonList(content)) .build(); // 调用 API TextEmbeddingResult result = textEmbedding.call(param); // 检查结果 List\u0026lt;Float\u0026gt; floatEmbedding = getFloats(result); return floatEmbedding; } } /** * 插入向量到 Milvus */ private void insertToMilvus(String content, List\u0026lt;Float\u0026gt; vector, Map\u0026lt;String, Object\u0026gt; metadata, int chunkIndex) throws Exception { try { // 生成唯一 ID（使用 _source + 分片索引） String source = (String) metadata.get(\u0026#34;_source\u0026#34;); String id = UUID.nameUUIDFromBytes((source + \u0026#34;_\u0026#34; + chunkIndex).getBytes()).toString(); // 构建字段数据 List\u0026lt;InsertParam.Field\u0026gt; fields = new ArrayList\u0026lt;\u0026gt;(); // ID 字段 fields.add(new InsertParam.Field(\u0026#34;id\u0026#34;, Collections.singletonList(id))); // content 字段 fields.add(new InsertParam.Field(\u0026#34;content\u0026#34;, Collections.singletonList(content))); // vector 字段 fields.add(new InsertParam.Field(\u0026#34;vector\u0026#34;, Collections.singletonList(vector))); // metadata 字段（JSON 对象） com.google.gson.Gson gson = new com.google.gson.Gson(); com.google.gson.JsonObject metadataJson = gson.toJsonTree(metadata).getAsJsonObject(); fields.add(new InsertParam.Field(\u0026#34;metadata\u0026#34;, Collections.singletonList(metadataJson))); // 构建插入参数 InsertParam insertParam = InsertParam.newBuilder() .withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME) .withFields(fields) .build(); // 执行插入 R\u0026lt;MutationResult\u0026gt; insertResponse = milvusClient.insert(insertParam); if (insertResponse.getStatus() != 0) { throw new RuntimeException(\u0026#34;插入向量失败: \u0026#34; + insertResponse.getMessage()); } logger.debug(\u0026#34;向量插入成功: id={}, source={}, chunk={}\u0026#34;, id, source, chunkIndex); } catch (Exception e) { logger.error(\u0026#34;插入向量到 Milvus 失败\u0026#34;, e); throw e; } } 总结 到这里，提问前数据准备的三个流程就讲完了。其实代码实现并不难，核心是要搞懂这3个步骤里面都做了什么事情，以及代码是怎么讲流程串联起来的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ARAG%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%981%28Java%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;CGsjby7n5oDwEDx7iQBcVvXsnub\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：RAG代码实战1(Java)"},{"content":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。\n核心代码目录：app/tools\n当前时间查询工具 @tool 会从函数本身自动推导元数据：\n字段 来源 name 函数名 → get_current_time description 函数 docstring（三引号文档字符串） 参数 schema 类型注解 + docstring 里的 Args:（LangChain 会解析） 实际跑起来就是：\nname: get_current_time description: \u0026#39;获取当前时间\\n\\n当用户询问\u0026#34;现在几点\u0026#34;...\u0026#39; 想显式指定 name / description 时\n@tool(\u0026#34;current_time\u0026#34;, description=\u0026#34;返回指定时区的当前日期时间\u0026#34;) def get_current_time(timezone: str = \u0026#34;Asia/Shanghai\u0026#34;) -\u0026gt; str: ... @tool def get_current_time(timezone: str = \u0026#34;Asia/Shanghai\u0026#34;) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;获取当前时间 当用户询问\u0026#34;现在几点\u0026#34;、\u0026#34;今天星期几\u0026#34;、\u0026#34;今天日期\u0026#34;等时间相关问题时，使用此工具。 Args: timezone: 时区，默认为 Asia/Shanghai（北京时间） Returns: str: 格式化的当前时间信息 \u0026#34;\u0026#34;\u0026#34; try: # 获取指定时区的当前时间 tz = ZoneInfo(timezone) now = datetime.now(tz) # 返回格式化的日期时间字符串 return now.strftime(\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;) except Exception as e: logger.error(f\u0026#34;时间查询工具调用失败: {e}\u0026#34;) return f\u0026#34;获取时间失败: {str(e)}\u0026#34; 腾讯云日志MCP工具 通过 MCP Server 查询日志服务 CLS 中存储的日志数据，以实现大模型平台/工具与日志数据的结合。例如使用自然语言查询日志，降低日志查询复杂度 https://cloud.tencent.com/developer/mcp/server/11710\nMCP配置：[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n首先我们创建一个 SSE MCP 客户端。创建后进行初始化，最后调用load_mcp_tools_safe获取所有可用的工具即可。其中client.get_tools()是langchain提供的api，只需要会调用即可。\nasync def load_mcp_tools_safe( client: MultiServerMCPClient, ) -\u0026gt; tuple[list[Union[BaseTool, Any]], str | None]: \u0026#34;\u0026#34;\u0026#34;加载 MCP 工具；失败时返回空列表与可读错误信息，不向上抛出。\u0026#34;\u0026#34;\u0026#34; try: tools = await client.get_tools() return tools, None except BaseException as e: return [], format_exception_chain(e) ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AD%E7%AB%A0%EF%BD%9CTool%20%E5%92%8C%20MCP%20%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ATool%20%E5%92%8C%20MCP%20%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%98%28Python%29/","summary":"前言 在《Tool 与 MCP 设计思路》一节中我们提到了几个工具，那么这一节我们就来手把手的写2个工具，并交给大模型使用。 核心代码目录：app/tools  当前时间查询工具 @tool  会从函数本身自动推导元数据：  字段   来源","title":"源码分析：Tool 和 MCP 代码实战(Python)"},{"content":"人与 AI 的分工说清楚了：人负责判断方向、划定边界，AI 负责把决策落成代码。接下来最容易被忽略的问题是，人的判断怎样才能稳定地传给 AI，而不是只在一次对话里短暂生效？\n这正是规范驱动开发（Specification-Driven Development，简称 SDD）要解决的问题。它的重点不是“多写几份文档”，而是先把需求、边界和验收条件写成可检查的约束，再让 AI 在这些约束内完成实现。\n📷 [图片 token=Tvcgbjil3ooQ7VxSWTsccbCZnwh（未能下载，见飞书原文）]\n[!SUCCESS] **本讲核心：**Prompt 告诉 AI 这一次要做什么；OpenSpec 则把“为什么做、做到什么程度、哪些不能动、如何证明完成”保存为项目可持续使用的工程上下文。\n本文会直接使用课程项目 OncallAgent 的真实仓库来讲解。这个项目不是单页 Demo，而是一个本地优先的 AIOps Agent 工作台，包含 FastAPI 后端、Vue 3 前端、流式聊天、知识库 RAG、MCP 工具、告警诊断、SQLite 持久化和 Milvus 向量检索。功能之间存在大量跨层依赖，因此特别适合用来理解 SDD 的价值。\n📷 [图片 token=T2cDbJcwEoSexbxJTvfc0rYZnHp（未能下载，见飞书原文）]\n一、Prompt 很清楚，为什么项目还是会跑偏 假设你对 AI 说：“修一下聊天流式输出，让回答逐字出现，同时别把上一轮的引用带到下一轮。”这句话对人来说不难理解，但对实现来说仍然留下了许多空白。\n“逐字出现”应该由后端拆分 SSE 事件，还是由前端拿到整段文本后做动画？推理过程和工具调用事件要不要一起拆？逐字输出以后，最终保存到数据库的正文能不能变化？新一轮开始时应该立刻清空引用，还是等回答完成再清空？重新打开历史会话时，展示全部历史引用，还是只展示最新回答的引用？\n📷 [图片 token=AmjhbswK1oQ2e8xlyj5cqUfInqg（未能下载，见飞书原文）]\n这些问题没有写清楚，AI 就只能根据训练数据和当前代码自行补全。它可能给出一个能运行的版本，却未必是你真正需要的版本。更麻烦的是，同一个需求在不同对话里执行两次，AI 自行补全的答案也可能不同。\n只给任务描述 使用 OpenSpec 变更 意图停留在聊天上下文里 意图写入 proposal，可回看、可讨论 边界由 AI 临场猜测 Goals、Non-Goals 和设计决策明确边界 “看起来能用”就可能结束 Requirement 与 Scenario 给出可验证结果 下一次修改容易忘记来龙去脉 归档保留完整决策历史，主规格记录当前事实 所以，SDD 与“把 Prompt 写详细一点”并不是同一件事。详细 Prompt 可以提升一次输出的质量；SDD 解决的是一个项目在几十次甚至几百次 AI 协作之后，仍然保持同一套产品逻辑和工程约束。\n📷 [图片 token=VzISbwvuKoqYvtx5u9scQc7anHb（未能下载，见飞书原文）]\n二、一个真实变更如何被规范“锁定” 仓库中有一个已经完成并归档的变更，名称是 fix-chat-turn-streaming。它处理的正是上面提到的两个问题：回答看似使用 SSE，较大的模型 chunk 却会整段出现；“本次回答引用”还可能残留上一轮内容。\n如果只把任务交给 AI，它可能只改前端，也可能把所有 SSE 事件都拆碎，甚至顺手调整数据库结构。OpenSpec 的做法，是先建立一个完整的“变更包”。\n📷 [图片 token=Unb3b09FDoPuUKx7BuHc19Kjnac（未能下载，见飞书原文）]\nproposal：先回答为什么改，以及影响哪里 proposal.md 记录问题、期望变化和影响范围。这个变更明确影响聊天前端 store、后端 SSE 编排服务和相关测试，同时明确不修改共享事件类型、知识引用结构与数据库模型。这样一来，AI 在动手之前就知道哪些区域属于任务范围，哪些区域不应被顺手重构。\n📷 [图片 token=DNK3bJAU1ohJnpxo52ycyQukn2c（未能下载，见飞书原文）]\nspec：把模糊体验改写成可观察结果 “流式效果更顺滑”无法直接验收。delta spec 把它改写成了接近测试用例的场景：\nWHEN Agent 一次产生“收到。”三个字符 THEN 后端按“收”“到”“。”的顺序发出 content.delta AND sequence 逐个递增 AND 最终持久化正文仍然等于“收到。” 引用隔离也不再用“不要串轮次”这种模糊说法，而是明确：新一轮发送开始时清空旧引用；重新加载会话时只读取最新一条 assistant 消息的引用；如果最新回答没有引用，即使更早的回答有引用，当前引用区域也要保持隐藏。\n📷 [图片 token=MDiibQv9LovRfAx83m6cajmbnhw（未能下载，见飞书原文）]\ndesign：不仅写怎么做，也写为什么这样做 design.md 把关键取舍固定下来：只在后端 ChatStreamingService 边界拆分最终回答正文，因为这里能区分正文与推理、工具调用、引用等其他事件；持久化仍按原始 chunk 拼接，避免改变最终消息；推理、工具、引用和完成事件保持原来的结构与粒度。\n这段设计说明非常重要。单独写“不要拆工具事件”只是命令，补上原因之后，AI 才能理解这是为了控制事件数量、保护既有时序和共享 SSE 契约，而不是一个可以随意删除的偏好。\n📷 [图片 token=Fja2btSQiouTe4xSVmOcPjXEnub（未能下载，见飞书原文）]\ntasks：把规范变成可以逐项完成的工作 tasks.md 将工作拆成后端逐字符输出、前端引用隔离、回归测试和交付验证。AI 每完成一项就勾掉一项，人也能随时看到剩余工作，而不是等到最后才发现“后端改了，前端测试没补”。\n最终实现与规范一一对应：后端在 apps/backend/src/super_ai/chat/streaming.py 中逐字符发出正文事件；前端在 apps/frontend/src/stores/chat.ts 中发送前清空引用，并在加载会话时只选择最新 assistant 的 citations；前后端测试分别覆盖事件顺序、持久化一致性和跨轮引用隔离。\n📷 [图片 token=XgNrbjPtDo4cZsxhVMscyx4Snff（未能下载，见飞书原文）]\n三、OpenSpec 把 SDD 变成一条可执行流水线 理论上的 SDD 常被概括为“先规范、再实现、再验证”。OpenSpec 进一步把这套思路落成了仓库结构和操作流程。对学生来说，可以先记住下面这条主线：\n理解现状 → 建立 change → 写清规格与设计 → 按任务实现 → 对照规格验证 → 同步并归档\n📷 [图片 token=HZo3bjFWTo8SZKxKZybcoBtSnwg（未能下载，见飞书原文）]\n第一步：理解现状，而不是立刻生成代码 先阅读相关主规格、现有实现和测试，确认当前系统已经支持什么、这次只改变什么。OncallAgent 的 AGENTS.md 明确要求：新增功能、用户可观察行为变化和非平凡缺陷修复，应先创建或继续一个聚焦的 OpenSpec change。\n这一步看似慢，实际上是在避免最昂贵的错误：AI 按一个错误前提写出大量正确代码。比如，仓库已经规定 packages/api-contracts 是 HTTP、错误码、OpenAPI 与 SSE 类型的唯一事实来源。如果没有先读现状，AI 很容易在前端或后端再复制一套临时 DTO，功能能跑，却给项目留下两套契约。\n📷 [图片 token=Q5X6bV3vMo9vJxxcbtuc1OkDnsf（未能下载，见飞书原文）]\n第二步：建立一个边界清楚的 change 变更名使用简短的 kebab-case，例如 fix-chat-turn-streaming。基础命令如下：\nopenspec new change fix-chat-turn-streaming openspec status --change fix-chat-turn-streaming 在这个仓库中，也可以通过配套的 AI skills 按完整生命周期推进：\n$openspec-propose $openspec-apply-change $openspec-verify-change $openspec-archive-change 一个标准 change 会逐步形成 proposal.md、specs/、design.md 和 tasks.md。四类文件回答四个不同问题：为什么改、外部行为是什么、内部怎样实现、具体先做什么后做什么。\n📷 [图片 token=DEPab2usvosF4gxtI8xcHYxenQc（未能下载，见飞书原文）]\n第三步：AI 按任务实现，人持续做决策 进入 apply 阶段后，AI 读取完整变更上下文，再按 tasks 逐项实现。此时人的工作不是盯着每一行代码，而是判断实现有没有偏离目标：是否触碰 Non-Goals，是否破坏共享契约，是否遗漏权限过滤，是否增加了不必要的依赖，是否为场景补上了测试。\n任务清单也不是一张“许愿单”。每完成一项就应立即更新状态；如果实现过程中发现设计不成立，应返回修改 design 或 spec，而不是让代码偷偷偏离文档。规范可以迭代，但规范与实现不能各走各的。\n📷 [图片 token=Rsy8biY7qodNHaxQosMc5NHMnlh（未能下载，见飞书原文）]\n第四步：验证的对象不是“代码能运行”，而是“需求被证明” 验证至少包含三个层面。首先看完整性：tasks 是否全部完成，Requirement 是否都有实现落点。其次看正确性：每个 Scenario 是否有对应逻辑与测试。最后看一致性：实现是否遵守 design、全局仓库规则和既有代码模式。\nOpenSpec 本身需要执行结构校验：\nopenspec validate --all 但这条命令不能替代代码测试。聊天或 SSE 变更还应运行共享契约测试、相关后端 pytest 和前端 Vitest；数据库变更要验证 Alembic migration；可见界面变化需要浏览器验收。只有实际执行并通过的检查，才可以写进交付结果。\n📷 [图片 token=G6dKbEmmuoOiUvxTOP6cw6ozntg（未能下载，见飞书原文）]\n第五步：同步主规格并归档，让项目拥有长期记忆 变更通过验证后，将 delta specs 同步到 openspec/specs/，再把 change 移入 archive。此后，主规格表达“系统现在应该怎样工作”，archive 保留“当时为什么这样改”。\n这一步解决了 AI 没有稳定长期记忆的问题。下一次处理聊天流时，AI 不需要依赖某个人记得半个月前的一次对话，只需要读取当前主规格和历史变更，就能恢复关键决策。\n📷 [图片 token=TPHYbiAtNokMc5xqQeDcLTcznDd（未能下载，见飞书原文）]\n四、仓库里的规范不是一份文件，而是四层约束 随着项目变复杂，把所有规则都塞进一个超长提示词并不可取。OncallAgent 采用分层的事实来源，每一层负责不同问题。\n层次 仓库位置 负责回答的问题 全局工程规则 AGENTS.md 整个仓库长期遵守什么原则 当前产品规格 openspec/specs/ 系统现在对用户承诺什么行为 本次变更规范 openspec/changes/\u0026lt;change\u0026gt;/ 这次为什么改、改什么、不改什么 机器可检查契约 packages/api-contracts/ 与测试 接口形状、事件类型和场景如何被自动验证 AGENTS.md 像项目宪法。例如，FastAPI 路由保持薄层，业务逻辑放进 service 或 repository；前端沿用 Vue 3、TypeScript strict、Pinia 和既有 client；所有用户数据必须携带 owner 或 tenant scope；日志不得记录密钥、用户消息、工具参数值和模型正文。\nopenspec/specs/ 像现行法律。这里分别描述聊天、知识库、MCP、AIOps、后台任务、认证授权等能力当前必须满足的行为。\n活动 change 像一次修正案。它只处理一个聚焦问题，用 proposal、delta spec、design 和 tasks 说明变化。完成后，真正需要长期保留的规则进入主规格，过程材料则进入归档。\npackages/api-contracts 和测试则把文字约束变成机器能检查的边界。例如共享 SSE 类型明确有哪些事件，前后端都从同一处消费；测试进一步证明逐字符事件顺序、错误结构和引用隔离没有被破坏。\n📷 [图片 token=HzABb4GHFojbyrxOX59ciR5Cntf（未能下载，见飞书原文）]\n五、能约束 AI 的规范，要满足四个条件 条件一：描述可观察行为，不写空泛形容词 “体验要流畅”“代码要优雅”“权限要安全”都无法直接检查。更有效的写法是：最终回答的每个 content.delta 只包含一个字符；跨 tenant 读取返回统一 403；知识检索必须携带当前用户和知识库过滤；空结果返回空数组，不生成虚构内容。\n判断方法很简单：两位同学看到同一条规范，能否写出基本相同的验收测试？如果不能，规范仍然太模糊。\n📷 [图片 token=AeoJbbUlZosANQxI5YwcHVQjnNc（未能下载，见飞书原文）]\n条件二：明确 Non-Goals，主动缩小解释空间 规范不仅要说“做什么”，还要说“这次不做什么”。聊天逐字流变更明确不调整 LangChain Agent、不修改数据库、不改变推理和工具事件粒度。Non-Goals 能阻止 AI 把一个小修复扩展成跨模块重构，也能帮助人判断某个“顺手优化”是否应该拆成另一个 change。\n📷 [图片 token=IXtZbkbyOohVOlxcyoZc5DuwnDb（未能下载，见飞书原文）]\n条件三：重要取舍要说明原因 涉及工程权衡时，只写禁止项往往不够。比如仓库规定前后端不能各自复制接口 DTO，因为 packages/api-contracts 是唯一事实来源；AIOps 不能伪造日志、告警和工具结果，因为诊断报告必须能追溯到真实证据链。原因会帮助 AI 在没有逐字覆盖的新场景中做出同方向判断。\n📷 [图片 token=Q4TLbdJXfoKp8ixje1McmDWznxe（未能下载，见飞书原文）]\n条件四：每条要求都能映射到验证 好的 spec 天然带着测试入口。WHEN 描述前置条件，THEN 描述必须出现的结果，MUST 与 SHALL 表示不可随意降级的约束。如果某条 Requirement 找不到实现位置，也找不到测试或手工验收方法，它大概率还没有写到可执行的程度。\n📷 [图片 token=BawQbb2EVoZeSdxjOZqcAxSOn8f（未能下载，见飞书原文）]\n六、AI 跑偏时，别只修代码，还要修轨道 第一次写出的规范一定不完整，这很正常。SDD 的关键不在于开工前预测所有问题，而在于把每次偏差转化为下一次可以复用的约束。\n仍以聊天逐字流为例。如果 AI 把 reasoning 事件也拆成单字符，修复代码只是处理这一次错误；把“非正文事件保持原粒度”补进 Scenario，才是在修轨道。如果前端重新加载会话时又聚合了全部历史引用，除了改 store，还应把“只取最新 assistant citations”写进当前主规格和回归测试。\n📷 [图片 token=NQlrbknTjob6w1xVlSTc0A6JnAb（未能下载，见飞书原文）]\n仓库中的许多全局规则也是这样沉淀出来的：API/SSE 统一走共享契约；路由不承载业务逻辑；SQLite 通过 repository 边界访问；Milvus 检索必须带 tenant 过滤；MCP 与 AIOps 不得用 mock 结果冒充真实成功；真实凭据不能进入 Git、日志或错误响应。\n每当 AI 偏离预期，可以按下面三个问题复盘：\n这是一次偶然实现错误，还是规范没有说明？\n这条约束只属于当前任务，还是以后所有模块都应遵守？\n怎样把它写成一个可以通过测试或检查证明的 Scenario？\n如果只影响本次实现，就更新 change 的 spec、design 或 tasks；如果是长期产品行为，就在归档时同步进主规格；如果是全仓库都要遵守的工程原则，就补充到 AGENTS.md 或共享契约。这样，偏差不会白白发生，而会成为项目知识的一部分。\n📷 [图片 token=NX1vbYcWTo48T3xDS2sclCgXnec（未能下载，见飞书原文）]\n七、第一次实践 OpenSpec，可以从小需求开始 不要一上来给整个系统写一份几十页的“大一统规范”。选择一个用户能感知、范围又足够小的变化最合适，例如“知识文档上传失败时保留可重试状态”“MCP 连接检查显示安全错误摘要”或“聊天最新回答没有引用时隐藏引用区”。\n📷 [图片 token=XWycbkh7VoPz6uxgRZYcCtQ5nue（未能下载，见飞书原文）]\n动手前检查：\n我已经阅读相关主规格、实现和测试，而不是只看需求描述。\n我能用一句话说明用户当前遇到的问题。\n我写清了这次变化的范围与 Non-Goals。\n每条 Requirement 至少有一个可观察的 Scenario。\n实现中检查：\nAI 正在按 tasks 逐项实现，完成后立即更新任务状态。\n实现没有越过 change 边界，也没有绕开共享契约和 tenant 规则。\n发现设计问题时，我先更新规范，再继续写代码。\n交付前检查：\n我运行了 openspec validate --all。\n相关前端、后端、契约或迁移测试已经真实执行。\ndelta specs 已同步到主规格，change 已验证并归档。\n这份清单的意义不是增加仪式感，而是帮助初学者把“我觉得做完了”转换为“我能证明它做完了”。当这种思维形成习惯后，你会发现 review 变得更快，因为检查不再依赖临场感觉，而是逐条核对已经约定的行为。\n📷 [图片 token=HVNrbJe5coI5Z8xKAkhce0q2nf7（未能下载，见飞书原文）]\n总结：真正的轨道，是可以验证和积累的决策 SDD 不是要求人把所有代码细节提前设计完，也不是让 AI 失去发挥空间。它要固定的是目标、边界和验收标准，把实现路径中的重复劳动交给 AI，把需要判断的工程取舍留给人。\n在 OncallAgent（Agent Py）仓库中，OpenSpec 让这套方法形成了闭环：AGENTS.md 提供全局工程纪律，openspec/specs/ 保存当前产品事实，change 记录一次变化的 proposal、spec、design 与 tasks，共享契约和测试负责自动验证，archive 留下决策历史。\n📷 [图片 token=UoV8bizSmonTugx8I74cdRURnix（未能下载，见飞书原文）]\n没有规范时，AI 也许能很快写出一段可运行代码；有了 OpenSpec，它才更有机会连续完成一个长期演进、跨前后端并且需要真实验证的工程项目。\n**记住这条公式：**先把需求写成可验证的 change，再让 AI 实现；发现偏差就更新规范，验证通过后同步并归档。规范越清晰，返工越少；项目积累得越久，这套轨道的价值越大。\n下一步可以选择仓库里的一个小改动，亲手创建第一个 OpenSpec change。不要从“请帮我写代码”开始，而要先回答四个问题：为什么改、外部行为是什么、哪些东西不能动、如何证明完成。\n📷 [图片 token=PmYDbOdzrocP56xjIJQchsponJb（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/02%EF%BD%9CAI%20Coding%20%E5%9F%BA%E7%A1%80%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A6%E6%9D%9F/%E8%A7%84%E8%8C%83%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91SDD%EF%BC%9A%E8%AE%A9AI%E6%B0%B8%E8%BF%9C%E5%9C%A8%E8%BD%A8%E9%81%93%E4%B8%8A/","summary":"人与 AI 的分工说清楚了：人负责判断方向、划定边界，AI 负责把决策落成代码。接下来最容易被忽略的问题是，人的判断怎样才能稳定地传给 AI，而不是只在一次对话里短暂生效？ 这正是规范驱动开发（Specification-Driven De","title":"规范驱动开发SDD：让AI永远在轨道上"},{"content":"咱们做Agent项目，核心是解决团队的真实痛点，但面试时要把这些痛点转化为有技术深度、有业务价值的亮点。下面结合项目实际，从面试高频关注的方向拆解亮点\n功能亮点 从人工回复到AI自动应答 **没有Agent时：**值班经常被同事折磨的不行，上游的业务同事反复问某某报错什么原因，前不久问过的今天还问，文档里都写了还一直问，烦也烦死了。\n**有了Agent之后：**彻底从人工客服解放，你的问题我都在文档里写的这么清楚，您慢慢跟AI问吧。面试时把上面的场景一说，面试官肯定能秒懂，比制造虚无的场景好解释多了。\n对话Agent的业务属性通用 **没有Agent时：**人工手动去翻技术文档、告警手册、历史工单\u0026hellip;\n有了Agent之后：只要文档上传到知识库了，就能进行问题匹配+知识检索。不再需要找某个文档翻好久的目录，不需要刻意记文档放哪了。只要提出问题就能快速匹配到对应的内容，无论是给研发用，业务用，运维用，前端用，全部都适用。问题匹配+知识检索无缝对接所有涉及到文档的场景。\n自动沉淀，天然隔绝口口相传的问题 **没有Agent时：**值班的祖传经验都在脑子里，其实很多人都懒得把值班遇到的问题和解决方案写到文档上。知识断层，新的人值班遇到了又是浪费时间，踩坑是常事，效率极其低。\n**有了Agent之后：**新人不用靠口口相传，工单问题和解决方案自动总结沉淀为知识库里的文档，下次遇到了相同的问题问一遍AI，快速解决。懒到文档都不自己写，AI自动总结沉淀真的好爽。\n跨系统联动日志和监控，提升效率 **没有Agent时：**排查故障要手动切换N个系统：告警群看消息、日志平台搜日志、监控平台查指标、办公软件问同事。折腾半天才能凑齐信息。\n**有了Agent之后：**运维Agent打通日志、监控、告警群、知识库。比如从告警消息提取接口名和时间，自动调用日志API查日志，最后汇总成故障排查报告。\n技术(装逼)亮点 AI的新技术名词比较多，适合装逼 **RAG：**面试官肯定会问RAG是什么意思，有哪些流程，怎么分片，怎么召回的更准？\n**prompt工程：**这是一个非常适合展示思考过程的点。 亮点准备不要只展示最终结果。建议准备一个迭代故事。第一版prompt有哪些缺点，第二版改了什么有什么好处\u0026hellip;\n**多轮对话：**记忆功能v1.0，记忆功能v2.0带总结版本\u0026hellip;可以把你的设计展开讲，并且还有阶梯式的优化，体现你的思考\n**流式输出SSE：**以前没接触过，但搞懂之后太简单了\u0026hellip;\n**Agent设计模式：**ReAct、Plan-Execute 这些设计模式的核心原理是什么，有什么好处？\n**ReAct：**无ReAct的Agent无法拆分任务、调用工具 。那么ReAct是怎么做到像人一样思考行动的？\n**Plan-Execute-Replan：**这种模式让运维Agent能应对复杂、多变的故障场景，那么它和ReAct的区别是什么？什么时候用ReAct，什么时候用这个模式。面试中主动说出来，那么这就是你的深入思考，你的亮点。\n以上都是面试中非常常见的问题，简直就是开卷考试，那么考试答案见《热门面试题》\n总结 把功能价值和技术亮点结合起来讲，既能让面试官看到项目解决的实际问题，又能体现你的技术深度和持续优化的能力，是面试时的加分项。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/%E9%9D%A2%E8%AF%95%E4%BA%AE%E7%82%B9%E6%89%93%E9%80%A0/","summary":"咱们做Agent项目，核心是 解决团队的真实痛点 ，但面试时要把这些 痛点 转化为 有技术深度、有业务价值 的亮点。下面结合项目实际，从面试高频关注的方向拆解亮点  功能亮点  从人工回复到AI自动应答 没有Agent时： 值班经常被同事折","title":"面试亮点打造"},{"content":"前面的文章解释了 AI Coding、OpenSpec 和 OncallAgent 的系统结构，但真正开始开发时，最容易卡住的不是概念，而是到底怎么迈出第一步。\n这篇文章直接做一遍。我们使用仓库根目录的 openspec从0到1项目实战的提示词.md，从 P01 开始，把一个空目录变成可验证的 Monorepo，再说明如何按相同节奏推进到完整 OncallAgent。文中的提示词来自准备稿，终端证据来自当前仓库 Git 历史与 OpenSpec 归档，最后一张图来自当前前端的实际运行页面。\n先看最终操作路径 一次完整的 OpenSpec 实战不是“让 AI 写代码”，而是让 Codex 沿着一条可检查的链路工作：\n在空仓库完成 Git、OpenSpec 和 Codex skills 初始化。\n一次只复制一个提案提示词，例如 P01。\nCodex 先生成 proposal、design、tasks 和 delta spec。\n继续 apply，让任务清单真正变成代码、配置与测试。\n运行 verify；失败时让 Codex根据原始错误修复并重跑。\n验证通过后同步 main specs，再 archive。\n确认归档与质量门禁都通过，才进入 P02。\n[!CAUTION] 本文用 P01 完整展开。P02–P27 的操作方式相同，但每次只推进一个 change。不要一次把 27 段提示词全部交给 Codex，也不要在上一个 change 未验证、未归档时开始下一个。\n在 Codex 打开空仓库并做 preflight 先创建空目录，用 Codex 打开这个目录。不要一开始就把最终需求写成长篇自由文本；先让工具链处于可用状态。macOS 或 Linux 终端依次执行：\ngit --version python3 --version uv --version node --version npm --version docker --version docker compose version openspec --version git init openspec init --tools codex . 这里的目的不是追求“所有工具版本越新越好”，而是尽早暴露缺失项。如果 openspec --version 返回 command not found，就先安装准备稿指定的 OpenSpec CLI 1.5.0，再继续初始化：\nnpm install -g @fission-ai/openspec@1.5.0 openspec --version openspec init --tools codex . 初始化完成后，在 Codex 中确认可以调用以下四个 skills：$openspec-propose、$openspec-apply-change、$openspec-verify-change、$openspec-archive-change。它们不是四个互不相关的快捷命令，而是同一个 change 的四个生命周期阶段。\n🎬 视频「01. 开发环境初始化确认@小林coding.mov」（飞书视频，无法在博客播放）\n第一次给 Codex 的提示词是什么 打开 openspec从0到1项目实战的提示词.md，定位到“P01：锁定技术栈与安全 Monorepo 骨架”，复制整个 text 代码块，不要只复制标题或前两段。下面是实际输入的核心部分：\n请完成第 01 个 OpenSpec change：bootstrap-secure-monorepo-foundation。 请依次使用 $openspec-propose、$openspec-apply-change、 $openspec-verify-change、$openspec-archive-change 完成完整生命周期。 不要只生成 proposal/design/spec/tasks 后停止；若验证发现问题， 先修复并重新验证，再同步 delta specs 和归档。 所有 OpenSpec 文档使用简体中文。 这是一个全新项目的第一提案。本提案必须先锁定整个项目的技术栈、 目录骨架、工程边界和质量基线，但不要实现认证、聊天、知识库、 AIOps、MCP 等产品功能。 建立最终目录： apps/backend、apps/frontend、packages/api-contracts、config、 infra、scripts、openspec、docs。 后端包必须位于 apps/backend/src/super_ai， 只允许 from super_ai...，禁止从 src.super_ai 导入。 模块 import 期间不得连接 SQLite、Milvus、LLM 或 MCP。 只提交无密钥的 config/project.template.json 与 config/user.project.template.json。本机 project.json 和 user.project.json 必须被 Git 忽略。 验收至少运行：openspec validate --all；backend 的 uv run ruff check .、uv run pyright、uv run pytest； contracts typecheck/test；frontend typecheck/test/build； git diff --check。所有门禁通过后才归档。 准备稿里的完整 P01 还明确了 Python、FastAPI、Vue、TypeScript、LangChain、Milvus、配置深合并、浏览器 public allowlist、Compose 边界和 sentinel secret 扫描。这里不要擅自删减，因为这些限制会进入 design、AGENTS.md、测试和后续所有 change 的上下文。\nCodex 收到提示词后，应该怎样推进 🎬 视频「02. 第一个提案演示@小林coding.mov」（飞书视频，无法在博客播放）\n3.1 propose：先把需求变成可审查的工程决策 Codex 首先读取仓库约束和 OpenSpec 配置，创建一个 focused change。此时先看文件，不要只看聊天窗口是否说“完成”。至少应出现：\nopenspec/changes/bootstrap-secure-monorepo-foundation/ ├── proposal.md ├── design.md ├── tasks.md └── specs/ └── project-foundation/ └── spec.md proposal.md回答为什么要做、改什么；design.md锁定技术选择、边界和取舍；tasks.md把工作拆成可勾选任务；delta spec 则写清新增或修改后的可验证行为。只出现这四类文件，说明 change 已经被建模，但代码还没有实现。\n3.2 apply：任务清单必须落到代码、配置与测试 进入 apply 后，Codex 才应该创建 apps/backend、apps/frontend、packages/api-contracts、config、infra 和 docs 等实际目录，并按任务逐项实现。观察过程中重点看三件事：\n后端是否真的使用 apps/backend/src/super_ai 的 src layout，而不是临时放在根目录。\n配置模板是否无密钥，本机配置是否被 Git 忽略，前端构建是否无法拿到 LLM、CLS、MCP 等 secret。\n每个“完成”是否有测试或检查支撑，而不是只把 tasks.md 的复选框改成已完成。\n如果 Codex 在生成 artifacts 后停下，可以继续发送：\n继续执行当前 change 的 apply。请按 tasks.md 顺序实现代码与测试， 不要创建新的 change，不要跳过质量门禁。完成后继续 verify； 若验证失败，先修复并重跑，不要只汇报失败。 3.3 verify：让错误输出驱动下一轮修复 verify 的目标不是“执行过测试”，而是证明 proposal、design、tasks、delta spec、实现和测试互相一致。P01 至少要跑以下门禁：\n# 仓库根目录 openspec validate --all npm --workspace packages/api-contracts run typecheck npm --workspace packages/api-contracts run test npm run frontend:typecheck npm run frontend:test npm run frontend:build git diff --check # apps/backend uv run ruff check . uv run pyright uv run pytest 如果某一条失败，不要把错误改写成一句“构建失败”再问 AI。保留原始命令、退出码和关键输出，让 Codex在同一 change 中定位。例如：\nverify 中 npm run frontend:build 失败。请基于刚才的原始错误定位根因， 只修改当前 P01 范围内的文件；不要删除测试、不要放宽 TypeScript strict 选项、不要把 secret 注入浏览器。修复后重新运行受影响测试和完整 P01 门禁，全部通过后再继续 spec sync 与 archive。 3.4 archive：归档不是移动文件这么简单 验证通过后，Codex 会将 delta spec 同步到 openspec/specs，再把 change 移入 openspec/changes/archive。如果归档阶段询问是否同步规格，选择 Sync now。完成后再次执行 openspec validate --all，确认 main specs 与 archive 都有效。\n归档成功以后才复制 P02。否则 P02 可能建立在未同步的 contracts 或错误的目录边界上，后面每个 change 都会放大这笔技术债。\n如何从 P01 连续开发到 OncallAgent P01 不是在“搭架子以后再随便写功能”，而是在建立后续 26 个 change 共同遵守的轨道。准备稿已经按依赖关系排好顺序：\n阶段 提案范围 每阶段可见结果 工程底座 P01–P09 Monorepo、HTTP/SSE contracts、SQLite、认证与 tenant 隔离、Qwen、Milvus、Vue 壳、durable jobs 知识系统 P10–P13 文档上传与切分、持久索引、混合召回、RRF、真实 rerank、知识库桌面 UI Chat Agent P14–P19 会话、SSE Agent、Prompt/Skill、记忆、真实 MCP、引用和打字机效果 AIOps P20–P24 真实告警与 CLS、LangGraph 诊断、证据链、案例沉淀、AIOps UI、反馈 交付闭环 P25–P27 真实 fixtures、readiness、可观测性、运维文档、OpenSpec WIKI 每个阶段都重复同一个小闭环：复制一个提案提示词，观察 artifacts，完成 apply，执行 verify，修复，sync，archive。真正的项目进度不是 Codex 回答了多少字，而是 archive 中多了一个可验证 change，main specs 多了一组当前事实，代码和测试多了一段可运行能力。\n下一步：按同样方式执行 P02 确认 P01 已归档且 openspec validate --all 通过后，回到准备稿复制 P02“统一 HTTP、错误、OpenAPI 与 SSE 契约”的完整提示词。P02 完成时，不只是多了几种 TypeScript 类型，而应该能在共享 contracts、FastAPI envelope、request-id、SSE parser 和前后端合同测试中看到同一套可验证结构。\n之后严格按 P03、P04……推进。你不需要重新发明提示词，也不需要每次临场决定技术方案；准备稿已经把历史上被推翻的路线裁剪掉，把修复、重构和 UI polish 吸收到对应功能的最终态提案中。实战的重点，是在 Codex 中执行每个 change 的完整生命周期，并用代码、测试、归档和运行。\n🎬 视频「03. 第二个提案演示@小林coding.mov」（飞书视频，无法在博客播放）\n三个最容易踩的坑 只让 Codex 生成 artifacts。 proposal、design、tasks 和 spec 不是交付物的全部。看到 artifacts 后就开始下一个需求，会留下大量“规格已写、代码未做”的幽灵 change。\n一次粘贴多个提案。 P01–P27 存在明确依赖。并发堆叠会让 contracts、migration、前后端 DTO 和 main specs 同时漂移，最后很难判断失败属于哪个 change。\n为了通过测试削弱门禁。 删除测试、关闭 strict、把外部依赖改成运行时 mock、把 secret 放进环境变量或浏览器 bundle，都会让“绿色”失去意义。正确做法是保留约束，修复实现。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/04%EF%BD%9C%E7%94%A8%20Codex%20+%20OpenSpec%20%E4%BB%8E%E9%9B%B6%E5%AE%9E%E6%88%98%20OncallAgent%EF%BC%9A%E6%8F%90%E7%A4%BA%E8%AF%8D%E3%80%81%E6%89%A7%E8%A1%8C%E4%B8%8E%E9%AA%8C%E6%94%B6%E5%85%A8%E8%AE%B0%E5%BD%95/","summary":"前面的文章解释了 AI Coding、OpenSpec 和 OncallAgent 的系统结构，但真正开始开发时，最容易卡住的不是概念，而是到底怎么迈出第一步。 这篇文章直接做一遍。我们使用仓库根目录的  openspec从0到1项目实战的","title":"04｜用 Codex + OpenSpec 从零实战 OncallAgent：提示词、执行与验收全记录"},{"content":"OncallAgent 的本地运行拓扑刻意把“容器基础设施”和“应用进程”分开。infra/compose.yaml 只运行 etcd、MinIO、Milvus、Attu 与 Alertmanager；FastAPI 后端、Vue 前端和官方 CLS MCP Server 由宿主机启动器运行。这个边界让代码调试、Python 与 Node 依赖保持本机开发体验，同时用 Compose 管理需要持久卷和服务依赖的基础设施。\n📷 [图片 token=SBg1bjM26oRTrsxv83qc5iudn7D（未能下载，见飞书原文）]\nMilvus 在系统中的职责很窄：只保存知识文档 chunk 的向量和检索所需标量。用户、认证 session、聊天消息、Prompt、Skill、文档元数据、索引任务、AIOps 证据和报告仍由 SQLite Repository 管理。把“向量数据库”理解成应用主数据库会误读删除、恢复和权限语义；它是可重建的知识索引，而不是业务事实的唯一来源。\n📷 [图片 token=MiWdbsb2Oo7EN7x5TvKcBozsnxc（未能下载，见飞书原文）]\n向量边界同时承担权限责任。每条 chunk 都携带 owner、tenant、知识库、文档和 chunk ID；搜索、标量枚举与删除都必须包含明确范围。授权后的知识库集合为空时，代码直接返回空结果，不建立无范围查询。对 AI Native 系统而言，这条规则比召回率更优先，因为一次无范围检索就可能把其他用户知识暴露给模型。\n📷 [图片 token=QqBWbHOrPoW6gVxBTTScX8ynn2c（未能下载，见飞书原文）]\n学习目标 📷 [图片 token=Twm6bhNfGo4xCzxs9EHcqyMqnRf（未能下载，见飞书原文）]\n理解 Compose 服务依赖、端口、健康检查和宿主机应用边界。\n掌握 Milvus collection 的字段、HNSW/COSINE 设置与显式连接生命周期。\n追踪文档从 SQLite 元数据、切分、embedding 到范围向量写入的完整流程。\n理解向量召回、BM25L、RRF 与 Qwen rerank 如何组合，又如何共享 tenant 范围。\n识别 Milvus、embedding 与容器失败时的安全状态和恢复路径。\n功能入口与完整调用链 基础设施入口是仓库根目录执行 docker compose -f infra/compose.yaml up -d etcd minio milvus attu alertmanager。etcd 与 MinIO 先通过各自 healthcheck，Milvus 的 depends_on 要求二者 healthy 后启动 standalone 服务，并在 9091 的 healthz 报告健康；Attu 再等待 Milvus healthy。宿主机通过 19530 访问 Milvus，浏览器可通过映射后的 8001 打开 Attu。Alertmanager 独立暴露 9093，并挂载只读配置。\n📷 [图片 token=QBSOb8ukJoGHefx08BpcDeZLniZ（未能下载，见飞书原文）]\nscripts/start-local.sh 与 scripts/start-local.bat 是完整本机启动器：它们启动 Compose 基础设施，安装依赖、执行 Alembic migration，并在宿主机启动 CLS MCP、后端和前端。由于这些操作会拉取镜像、修改依赖、迁移数据库并创建进程，启动器不是无副作用验证命令。Compose 自身不构建应用镜像，也不包含 backend、frontend 或 MCP 服务。\n📷 [图片 token=BDE1b9pY3o3YZ7xJP2ec87QlnBd（未能下载，见飞书原文）]\n后端向量设置由 apps/backend/src/super_ai/vector_store/config.py 的 load_milvus_vector_store_settings 从合并项目配置的 vectorStore section 读取。默认语义是本机 19530、collection knowledge_chunks、1024 维、HNSW、COSINE、索引参数 M 16 与 efConstruction 200、搜索参数 ef 64。类型化设置创建 MilvusVectorStore，但不会在模块导入或对象构造时连接。\n📷 [图片 token=Isz3bNweeotT2lx6V1ucGqwJnld（未能下载，见飞书原文）]\n文档索引从受保护 API 创建 SQLite 业务任务和 durable background job。worker 调用 DocumentIndexingService.run_task，顺序是：owner-scoped 读取任务、标记 running/indexing、读取文档、按持久化策略切分、调用 embedding、验证向量数量、initialize collection、按 tenant/知识库/文档删除旧 chunk、insert_chunks 批量写入、更新 succeeded/indexed。读取文档以及后续切分、模型和向量步骤位于异常捕获范围内，失败会把任务与文档标记为 failed 并保存有界原因；最初的任务读取和 running/indexing 状态更新在该捕获块之外，若这些 Repository 操作本身失败，则由上层 durable job 记录失败，业务记录可能停留在原状态或中间状态。\n📷 [图片 token=LVchbnXNSoicZbx1RhNczeHWnlf（未能下载，见飞书原文）]\nMarkdown / PDF 上传 → SQLite 保存文档元数据与可索引文本 → durable document_index job → chunk_document_text → OpenAI-compatible embedding → MilvusVectorStore.initialize → tenant + knowledgeBase + document 范围删除 → 批量 insert knowledge chunk vectors → SQLite 更新任务和文档状态 📷 [图片 token=AzvMbewZLoExWexb9k4ciD0Lnc0（未能下载，见飞书原文）]\n检索时，KnowledgeRetrievalTool.run 先把调用者请求的知识库与当前用户可访问集合求解；越权 filter 直接抛 AUTH_FORBIDDEN，空集合直接返回空 results/citations。非空时并行执行向量召回和关键词召回：前者为 query 生成 embedding 并调用 search_chunks，后者调用 list_chunks 只读取标量内容，再以内存 BM25L 排名。两路候选用 RRF 融合，最后由 rerank 模型精排并生成带各阶段排名、分数和来源的引用。\n📷 [图片 token=ZparbLKkBoEaFJxwUIfcQsKpnwg（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 infra/compose.yaml etcd、minio、milvus、attu、alertmanager 定义本地容器拓扑、持久卷、端口、依赖和健康检查。 infra/README.md 基础设施与宿主机应用说明 记录启动、停止、端口与“不自动索引/上传”的操作边界。 apps/backend/src/super_ai/vector_store/config.py MilvusVectorStoreSettings、load_milvus_vector_store_settings 从项目 JSON 加载 collection、维度、索引、metric 与超时。 apps/backend/src/super_ai/vector_store/schema.py build_chunk_collection_schema、build_index_definitions 以无连接的数据结构定义 collection 字段和标量/向量索引。 apps/backend/src/super_ai/vector_store/milvus.py MilvusConnectionManager、MilvusVectorStore、VectorChunkRecord 显式连接、初始化、写入、搜索、枚举、删除和健康检查。 apps/backend/src/super_ai/memory/vector_scope.py build_vector_chunk_metadata、build_milvus_tenant_filter 生成统一权限 metadata 和转义过的 Milvus 范围表达式。 apps/backend/src/super_ai/documents/indexing.py DocumentIndexingService、chunk_document_text、_vector_chunk_record 切分、embedding、范围重建向量并同步 SQLite 状态。 apps/backend/src/super_ai/retrieval/tool.py KnowledgeRetrievalTool、_resolve_knowledge_base_ids 授权知识库过滤、双路召回、融合、rerank 和引用构造。 apps/backend/src/super_ai/retrieval/hybrid.py rank_bm25_documents、reciprocal_rank_fusion、RRF_K 提供中英运维文本 token 化、BM25L 排名和确定性 RRF。 apps/backend/src/super_ai/api/app.py _milvus_readiness_payload、文档与索引路由 把向量存储接到 API、后台任务和安全 readiness。 📷 [图片 token=F1EKbzOOFopP7sxflbTcQOkXnrc（未能下载，见飞书原文）]\n代码调用流程图 图中上半部分是 Compose 基础设施依赖，下半部分是应用侧索引与检索链路。二者通过 Milvus 的连接和 collection 边界汇合。\n📷 [图片 token=AhwvbenrDo1VBpxuIaTccj7Zn9c（未能下载，见飞书原文）]\n关键实现拆解 Collection schema 与索引 build_chunk_collection_schema 定义十个字段：主键 chunkId，以及 documentId、knowledgeBaseId、ownerUserId、tenantId、content、source、createdAt、JSON metadata 和 FLOAT_VECTOR vector。vector 的 dimension 来自设置，而不是 schema 常量写死。dynamic field 被禁用，减少意外字段绕过审查。\n📷 [图片 token=OscCbDjN0o2jslx5RZUcmJofnDb（未能下载，见飞书原文）]\nbuild_index_definitions 为 tenant、知识库、owner、文档与时间标量字段生成 AUTOINDEX，再为 vector 使用配置的 HNSW/COSINE 参数。initialize 连接后构造 PyMilvus schema 和 index params；collection 不存在时创建，已存在时确保索引，然后 load collection。它不会在 Python import 阶段创建客户端，pymilvus 也通过 import_module 在构造真实 schema 时才加载。\n📷 [图片 token=LYsNbN3r6o21WUxYWbxciM8pn0g（未能下载，见飞书原文）]\n看什么：collection 把权限范围放在顶层标量，并让向量维度直接来自当前设置。\nfields=( MilvusFieldDefinition(CHUNK_ID_FIELD, \u0026#34;VARCHAR\u0026#34;, is_primary=True, max_length=128), MilvusFieldDefinition(DOCUMENT_ID_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=128), MilvusFieldDefinition(KNOWLEDGE_BASE_ID_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=128), # 1. owner、tenant、知识库和文档都是可过滤标量。 MilvusFieldDefinition(OWNER_USER_ID_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=128), MilvusFieldDefinition(TENANT_ID_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=128), MilvusFieldDefinition(CONTENT_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=65535), MilvusFieldDefinition(SOURCE_FIELD, \u0026#34;VARCHAR\u0026#34;, max_length=1024), MilvusFieldDefinition(CREATED_AT_FIELD, \u0026#34;INT64\u0026#34;), MilvusFieldDefinition(METADATA_FIELD, \u0026#34;JSON\u0026#34;), # 2. 向量维度由配置决定，不在 schema 中写死。 MilvusFieldDefinition( VECTOR_FIELD, \u0026#34;FLOAT_VECTOR\u0026#34;, dimension=settings.vector_dimension, ), ) 📷 [图片 token=D4zNb858Po5LewxK7xwcsIdqnSd（未能下载，见飞书原文）]\n代码证明 Milvus 只保存知识 chunk 及其权限标量，而不是用户、聊天或诊断表。维度不匹配会在写入边界失败；dynamic field 关闭后，新增字段必须通过 schema 变更显式进入审查，不能依赖任意 metadata 绕过顶层过滤。\n📷 [图片 token=BeZ0bLmMto9JAvxJrQ4cqEPHnrg（未能下载，见飞书原文）]\n看什么：初始化分支对缺失和已存在 collection 采取不同动作，但两者最终都确保索引并 load。\n初始化不会从应用代码启动 Milvus 服务，只针对已可达的 Compose 依赖创建或复用 collection。连接、schema 构造、索引或 load 任一步失败都会阻断写入，不能被标记为索引成功。\n📷 [图片 token=FYEkb4rKVo32iFxkQ1scZ6C1nqh（未能下载，见飞书原文）]\n写入与重建语义 chunk_document_text 支持固定字符、Markdown 标题、段落和兼容旧文档的 word 策略。每个 DocumentChunk 有稳定序号、正文、起止位置和可选 heading path。_vector_chunk_record 用文档 ID 和四位 chunk 序号构造稳定 chunk ID，并在 metadata 加入切分策略、参数、知识类型和完整 owner/tenant 标识。\n📷 [图片 token=KqshbvUNQopHK4xG7gVcWRLGn3g（未能下载，见飞书原文）]\nMilvusVectorStore._chunk_to_entity 在写入前检查向量长度等于配置维度，再把权限字段既写入顶层标量，也更新到 metadata。重建不是直接追加：服务先初始化，再调用 delete_document_chunks 删除同 tenant、知识库和文档的旧向量，最后插入新批次。若 tenant、知识库或文档 ID 为空，删除方法在连接 Milvus 前抛错，防止退化成大范围清理。\n📷 [图片 token=K7ezbZnKIonc7MxheeYcVXNfn8f（未能下载，见飞书原文）]\nSQLite 与 Milvus 没有一个跨系统原子事务。当前顺序能保证业务任务记录先存在，并让失败可见、可重试，但如果外部进程在删除旧向量与插入新向量之间终止，向量索引可能暂时为空。durable job、失败状态和手动重建是当前恢复机制；不能声称两库强一致。\n📷 [图片 token=P9hAbr6aHoCgtFxRtvocQOMGnyh（未能下载，见飞书原文）]\n看什么：在 Milvus 重建之前，服务已得到全部 chunk 和 vectors，并严格检查数量对应。\n# 1. 一条 chunk 必须精确对应一条向量。 if len(vectors) != len(chunks): raise DocumentIndexingError( \u0026#34;Embedding provider returned an unexpected vector count.\u0026#34; ) self._vector_store.initialize() # 2. 先删除同 tenant、知识库、文档的旧向量。 self._vector_store.delete_document_chunks( tenant_id=owner_user_id, knowledge_base_id=document.knowledge_base_id, document_id=document.id, ) # 3. 使用 strict zip 防止静默截断。 self._vector_store.insert_chunks( [ _vector_chunk_record( document=document, tenant_id=owner_user_id, chunk=chunk, vector=vector, ) for chunk, vector in zip(chunks, vectors, strict=True) ] ) 📷 [图片 token=USqSbTz44ogGTvx2swecISFCnFg（未能下载，见飞书原文）]\n片段证明重建是文档范围的替换，而非无界追加，并且向量与 chunk 不会静默错位。当前一致性边界仍不是事务：删除成功而插入前崩溃会暂时留下空范围；SQLite 任务失败、durable retry 与确定性 chunk ID 是恢复手段。\n📷 [图片 token=PLlTb82OWorz0RxYdBGcrch1nod（未能下载，见飞书原文）]\n看什么：下面的时序图区分 SQLite 业务事实与 Milvus 派生索引，重点看非原子窗口。\n图中只有最后一次 SQLite 更新能证明完整流程结束。embedding 或 Milvus 失败时要保存安全原因；任务再次运行时按同一文档范围重建，而不是把旧失败任务改写成从未失败。\n📷 [图片 token=VRH2bo4zDo0mRTxdEJDc2462nYg（未能下载，见飞书原文）]\n范围搜索、标量枚举与混合召回 search_chunks 要求 query vector、tenant ID、允许知识库 IDs 和 limit。它把 build_milvus_tenant_filter 生成的表达式交给 Milvus search，指定 COSINE 与搜索参数，并只返回检索所需字段。list_chunks 使用同样 filter，通过每批 1000 的 query iterator 读取标量；输出字段不包含 vector，因为 BM25 只需要正文和标识。iterator 无论成功失败都在 finally 中关闭。\n📷 [图片 token=Hc6ebTf6HozpnwxzHULcWOybn3c（未能下载，见飞书原文）]\nKnowledgeRetrievalTool 对 Milvus 返回结果再做一层 owner、tenant、知识库、可选文档和 metadata 检查，形成纵深防御。向量候选与 BM25L 候选以 RRF_K = 60 融合，再调用 rerank。无候选时返回空结果，不生成“可能相关”的虚假 chunk；Milvus、embedding 或关键词召回异常转换为 SYSTEM_UNAVAILABLE，rerank 失败也以明确的临时不可用错误结束。\n📷 [图片 token=Ncw8bGrwKoqRoyxzPgnc5zV4nuf（未能下载，见飞书原文）]\n看什么：向量 search 对空知识库集合先短路，非空时才连接并把 tenant filter 传给 Milvus。\n# 1. 空授权范围不连接、不发起无范围搜索。 if not knowledge_base_ids: return [] client = self._connection_manager.connect() search_result = client.search( collection_name=self._settings.collection_name, data=[list(query_vector)], anns_field=VECTOR_FIELD, filter=build_milvus_tenant_filter( tenant_id=tenant_id, knowledge_base_ids=knowledge_base_ids, ), limit=limit, # 2. metric 与 search params 来自类型化配置。 search_params={ \u0026#34;metric_type\u0026#34;: self._settings.metric_type, \u0026#34;params\u0026#34;: dict(self._settings.search_params), }, # 3. 只取检索所需标量，不回读 vector。 output_fields=list(OUTPUT_FIELDS), timeout=self._settings.timeout_seconds, ) 📷 [图片 token=NfbmbbaJgoXtQUxLEdpc572knsc（未能下载，见飞书原文）]\n代码证明向量召回必须有明确 tenant 与允许知识库集合，空集合不是“搜索全部”的别名。返回后检索工具还会校验 owner 和 metadata；任何粗召回分支失败都会成为安全系统错误，不静默使用另一分支补齐。\n📷 [图片 token=Tjjibjpp1oYzxYxBp7UcfDt5nyc（未能下载，见飞书原文）]\n看什么：混合召回的两个分支共享相同授权范围，但读取的数据和评分方式不同。\n空结果不会生成回退文档内容；向量、枚举、BM25 或 rerank 任一步异常也不会伪造精排分数。BM25 枚举虽然分批读取，仍会收集 tenant 范围语料，性能优化必须保留同一 filter 与空范围短路。\n📷 [图片 token=Cn9PbJSwWo2Br0xhY2vc207HnIe（未能下载，见飞书原文）]\n显式连接与 readiness MilvusConnectionManager.connect 在首次调用时才用 settings 创建 MilvusClient，后续复用。build_default_milvus_vector_store 只加载配置并创建边界对象。由此 create_app 可以完成装配而不在启动导入阶段访问网络，单元测试也能注入 fake client。\n📷 [图片 token=Rx8OboF9TouJh0xCowwco6m4nCd（未能下载，见飞书原文）]\nhealth_check 通过列出 collections 判断连接并返回 URI、collection、延迟和错误字段。API 层 _milvus_readiness_payload 在线程池运行同步检查；异常或不健康结果对外统一为“Milvus is unavailable”，不把传输内部异常放进 /ready。/health 不探测 Milvus，所以可用于判断后端进程存活；/ready 才代表依赖聚合状态。\n📷 [图片 token=No7Hbr4PSofESnxSfOxchHs2nib（未能下载，见飞书原文）]\n看什么：连接管理器只有在 connect 被显式调用时才创建客户端，并缓存同一个实例。\ndef connect(self) -\u0026gt; MilvusClientProtocol: \u0026#34;\u0026#34;\u0026#34;Create the client on first explicit use and return it.\u0026#34;\u0026#34;\u0026#34; # 1. import 或 store 构造阶段都不会创建客户端。 if self._client is None: self._client = self._client_factory(self._settings) return self._client # … 省略与本节无关的代码 def health_check(self) -\u0026gt; MilvusHealthCheckResult: \u0026#34;\u0026#34;\u0026#34;Return a readiness result without leaking transport internals.\u0026#34;\u0026#34;\u0026#34; started_at = monotonic() try: client = self._connection_manager.connect() # 2. readiness 通过真实轻量调用验证连接。 client.list_collections(timeout=self._settings.timeout_seconds) except Exception as exc: return MilvusHealthCheckResult( ok=False, uri=self._settings.uri, collection_name=self._settings.collection_name, latency_ms=_elapsed_ms(started_at), error=_safe_error_message(exc), ) 📷 [图片 token=XeTmbxDgkocm6Jx65ZfcaSJUnYf（未能下载，见飞书原文）]\n片段证明“对象已装配”不等于“Milvus 已连接”，并让导入测试可以断言 pymilvus 尚未加载。health_check 内部保留有界诊断，API 对外再归一化安全消息；/health 不调用它，不能据此宣称向量依赖 ready。\n📷 [图片 token=XAG1bbBblo8XWExGPx4cPxFgnnd（未能下载，见飞书原文）]\n基础设施生命周期与数据保留 Compose 为 etcd、MinIO、Milvus 和 Alertmanager 声明命名卷，因此普通 down 不等于删除数据；down -v 才会移除卷，是需要谨慎执行的本地重置动作。Milvus 使用 etcd 保存元数据、MinIO 保存对象数据，自身服务健康依赖二者。只看到 19530 端口打开不足以判断完整可用，Compose healthcheck 与后端 /ready 分别从容器和应用视角提供证据。\n📷 [图片 token=PCRjb0JkqoQgHbxQPN7cz8nfn0r（未能下载，见飞书原文）]\nAttu 依赖 Milvus healthy 后启动，但 Attu 可访问不代表后端配置指向同一 collection，也不代表 embedding 与 rerank 可用。Alertmanager 与 Milvus 同处 Compose 只是本地运维便利，它们没有数据耦合：前者提供告警入口，后者提供知识检索。把五个容器视为一个“全栈应用”会掩盖宿主机前端、后端、MCP 和远端模型仍需单独检查。\n📷 [图片 token=R4ODbezcDoAyWjxD0JpcU1gonST（未能下载，见飞书原文）]\n看什么：Compose 只定义基础设施依赖，Milvus 等 etcd 和 MinIO healthy 后启动；这里没有 backend 或 frontend 服务。\nmilvus: image: milvusdb/milvus:v3.0-beta command: [\u0026#34;milvus\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;standalone\u0026#34;] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin ports: - \u0026#34;19530:19530\u0026#34; - \u0026#34;9091:9091\u0026#34; # 1. named volume 让普通 down 不删除 Milvus 数据。 volumes: - milvus-data:/var/lib/milvus # 2. 服务启动依赖两个有状态组件的健康检查。 depends_on: etcd: condition: service_healthy minio: condition: service_healthy 📷 [图片 token=IPREbZxonoM0jqxxTGVcFiuxnsf（未能下载，见飞书原文）]\n代码证明本地 Compose 拓扑和数据保留边界：普通停止与删除卷不是同一操作，down -v 才是破坏性重置。默认基础设施凭据只适合本地开发；宿主机 FastAPI、Vue 和 CLS MCP 仍需单独启动、配置和检查。\n📷 [图片 token=H73xbp48Zot9avxBcQDcia0pnWh（未能下载，见飞书原文）]\n看什么：容器健康、应用 readiness 和模型能力是三层证据，不应由 Attu 页面或一个端口互相替代。\nAttu 能打开只证明管理界面与某个 Milvus 可通信，不证明后端 collection 配置、embedding 或 rerank 正常。Alertmanager 与向量库同处 Compose 也不产生数据耦合，排障时应沿实际业务链逐层取证。\n📷 [图片 token=Penvbr7LToXGDLxFi28cSkfmnIg（未能下载，见飞书原文）]\n索引任务的恢复观察 创建文档索引 API 返回 202 时，含义是业务任务与 durable job 已接受，不是向量已经可检索。界面应观察 DocumentIndexTask.status 和文档 indexStatus，只有 succeeded/indexed 才表示完整写入流程结束。pending 或 running 可由 worker 继续处理，failed 保存原因并允许显式 retry，cancelled 表示取消路径。\n📷 [图片 token=OkTWbRFbVoJONLxstJtcrpfInEh（未能下载，见飞书原文）]\n任务失败后，SQLite 中的上传文档与可索引文本仍存在，所以重试不要求重新上传。重试创建新的索引任务并保留 retry_of_task_id，而不是把旧失败记录改成成功。这个审计链能区分多次尝试。若文档已删除或不属于当前 owner，Repository 拒绝任务，worker 不应从 Milvus 猜测其存在。\n📷 [图片 token=SZvrberYqoy0KjxFFOFc5UYgnIC（未能下载，见飞书原文）]\n看什么：恢复图同时查看业务索引任务和 durable job，防止只根据一个状态推断另一侧已经完成。\n📷 [图片 token=B0pJbZOM2oQtTixlHiUcDpmPn8f（未能下载，见飞书原文）]\n图中重试是新记录，不会抹去旧 attempt；服务重启后 queued 或租约过期 job 可再次领取。若最初 Repository 状态更新失败，业务任务可能停在中间态，因此排障要同时检查 background job 的 attempt、租约和安全错误。\n📷 [图片 token=GCFDbHtDTolN6ExnON1ckVfCngc（未能下载，见飞书原文）]\n检索可解释性来自阶段数据 向量 search 的 score、BM25L 的 score、RRF score 和 rerank relevance 含义不同，不能直接横向比较。KnowledgeRetrievalHit 同时保存 vectorRank、bm25Rank、rerankRank 及各阶段分数；最终兼容字段 score 等于 rerankScore。前端引用视图据此展示候选如何从两路召回进入融合与精排，而不是只给出一个来源不明的“相关度”。\n📷 [图片 token=I8qDb7VaYoyidQx5ASycpe6rn6f（未能下载，见飞书原文）]\n关键词召回从 Milvus 枚举当前 tenant 的标量 chunk 到内存构建 BM25L，这意味着语料越大，读取成本越需要关注；当前实现通过 iterator 分批读取但仍会收集范围内 chunks。优化时可以改变存储或索引策略，但必须保留 tenant filter、空范围短路和同一引用字段语义。性能优化不能把全 collection 缓存成跨用户共享语料。\n📷 [图片 token=WaE0beHzIoVKs1xzZghc3vxrnQb（未能下载，见飞书原文）]\n看什么：最终命中把三个阶段的 rank/score 分开保存，并让兼容 score 明确等于 rerank 分数。\ndef _hit_from_fused_candidate( candidate: _FusedCandidate, *, rerank_rank: int, rerank_score: float ) -\u0026gt; KnowledgeRetrievalHit: result = candidate.chunk return KnowledgeRetrievalHit( chunk_id=result.chunk_id, document_id=result.document_id, knowledge_base_id=result.knowledge_base_id, owner_user_id=result.owner_user_id, tenant_id=result.tenant_id, content=result.content, source=result.source, metadata=result.metadata, # 1. 兼容 score 只表示最终 rerank 分数。 score=rerank_score, vector_rank=candidate.vector_rank, bm25_rank=candidate.bm25_rank, rerank_rank=rerank_rank, vector_score=candidate.vector_score, bm25_score=candidate.bm25_score, rrf_score=candidate.rrf_score, rerank_score=rerank_score, ) 📷 [图片 token=D4HqbwKWYoXMNgxDzpTc23mPnjf（未能下载，见飞书原文）]\n代码证明向量、BM25、RRF 和 rerank 不是一个可混用的分数轴，单路未命中时相应字段允许为空。引用从同一个 hit 派生，保持 chunk 与各阶段排名一致；rerank 失败时不能回填其他分数伪装最终相关度。\n📷 [图片 token=RB3tbsFusocUEvxcLpFcsaZynyc（未能下载，见飞书原文）]\n看什么：阶段图解释同一 chunk 如何携带可空的粗召回排名，再获得唯一最终排名。\n某个 chunk 可只来自一条粗召回分支，所以未命中一侧保留空值而不是零分。前端应展示字段来源，不能把不同算法分数直接比较或合成一个未经声明的“置信度”。\n📷 [图片 token=SSvNbNnfpoCE3dxpPavcpRcInWf（未能下载，见飞书原文）]\n运维检查的最小顺序 遇到知识检索不可用时，可先检查 Compose 服务状态，再访问后端 /ready 看 Milvus 组件；如果 Milvus ready 而索引失败，再检查 embedding 与具体 document task；若索引成功但检索为空，核对当前用户知识库范围、文档 indexStatus、collection 和查询 filter；若已有候选但最终失败，再检查 rerank。按调用链排查比直接在 Attu 中手工改数据更安全。\n📷 [图片 token=RG9CbpYXnoHZ7VxkhCkcqoX8ncf（未能下载，见飞书原文）]\n手工操作 collection 可能绕过确定性 chunk ID、权限 metadata 和 SQLite 状态，因此不属于正常文档管理流程。需要重建时应使用受保护的索引任务 API，让服务先做 owner 校验并同步状态。真实文档上传、SOP 索引、日志上传和告警发布也都保持显式动作，启动基础设施本身不会制造演示数据。\n📷 [图片 token=HbmDbRCQyoQUDQxDZB9czchInwd（未能下载，见飞书原文）]\n看什么：最小排障路径从无侵入的基础设施证据逐层进入业务状态，最后才检查 rerank。\n这条顺序避免一开始就在 Attu 中改数据，从而绕过 owner metadata、确定性 chunk ID 和 SQLite 状态。启动基础设施不会自动上传真实文档、SOP、日志或告警；这些仍需授权用户显式操作并留下业务记录。\n📷 [图片 token=VFlvbciGDo3ERNxCdnbcyzgfnjg（未能下载，见飞书原文）]\n数据、契约与状态 Milvus 顶层标量中的 ownerUserId 和 tenantId 当前通常都等于认证用户 ID，但两者都保留是为了明确所有权与查询范围语义。knowledgeBaseId 和 documentId 进一步收窄检索与删除。JSON metadata 存放 chunk 边界、heading、知识类型和切分参数，同时复制权限标识供下游契约使用。\n📷 [图片 token=Hsd5bDaINoa8lyxF8TzcLtBLn0e（未能下载，见飞书原文）]\n业务文档与任务状态在 apps/backend/src/super_ai/memory/models.py 中。KnowledgeDocumentModel 保存文件名、大小、MIME、哈希、status、index_status、来源、metadata 和软删除时间；DocumentIndexTaskModel 保存 pending、running、succeeded、failed 或 cancelled、失败原因、重试来源与时间。向量库中没有这些完整状态，因此列表页面和恢复逻辑必须读 SQLite。\n📷 [图片 token=NphMb5R4vo7QopxIljqclihdnxe（未能下载，见飞书原文）]\npackages/api-contracts/src/vector.ts 在前端共享层也定义 VectorChunkMetadata 与 tenant filter builder，packages/api-contracts/src/documents.ts 和 packages/api-contracts/src/indexing.ts 定义文档与任务 DTO。真正执行 Milvus filter 的是后端 Python；TypeScript helper 用于保持字段语义和测试一致，不能被误解为浏览器侧权限控制。\n📷 [图片 token=UsZSbXuSKoE8Aix1QzMcJTuMncd（未能下载，见飞书原文）]\n权限、安全与失败边界 没有可访问知识库时，KnowledgeRetrievalTool、search_chunks 与 list_chunks 都有空范围短路。请求指定了不在授权集合中的知识库时，_resolve_knowledge_base_ids 返回 AUTH_FORBIDDEN，不调用 embedding 或 Milvus。删除则必须同时提供三个非空范围字段。任何为了“默认搜索全部”而省略集合的行为都与当前安全边界冲突。\n📷 [图片 token=Gbf6bc9fRot0z9xiLHYclkHhnPf（未能下载，见飞书原文）]\nMilvus 只保存知识 chunk 向量，不用于存储用户、聊天、诊断证据、工具审计或 MCP 配置。Attu 是本地管理界面，不是最终用户权限层；能访问本机 Attu 的开发者可能直接查看 collection，因此其端口与主机访问也属于开发环境安全范围。Compose 中的本地默认基础设施凭据不应被当作生产安全方案。\n📷 [图片 token=R1opbtlU5oQMfXxWzDpcc9k4nY0（未能下载，见飞书原文）]\n外部依赖失败必须留下真实状态。Milvus 不可用时 readiness 降级，索引任务失败且可重试，检索返回结构化不可用错误；embedding 不可用时不应写入空向量；rerank 不可用时不应伪造最终分数。启动流程不会自动上传真实 CLS 日志、发布告警或索引文档，这些只能由开发者显式执行。\n📷 [图片 token=SeKsbHWJtoQur1xV0o3ccYUrnAe（未能下载，见飞书原文）]\n阅读顺序与小结 先读 infra/compose.yaml 和 infra/README.md，画出容器与宿主机边界。\n阅读 apps/backend/src/super_ai/vector_store/config.py 与 apps/backend/src/super_ai/vector_store/schema.py，掌握静态设置和 collection 结构。\n逐方法阅读 MilvusVectorStore，重点看 connect、空范围、filter、output fields 和删除。\n沿 DocumentIndexingService 追踪写入，沿 KnowledgeRetrievalTool 追踪读取。\n最后沿 Compose、Milvus、索引与 tenant filter 串起完整路径，核对失败和越权边界。\n本地基础设施的设计重点是边界清楚：Compose 管有状态基础设施，宿主机运行应用；SQLite 保存业务事实，Milvus 保存可重建的知识 chunk 向量；模型和向量服务都按需连接且允许失败；每一次向量操作都携带 tenant 与知识范围。掌握这些边界后，才能安全地优化 chunk、索引或召回，而不会用性能改动破坏数据隔离与可恢复性。\n📷 [图片 token=MdY0b4QcVo5yHTxwhSTcaGkungh（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/05.%20%E6%9C%AC%E5%9C%B0%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E4%B8%8E%20Milvus%20%E5%90%91%E9%87%8F%E8%BE%B9%E7%95%8C/","summary":"OncallAgent 的本地运行拓扑刻意把“容器基础设施”和“应用进程”分开。 infra/compose.yaml  只运行 etcd、MinIO、Milvus、Attu 与 Alertmanager；FastAPI 后端、Vue 前端","title":"05. 本地基础设施与 Milvus 向量边界"},{"content":"真正有区分度的技术表达，不是背出 proposal、design、tasks 几个文件名，而是能解释它们为什么分开、怎样约束 Codex、如何与代码和测试互相证明，以及归档后如何保留决策历史。下面的问题都以 OncallAgent 当前仓库为依据，回答时可以先给结论，再根据追问补充案例和边界。\n📷 [图片 token=FQdVbBNWto4sOtxDxIccLq2Mnkh（未能下载，见飞书原文）]\n先讲清 OpenSpec 的位置 1. 已经有 Git、Issue 和测试，为什么还需要 OpenSpec？ **回答：**它们记录的是不同维度。Git 擅长回答“哪些文件发生了变化”；Issue 更适合讨论、分工和状态跟踪；测试证明部分可执行行为是否成立。它们通常不会完整保存一次变更的动机、非目标、行为契约、技术取舍和实施顺序。\nOpenSpec 把这些信息组织成同仓库的 change。proposal 说明为什么做和影响范围，delta spec 定义必须满足的行为，design 记录实现决策，tasks 管理执行与验证。它不是替代 Git、Issue 或测试，而是把三者之间缺少的“变更语义”补齐。\n📷 [图片 token=UsUtbpQMHolyyRxatXqcngn5nAc（未能下载，见飞书原文）]\n不要说：“用了 OpenSpec 就不需要写测试或提交记录。”更准确的说法是，OpenSpec 负责定义和追踪变化，Git 保存代码历史，测试提供验证证据。\n📷 [图片 token=VoLRbk1RZohJWBxsx6EcsqBxn4e（未能下载，见飞书原文）]\n2. 哪些改动应该先创建 change？ **回答：**OncallAgent 的仓库规则要求，新增功能、用户可观察行为变化和非平凡缺陷修复，先创建或继续一个聚焦的 OpenSpec change。纯文档、仓库元信息或不改变行为的机械调整可以直接处理，除非任务明确要求走 OpenSpec。\n判断重点不是“改了多少行”，而是“是否改变了系统承诺”。一个只改五行代码、却改变权限或 SSE 事件语义的修复，仍然需要规格；一次大规模但完全机械的格式调整，反而未必需要。\n📷 [图片 token=If4ibLNRroEKCLxm1YbcqcIXn0b（未能下载，见飞书原文）]\n3. Codex、OpenSpec 和开发者分别负责什么？ **回答：**OpenSpec 提供结构化上下文和验收边界；Codex 负责阅读仓库、生成或调整 artifacts、实施代码、运行检查并报告证据；开发者负责目标、范围、取舍和最终批准。AI 可以加快分析和执行，但不能替代方向判断。\n在当前本地 Skills 中，Codex 还应读取 OpenSpec CLI 返回的 planningHome、changeRoot、artifactPaths 和 contextFiles，不能凭经验猜测文件位置。这个细节说明 OpenSpec 不只是几份模板，而是一张由 schema 和 artifact graph 驱动的工作图。\n📷 [图片 token=HNjibGDIAoG1LfxPf58c2ufPnLc（未能下载，见飞书原文）]\n4. OpenSpec 是瀑布开发吗？ **回答：**不是。它要求在编码前先收敛关键不确定性，但 artifacts 可以随着新证据更新。apply 阶段如果发现 design 不成立、Scenario 遗漏或任务粒度不合理，应暂停实现，先修正对应产物，再继续工作。\n与瀑布式“文档签字后冻结”不同，OpenSpec 更接近可追踪的增量决策：允许变化，但要求变化留下记录，并重新建立规格、实现和验证之间的一致性。\n📷 [图片 token=FLSBbAEvAol32IxyxiOcJiEznhd（未能下载，见飞书原文）]\n为什么要拆成多种产物 5. proposal.md 解决什么问题，为什么不能直接写 design.md？ **回答：**proposal 先回答“为什么值得做、准备改变什么、涉及哪些能力、会影响哪些边界”。当前仓库常见结构是 Why、What Changes、Capabilities 和 Impact。它让审查者先判断问题与范围是否正确。\ndesign 回答的是“怎样实现”。如果一开始只讨论类、接口和数据库，很容易在错误的问题上做出漂亮方案。把两者分开，能先控制方向，再优化路径。\n📷 [图片 token=A9LbbXbkDoMOa3xcUDAcublbndd（未能下载，见飞书原文）]\n6. delta spec 与普通需求描述有什么区别？ **回答：**普通需求常停留在“增加某功能”。delta spec 以 capability 为组织单位，用 Requirement 和 Scenario 写出可观察行为，并明确这次是 ADDED、MODIFIED、REMOVED 还是 RENAMED。\n例如，“展示检索排名”还不够验收。更完整的 Scenario 会说明：当引用只在 BM25 一路召回时，向量排名和分数必须为空，前端必须显示“未召回”，不得伪造为第 0 名或 0 分。这样的描述才能稳定映射到契约、实现和测试。\n📷 [图片 token=Pf7TbMEFpoK8SJxFiKEcMwUznu5（未能下载，见飞书原文）]\n7. design.md 应该写什么？ **回答：**design 记录上下文、目标与非目标、关键决策、替代方案、风险和迁移策略。最有价值的内容不是“修改哪些文件”，而是“为什么由这一层承担责任”。\n在 Embedding 批量限制案例中，设计选择把 chunk_size=10 放进 Provider，而不是让文档索引服务理解厂商上限。这样所有调用方共享同一安全行为，业务层仍然提交完整文本列表。这个“责任归属”的解释，比文件清单更能体现工程判断。\n📷 [图片 token=Di3NbEKCDo3SJfx2FqFc6DqQngh（未能下载，见飞书原文）]\n8. tasks.md 的价值是什么？ **回答：**tasks 把规格和设计转换成有顺序、可执行、可验证的工作单元。合格任务会明确产出或检查，例如“扩展共享引用契约并覆盖旧消息兼容测试”，而不是笼统写“完成后端开发”。\napply 会以未完成 checkbox 作为进度入口，但 checkbox 只是状态声明。任务越多并不代表越专业，关键是每项能够独立判断是否完成，并覆盖代码、测试和验证。\n📷 [图片 token=F9lwb7wJZoMDp2xc3nhc5LP5nFH（未能下载，见飞书原文）]\n9. .openspec.yaml 与 openspec/config.yaml 有什么区别？ **回答：**change 目录中的 .openspec.yaml 是变更级元数据，当前归档案例通常记录 schema: spec-driven 和创建时间，并随整个 change 一起归档。根目录的 openspec/config.yaml 是仓库级 OpenSpec 配置，声明默认 schema，并可提供项目上下文和 artifact 规则。\n两者都不是应用运行配置，也不应该保存 API Key、数据库地址或云凭据。\n📷 [图片 token=DnH6bJqqNoLQwkxVMKfckdX5njd（未能下载，见飞书原文）]\n规格怎样与代码建立关系 10. delta spec 和 main spec 为什么要分开？ 回答：openspec/changes/\u0026lt;change\u0026gt;/specs/ 表达“这次准备改变什么”，openspec/specs/ 表达“系统当前已经承诺什么”。分开后，未完成方案不会提前污染主规格，审查者也能清楚看到本次增量。\n实现和验证完成后，sync 将 delta 智能合入 main spec。change 仍可保持 active，直到完成归档。两者可能包含相同 Requirement 的部分内容，但职责和生命周期不同。\n📷 [图片 token=Eg0zbOOKqoBZFNxauGucBuWZngh（未能下载，见飞书原文）]\n11. Requirement、Scenario 和测试是什么关系？ **回答：**Requirement 定义能力必须满足的行为，Scenario 给出触发条件和预期结果，测试则是证明这些行为成立的可执行证据之一。一条 Scenario 不一定机械对应一个测试函数，一个测试也可能覆盖多个边界。\nverify 应从 Scenario 向实现和测试追踪，也应从关键代码反查它兑现了哪条 Requirement。写了 Scenario 不等于已经测试，测试通过也不自动证明所有 Requirement 都有覆盖。\n📷 [图片 token=G52hbn5VwoOPwWxA39actC4EnZb（未能下载，见飞书原文）]\n12. 如何证明追踪链不是形式主义？ **回答：**要能给出一条可以逐层定位的证据链。例如 Embedding 案例中：proposal 说明单次最多 10 条文本的问题；delta spec 定义小批量、大批量拆分和完整索引场景；design 决定在 Provider 设置批量上限；tasks 安排实现和两层测试；代码定义批量常量；测试验证 11 条输入拆成 10+1 且顺序保持；main spec 保存已生效行为；archive 与 WIKI 保存历史。\n只有目录里存在几份 Markdown 文件不算追踪。每一层都需要回答上一层提出的问题，并能在下一层找到证据。\n📷 [图片 token=KcsIbPTziokKNKxgRVTcLVl5nqd（未能下载，见飞书原文）]\n13. 为什么共享契约变化要先改 packages/api-contracts？ **回答：**这是 OncallAgent 的仓库约束：HTTP envelope、错误码、DTO、OpenAPI 路径和 SSE payload 以 packages/api-contracts 为唯一事实来源。API 或 SSE 变化先更新共享契约，再同步后端序列化、前端消费和双方测试。\nOpenSpec 在这里发挥的是边界检查作用。proposal 的 Impact、delta spec 的契约 Scenario、design 的兼容策略和 tasks 的实施顺序共同防止前后端各自复制一套类型。\n📷 [图片 token=ZQXYbMX7UozpoExmtOBcZFDdnIy（未能下载，见飞书原文）]\n不同工作入口怎样选择 14. new、continue、ff 和 propose 有什么区别？ **回答：**当前本地 Skill 中，new 只建立 change 并给出第一个 artifact 的指引；continue 每次创建一个当前 ready 的 artifact；ff 自己先创建 change，再按依赖顺序快速生成达到 apply-ready 所需的产物；propose 也是一站式创建并生成 apply-ready artifacts，适合边界已经比较清楚的需求。\n因此不要把“先 new，再 ff”写成固定流程：本地 ff 和 propose 都包含新建动作。同名 change 已存在时应继续它，而不是重复创建。\n📷 [图片 token=UZXIbFjrkoKbVtx7fzocImiEnX7（未能下载，见飞书原文）]\n15. Artifact 的顺序可以写死吗？ **回答：**不能跨版本、跨 schema 写死。spec-driven 常见依赖是 proposal 先完成，specs 与 design 随后 ready，tasks 等它们完成后再生成；真正执行时仍应以 openspec status --change ... --json 返回的 done、ready、blocked 和 applyRequires 为准。\n其他 schema 可能使用不同 artifact 名称。成熟的做法是读取状态图和 instructions，而不是把某篇教程的目录结构当成所有项目的永恒规则。\n📷 [图片 token=PKIObeD3boJVapxx6hscZVgen7g（未能下载，见飞书原文）]\n16. apply 只是按 tasks.md 写代码吗？ **回答：**不是。当前 apply Skill 先读取 status，再读取 openspec instructions apply 返回的全部 contextFiles。在 spec-driven change 中通常包括 proposal、delta specs、design 和 tasks，然后才逐项实施并更新 checkbox。\n如果实现暴露了设计缺口，apply 可以暂停并回到 artifacts；如果任务需要 API、SSE、迁移或 tenant 边界，还必须遵守仓库对应规则。只读 tasks 而忽略规格与设计，会把执行清单误当成完整需求。\n📷 [图片 token=HLe3bgaPyoB26Fx250HcLdX8n8c（未能下载，见飞书原文）]\n完成、同步和归档意味着什么 17. tasks 全部勾选，是否等于变更完成？ **回答：**不等于。全部 [x] 只表示任务文件记录为完成，仍需检查 Requirement 是否有实现、Scenario 是否有测试证据、代码是否遵守 design、真实验证命令是否运行成功。\n历史归档里的勾选也只能说明当时记录的状态，不能当成本轮环境重新验证的证据。OncallAgent 明确要求只报告实际运行并通过的命令，没有执行的检查必须如实说明。\n📷 [图片 token=O8FMbxg4BoyWGaxEjyAcpeo2n8e（未能下载，见飞书原文）]\n18. verify 与“把测试跑绿”有什么区别？ **回答：**verify 从完整性、正确性和一致性三个维度审查。完整性关注 artifacts 与 tasks 是否完整；正确性把 Requirement 和 Scenario 映射到实现及测试；一致性检查代码是否兑现 design 并遵守仓库既有模式。\n测试是重要证据，但不是全部证据。测试可能遗漏场景，代码也可能虽然通过测试却绕过共享契约或 tenant scope。反过来，verify 中的搜索和推断也不是数学证明，关键结论仍要用真实代码、测试结果和运行证据支持。\n📷 [图片 token=P0Wqbg2hFol6LtxllLZcw984n2f（未能下载，见飞书原文）]\n19. sync-specs 是复制粘贴吗？ **回答：**不是。当前 Skill 把它定义为 agent-driven 的智能合并：ADDED 新增 Requirement，MODIFIED 只修改目标内容并保留未提及场景，REMOVED 删除已废弃行为，RENAMED 处理名称变化。如果 capability 不存在，才创建新的 main spec。\n同步应尽量保持幂等，重复执行不应不断产生相同内容。sync 完成后 change 仍然 active，它只更新当前事实，不负责归档历史。\n📷 [图片 token=BO1CbmuEtoZ89sxix4AcUV4An0g（未能下载，见飞书原文）]\n20. archive 是删除还是 Git 回滚？ **回答：**都不是。archive 在检查 artifact、tasks 和 delta 同步状态后，把整个 change 移入 openspec/changes/archive/YYYY-MM-DD-\u0026lt;change-name\u0026gt;/，proposal、design、tasks、delta specs 和 .openspec.yaml 都被保留。\n归档目录用于审计和追溯，不应继续开发。要修改已经生效的行为，应建立新的 change；要撤销代码，应通过 Git 回滚或反向变更处理，而不是改写历史档案。\n📷 [图片 token=QyEcby1tmorQwSxD9T1cLXVon6b（未能下载，见飞书原文）]\n21. wiki-sync 是 OpenSpec 自带功能吗？ **回答：**不是。它是 OncallAgent 仓库自定义的 VitePress 同步 Skill，也不是飞书同步。它为 active/archive change 生成 docs/changes/.../index.md，通过 @include 引用 OpenSpec 原文件，并维护总索引与 Sidebar。\n脚本还校验符号链接、include 目标、页面集合、导航顺序以及归档 delta 与 main specs 的同步状态。脚本本身不执行 npm run docs:build，归档同步后仍需单独构建。OpenSpec archive 与 wiki-sync 是两个动作。\n📷 [图片 token=TOvwbSgfCoEDDwxAZMNcCA8Unab（未能下载，见飞书原文）]\n版本差异与最终表达 22. 为什么不能不加判断地照搬网页教程？ **回答：**OncallAgent 当前本地 OpenSpec Skills 标记为 generatedBy: 1.5.0实际协作应以仓库中的 .codex/skills/openspec-*、AGENTS.md、当前 openspec --help 和 status/instructions 输出为准。\n📷 [图片 token=BLnCblbRVonankxZLoyc815kn7b（未能下载，见飞书原文）]\n23. 面试中怎样用一分钟讲清楚？ 我在 OncallAgent 中把 OpenSpec 当作变更控制面，而不是文档生成器。需求明确后先用 proposal 对齐价值和范围，再用 delta spec 把行为写成可验收 Scenario，用 design 记录责任归属和取舍，用 tasks 驱动代码、测试与验证。实施完成后，我会从完整性、正确性和一致性验证规格与代码，随后把 delta 智能合入主规格，将完整 change 按日期归档。仓库自定义的 wiki-sync 再通过符号链接和 VitePress include 生成可浏览历史，因此 Git 记录“改了什么”，OpenSpec 解释“为什么这样改、怎样证明改对了”。\n如果继续追问，应立即落到真实案例，不要继续堆概念。小案例可以讲 Qwen Embedding 的 10 条批量上限；跨层案例可以讲三阶段检索排名如何贯穿后端、共享契约、SSE、前端和测试。\n📷 [图片 token=HGIBbt8QNoDxUFxACjTcYDzenEf（未能下载，见飞书原文）]\n回答时的三个原则 **先讲边界，再讲能力。**明确 OpenSpec 不替代 Git、测试和工程判断，可信度会高于夸大自动化能力。\n**用证据链代替术语堆叠。**至少能指出一个 proposal、一个 Scenario、一项 design 决策、一条实现路径和一组测试。\n**区分历史记录与当前验证。**归档 tasks 的勾选属于历史；只有本轮真实执行的命令和结果，才能作为当前验证证据。\n📷 [图片 token=ZT0tbWuUIoMd3UxQbepcBF2onNe（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%E9%AB%98%E9%A2%91%E8%BF%BD%E9%97%AE%E4%B8%8E%E5%9B%9E%E7%AD%94/","summary":"真正有区分度的技术表达，不是背出 proposal、design、tasks 几个文件名，而是能解释它们为什么分开、怎样约束 Codex、如何与代码和测试互相证明，以及归档后如何保留决策历史。下面的问题都以 OncallAgent 当前仓库","title":"OpenSpec高频追问与回答"},{"content":"proposal.md 是一次变更的入口文件。它不是详细技术设计，也不是产品宣传稿，而是用最短路径建立四个共识：为什么现在要做、具体改变什么、涉及哪些长期能力、影响边界在哪里。\n如果 proposal 没有把问题说清楚，后面的 spec、design 和 tasks 会在错误前提上越写越完整。AI 编程最危险的情况不是生成速度慢，而是方向错误时仍然高速产出。proposal 的价值就是在成本最低的阶段暴露方向性问题。\n📷 [图片 token=NUVabGiXHoCDJWxhurNccMXynxe（未能下载，见飞书原文）]\n推荐组织结构 ## Why ## What Changes ## Capabilities ### New Capabilities ### Modified Capabilities ## Impact 📷 [图片 token=OoTnbcz9foF9VcxxHxlcSCNsn9g（未能下载，见飞书原文）]\nWhy：解释问题与时机 Why 应描述当前可复现的问题、限制或机会，以及不处理会造成什么后果。它需要给出足够上下文，让不了解这次对话的人也能理解动机，但不应提前展开类名和具体代码。\n主案例的 Why 很具体：百炼 text-embedding-v4 单次最多接受 10 条文本，而索引服务会把全部 chunk 交给默认 Embedding 客户端；文档产生 11 个以上 chunk 时会在向量生成阶段收到 HTTP 400，索引任务失败。这段话包含外部约束、当前行为、触发条件和用户可见后果，因果链完整。\n📷 [图片 token=SKLdbItI5oI37Bx5gqqcY3ptnHg（未能下载，见飞书原文）]\nWhat Changes：说明将发生的变化 这一节写“做完后系统有什么不同”，通常用几条有边界的变化描述。主案例写了三件事：客户端单批限制为 10；更大的输入自动分批并保持输入输出顺序；增加超过上限的回归测试。它没有在这里决定常量放在哪个函数，因为那属于 design。\n📷 [图片 token=TafzbGDgEoCDJexn67gcj3cunoc（未能下载，见飞书原文）]\nCapabilities：把 change 映射到长期规格 Capability 是主规格的逻辑分组，也是 delta spec 的目录名。New Capabilities 表示系统以前没有的长期能力；Modified Capabilities 表示修改已有能力。主案例没有新增产品能力，而是修改 qwen-openai-provider 的兼容性约束，所以 New 写“无”，Modified 指向现有 capability。\n这个区分很重要。如果把 change 名直接当 capability，例如为每个 Bug 都创建一个永久规格目录，主规格会迅速变成按工单组织的历史堆积，而不是按系统能力组织的当前事实。\n📷 [图片 token=IXMpbVHnioDEHuxEX0ncjEf2nHt（未能下载，见飞书原文）]\nImpact：明确修改面和不修改面 Impact 应指出可能涉及的代码、契约、存储、测试、迁移和用户界面。主案例明确影响后端 Embedding 客户端构造、Provider 单测和索引回归测试；同时明确不修改 HTTP API、SSE 契约、Milvus schema 和前端行为。\n“不修改什么”同样是重要信息。它让 Codex 不会为了修一个 Provider 兼容性问题，顺手重构前端或协议层，也让评审者能快速发现实际 diff 是否越界。\n📷 [图片 token=N2LnbObwWoNtmWxzSFjcYx2snid（未能下载，见飞书原文）]\n为什么不能用 design 代替 proposal proposal 讨论的是意图和范围，design 讨论的是实现方法。如果一开始就写“在 OpenAIEmbeddings 构造时增加 chunk_size=10”，看似具体，却跳过了几个必要判断：为什么是 10、谁受到影响、顺序是否必须保持、业务层应不应该感知、接口和存储是否需要变化。\n先确认 proposal，意味着即使技术方案后来改变，问题定义和验收边界仍然稳定。技术方案可以从 LangChain 参数调整为自定义适配器，但“每个请求最多 10 条、任意输入都返回完整且有序的向量”这一目标不应丢失。\n📷 [图片 token=OF1DbrUhuoCK1FxZTc4cHeeHnFg（未能下载，见飞书原文）]\n怎样判断 proposal 写得好不好 **问题是否可证实。**避免“体验不好”“需要优化”这类无法判断的描述。应说明触发条件、现有结果和期望结果。\n**范围是否单一。**一个 change 应围绕一个可解释目标。修 Embedding 拆批时，不应顺便更换向量库、重做切分算法或设计新前端。\n**能力命名是否长期稳定。**Capability 应描述系统边界，例如 qwen-openai-provider，而不是一次性的任务名。\n**Impact 是否与仓库真实结构对应。**OncallAgent 有共享 HTTP/SSE 契约、FastAPI 后端、Vue 前端、SQLite、Milvus 和用户级 MCP 等边界。涉及哪一层应如实列出，不涉及也应明确排除。\n📷 [图片 token=Fik5bMD1goj3c9x2ZMtc8MBNn1g（未能下载，见飞书原文）]\n常见反例 把 proposal 写成口号。“提升系统稳定性和用户体验”没有说明什么会改变，也无法生成可靠的 spec。\n**提前锁死实现。**proposal 中堆类名、函数名和伪代码，会让意图与方案耦合。实现细节应放进 design。\n**只写要做什么，不写不做什么。**没有 Non-Goals 或 Impact 边界时，AI 很容易顺手扩大范围。\n**Capability 与 change 混淆。**change 是一次修改，capability 是长期系统能力；归档 change 后，capability 仍留在主规格中。\n📷 [图片 token=RKNObsTmgoFwzjxHX1rcFQ1inWd（未能下载，见飞书原文）]\n面试表达 我把 proposal 当成变更的立项边界，而不是技术方案。它先把问题、变化、能力归属和影响面讲清楚。以 Embedding 批量兼容为例，proposal 明确故障来自 10 条上限，目标是透明拆批并保持顺序，同时排除 HTTP/SSE、Milvus 和前端变化。这样 design 可以专注决定约束放在哪一层，代码评审也能用 Impact 检查有没有越界。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/proposal.md%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"proposal.md  是一次变更的入口文件。它不是详细技术设计，也不是产品宣传稿，而是用最短路径建立四个共识：为什么现在要做、具体改变什么、涉及哪些长期能力、影响边界在哪里。 如果 proposal 没有把问题说清楚，后面的 spec、","title":"proposal.md文件作用介绍"},{"content":"你有没有遇到过这种情况。\n让 AI 帮你整理电脑里的文件，它给你写出一整套整理方案，分类规则写得头头是道，但最后还是得你自己一个个打开文件夹、手动移动；让它帮你查今天的天气，它说「我的知识截止到某年某月，无法获取实时天气」；让它帮你订明天的机票，它只能告诉你去哪个网站、怎么操作，然后你默默打开了去哪儿网。\n这不是大模型不够聪明。大模型非常聪明，但它天生就被关在一个「小盒子」里，它只能接收文字、输出文字，它的「手」根本伸不出来，没有办法和外部世界产生任何交互。\n这就引出了我们这章的主角：Tool（工具）。Tool 就是给大模型「装上手」的方案，让它从「只动嘴的参谋」，变成「能落地干活的实干家」。\n一、Agent + Tool：两个角色怎么配合干活 在聊 Tool 之前，先把另一个容易混淆的概念说清楚：Agent。\n很多同学以为「Agent 就是更厉害的大模型」。这个理解偏了。Agent 本质上是我们开发者写的一套程序，它不是更聪明的 AI，而是一个全程在线的「调度员」，负责在用户、大模型、工具之间协调任务、传递信息、推进流程。\n用一个外卖平台来类比这三个角色的关系：\n📷 [图片 token=EYGfbTQ3WoQfWGxteXac4q62nsd（未能下载，见飞书原文）]\n你（用户）：下单的人，提需求\n大模型：后厨主厨，负责决策，先做哪道菜、下一步做什么、什么时候上桌\nAgent：外卖平台的调度系统，协调主厨、骑手和顾客，确保整个流程跑通\nTool：骑手和各个执行部门，真正出去跑腿、干具体事情的\n大模型只「动脑」，工具才「动手」。Agent 就是把这两者打通的中间层。\n具体来说，当你给 Agent 下一条指令：「帮我读取 C 盘目录下的 hello_world.cpp 文件，移动到 D 盘目录下，最后给我总结一下这个文件的核心内容」，整个执行流程是这样的：\n📷 [图片 token=WpFRbJO7eoQVJix9PNPcn4iJn4e（未能下载，见飞书原文）]\nAgent 把需求 + 工具清单打包，发给大模型：「用户想做这件事，你有这些工具可以用，第一步该怎么做？」\n大模型做决策，返回指令：「调用【读取文件】工具，路径是 C://hello_world.cpp」\nAgent 执行指令，调用工具：真正去磁盘上读文件\nAgent 把结果回传给大模型：「文件读取成功，内容是……」\n大模型根据结果，决定下一步：「好，现在调用【移动文件】工具，把它移到 D 盘」\n循环执行，直到任务完成：所有步骤跑完，大模型生成最终总结\nAgent 把结果反馈给你：任务完成\n你看，这整个过程里，「决策」始终在大模型，「执行」始终在工具，而 Agent 就是那个来回传话、推进任务的协调员。\n二、Tool 到底是什么？光写函数不够？ 好，现在聚焦到工具本身。\n你可能会想：「工具不就是函数嘛，我写一个 Python 函数，Agent 直接调用就行了？」\n听起来很合理，但这里有个关键问题，大模型根本看不到你的 Python 代码。\n大模型不是程序员，它不会去扫你的代码库，发现「哦你这里有个 get_weather 函数」。它能读的，只有你放进它对话上下文里的文字。\n所以，哪怕你把函数写得多优雅，只要没有专门告诉大模型「这个函数叫什么、干嘛用的、参数怎么传」，大模型就永远不知道这个工具的存在。\n结论：要让大模型用上工具，必须额外给它写一份「说明书」。\n函数是「手」，说明书才是「让大模型学会用这只手的指南」。两者缺一不可。\n这份说明书由四个部分组成，就是 Tool 的四要素。\n三、Tool 的四要素 📷 [图片 token=BX30bOdQmojT7mx1TswcbKFrnId（未能下载，见飞书原文）]\n函数本体，真正干活的代码 这是实现具体功能的代码，查数据库就是查数据库，发通知就是发通知。\n这部分大模型看不到，但是 Agent 最终会执行它。大模型只负责「决策调用哪个工具、传什么参数」，至于这个工具内部怎么实现，大模型完全不关心。\nname（名称），大模型的识别标签 大模型决定「调哪个工具」的时候，第一步靠的就是名称。名字要望名知意。\nget_weather 一眼就知道是「查天气」；func_001 就算加了再多描述，大模型也会更难联想到它的用途。起名的原则很简单：让大模型看名字就能猜出大概。\ndescription（描述），整个工具定义里最重要的字段 没有之一。\n大模型每轮决策的核心问题是：「当前这一步，该不该调这个工具？」它做这个判断的唯一依据，就是工具的 description。\n描述写清楚了，大模型准确选工具；描述写模糊了，大模型要么选错工具，要么该用的时候没用，或者不该用的时候乱用。\n一个好的 description 应该包含三件事：\n能做什么：这个工具的核心功能是什么\n什么场景用：遇到哪类问题、哪种情况该选它\n返回什么：调用完会得到哪些信息\nparameters（参数定义），告诉大模型怎么「填参数」 有了函数，大模型还得知道：调用这个工具要传哪些参数、每个参数是什么类型、哪些是必填的。\n这些信息用 JSON Schema 格式来定义。\nJSON Schema 听起来很技术，但你可以把它理解成网站上的一张「填写表单」：\n你在网上买东西填收货地址的时候，表单会告诉你，「姓名」「地址」「手机号」是必填项（标了 * 号），「备注」是选填的；「手机号」只能填数字，不能填文字。\nJSON Schema 做的就是同样的事：定义这个工具有哪些「格子」需要填、每个格子填什么类型的数据、哪些格子必须填。大模型看到这份定义，就知道该怎么构造调用指令了。\n用一个完整的例子把这四个部分串起来。假设我们要给 Agent 提供一个查询天气的工具：\n先写函数本体\n# ===== 第一部分：函数本体（大模型看不到，Agent 负责执行）===== import requests def get_weather(city: str, date: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;调用天气 API，查询指定城市指定日期的天气\u0026#34;\u0026#34;\u0026#34; response = requests.get( \u0026#34;https://api.weather.com/v1/forecast\u0026#34;, params={\u0026#34;city\u0026#34;: city, \u0026#34;date\u0026#34;: date} ) data = response.json() return { \u0026#34;city\u0026#34;: city, \u0026#34;date\u0026#34;: date, \u0026#34;weather\u0026#34;: data[\u0026#34;condition\u0026#34;], # 晴/多云/雨 \u0026#34;temperature\u0026#34;: data[\u0026#34;temp_c\u0026#34;], # 摄氏度 \u0026#34;humidity\u0026#34;: data[\u0026#34;humidity\u0026#34;] # 湿度百分比 } 这段代码没什么特别的，就是调了一个天气 API，把结果整理成字典返回。注意：大模型看不到这段代码，它只负责决定「要调这个工具、传什么参数」，至于你用什么 API、代码怎么写，大模型完全不关心。\n再写工具说明书\n函数有了，接下来写大模型能看到的部分，name + description + parameters：\n// ===== 第二三四部分：工具说明书（大模型看到的部分，靠这个决定要不要调用）===== { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询指定城市在指定日期的天气情况。当用户询问某地天气、出行建议、是否需要带伞等问题时使用此工具，返回天气状况、气温和湿度信息。\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;要查询天气的城市名称，例如：北京、上海、广州\u0026#34; }, \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询日期，格式：YYYY-MM-DD，例如：2025-03-21\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;, \u0026#34;date\u0026#34;] } } 来逐行拆解这份说明书：\nname** 字段**：get_weather，望名知意，大模型一看就猜到这是查天气的工具。\ndescription** 字段**：这是最关键的部分。注意它写的不是「这是一个天气 API」，而是写清楚了使用场景：「当用户询问某地天气、出行建议、是否需要带伞等问题时使用」。这就是在告诉大模型：你遇到这类问题，就来用我。如果只写「查询天气」，大模型在面对「我明天去杭州要带伞吗」这种问法时，可能就不确定该不该调用了。\nparameters** 字段**：定义了两个参数，city（城市名，字符串类型）和 date（日期，字符串类型），两个都在 required 里，也就是「必填格子」，大模型构造调用指令时不会漏填。每个参数的 description 里还给了例子（「北京、上海、广州」「2025-03-21」），这是个好习惯，大模型按什么格式填参数，全靠这里的说明。\n这就是函数本体和工具说明书的完整对照。用户问「明天上海天气怎么样」，大模型读完说明书，就知道：该调 get_weather，city 填「上海」，date 填明天的日期。\n四、大模型怎么「认识」这些工具？ 工具写好了，下一个问题：大模型怎么知道我有哪些工具？\n答案是：你得主动告诉它。\nAgent 在每次对话开始前，会把所有可用工具的 name + description + parameters 打包成一个列表，通过 API 的 tools 参数传给大模型。这个列表就叫工具清单。\n大模型每次做决策，实际上是在读这份清单，然后判断：「当前任务需要什么操作？哪个工具能完成？该怎么调用？」\n📷 [图片 token=XypMbuKTUoRKNjxfjr5cnqiNnOg（未能下载，见飞书原文）]\n用一个场景帮你理解：你入职一家新公司，第一天 HR 给你发了一份《内部系统清单》，\nOA 系统：用来提交请假申请、报销单据。当你需要请假或者报销时使用，填写「开始日期」「结束日期」「事由」。\n代码仓库（GitLab）：用来提交代码、发起 MR。当你完成功能开发需要合并代码时使用，填写「分支名」「目标分支」。\n监控平台：用来查看服务状态、报警记录。当排查线上问题时使用，填写「服务名」「时间范围」。\n拿到这份清单，你自然就知道：「哦，我下周要请假，得用 OA 系统，填开始日期和结束日期」。\n大模型收到工具清单，完全是同样的逻辑，它遇到用户的问题，就在清单里找「哪个工具能处理这个场景」，然后按照 parameters 定义填好参数，告诉 Agent 去调用。\n这里有一个结论很重要，要记牢：\n📷 [图片 token=Vbasb9ZEbogL7ox4ofRc2lDan5c（未能下载，见飞书原文）]\n大模型选对工具的能力，不只取决于模型有多聪明，更取决于工具描述写得有多清晰。\n描述太模糊、两个工具描述太相似、工具太多但没有区分场景，这些问题，换再强的模型也没用。根本原因在工具描述本身写得不够好。这就是为什么前面把 description 列为「最重要的字段」。\n工具清单越丰富、描述越清晰，Agent 能干的事越多、越准确。\n五、工具有哪些类型？风险从低到高 工具不是只有一种，按照「对系统的影响程度」，从低风险到高风险分成四类。\n这个「风险从低到高」的顺序不是随意排的，它决定了每类工具在 Agent 里该怎么用、要不要加人工审批。\n查询类（只读） 查天气、搜索网页、读取文件、查数据库记录……只读取数据，不改变任何状态。查完数据库，数据库还是那个数据库；读完文件，文件没有任何变化。\n风险等级：低。 这类工具可以放心让 Agent 自主调用，不需要人工确认。出错了顶多是拿到没用的数据，不会产生不可逆的后果。\n写入类（有副作用） 往数据库插记录、发通知消息、发邮件、更新配置……会改变系统状态，而且有些操作不可逆，发出去的通知收不回来，写进数据库的记录再删也留了痕迹。\n风险等级：中。 对高风险的写入操作（比如发通知给几百个人、修改线上配置），建议加一个人工确认步骤，而不是让 Agent 完全自主执行。\n执行类（高风险） 执行 shell 命令、跑脚本、重启服务、部署代码……能直接操作系统。一条错误的 shell 命令能删掉整个目录，一次错误部署能把线上服务打挂。这类工具的影响范围最大，破坏性最强。\n风险等级：高。 两件事必须做到：严格限制权限范围（比如只允许执行白名单里的命令）；重要操作必须走人工审批，绝对不能让 Agent 自主决策。\nAI 辅助类 这是很多同学没想到的一类：AI 能力本身也能封装成工具。\n比如，给 Agent 配一个 RAG 检索工具，让它能随时查内部知识库、历史故障案例；或者把一个专门的分类模型封装成工具，Agent 调用它对日志做分类，只拿结论，不需要自己处理所有细节。\n风险等级：低到中。 操作本身不会改变外部系统状态，主要关注检索结果的质量和子模型的可靠性。\n这四类工具，风险依次递增，设计 Agent 系统时，每个工具的风险等级直接决定了两件事：它能不能自主调用？需不需要人工确认？ 这是保证 Agent 安全可靠运行的基本原则，不要等出了问题再加。\n总结 整理一下这章的核心认知：\n工具是 Agent 的「双手」，没有工具，Agent 只是一个只会输出文字的大模型；有了工具，Agent 才能真正和外部世界交互、完成具体任务。\n一个工具 = 函数本体 + 名称 + 描述 + 参数定义。大模型看不到函数本体，它靠 name + description + parameters 来判断该不该用这个工具、怎么用。其中 description 最关键，是大模型选工具的唯一依据。\n四类工具，风险从低到高：\nnhLI9k 类型 典型场景 风险 Agent 能自主调用？ 查询类 查天气、读文件、搜索网页 低 可以 写入类 发通知、改配置、写数据库 中 高风险操作需人工确认 执行类 跑脚本、重启服务、部署代码 高 必须人工审批 AI 辅助类 RAG 检索、调用子模型 低～中 通常可以，关注结果质量 工具描述的清晰度决定 Agent 的准确率，不是换更聪明的模型，是把 description 写清楚。\n后面几章会继续深入这条链路：\nFunction Calling：大模型是怎么把「我要调哪个工具、传什么参数」这个决策，用标准化格式告诉 Agent 的，这是工具调用的底层机制，这章我们只是看到了结果，下一章讲清楚原理\nRAG：把知识库检索封装成工具，让 Agent 能访问私有数据，重要到单独一章\nMCP：工具的标准化协议，让你不用每次都从头写工具，直接接入现成的工具生态\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%20Tool%EF%BC%9F/","summary":"你有没有遇到过这种情况。  让 AI 帮你整理电脑里的文件，它给你写出一整套整理方案，分类规则写得头头是道，但最后还是得你自己一个个打开文件夹、手动移动；让它帮你查今天的天气，它说「我的知识截止到某年某月，无法获取实时天气」；让它帮你订明天","title":"什么是 Tool？"},{"content":"在运维Agent替代人工处理告警排查的需求中，传统人工方式存在四大痛点，而Plan-Execute-Replan模式正是为解决这些问题而生：\n流程无结构化导致效率低下：人工排查依赖经验，新人易步骤混乱（如告警时不知先查日志还是监控）\n动态异常应对能力弱：人工遇到无结果场景（如日志无异常）易停滞\n数据孤岛跨系统联动麻烦：人工需在日志、监控、告警群间切换\nPlan-Execute-Replan模式完美匹配运维Agent的需求特性：\n结构化规划解决流程混乱：Planner将模糊需求转化为清晰步骤，对应人工步骤无序的痛点。\n动态调整应对异常场景：Replanner的反馈环可实时校准方向，解决人工停滞问题。\n组件协同打破数据孤岛：Executor统一调用工具，实现跨系统联动，对应人工跨平台切换的低效。\n此模式通过规划→执行→调整的闭环，让运维Agent像资深工程师一样高效处理告警，成为替代人工的核心方案。\n📷 [图片 token=CLXrbKsJPoB7SSxrsQtcuTiAn1c（未能下载，见飞书原文）]\nPlan-Execute-Replan 是什么？ 先给定义：Plan-Execute-Replan 是 Agent 的结构化任务执行模式\n核心是先规划执行步骤，再按步骤行动，随时校准方向，通过规划→执行→评估→调整，让 Agent 像项目经理一样拆解复杂任务、稳步推进，还能应对突发变化。\n大白话解释：装修房子的全流程 想象你要装修一套新房，完全没经验的话，你会怎么避免手忙脚乱？\nPlan（规划）：先找设计师出详细方案，拆改哪里、水电怎么走、用什么材料、分几个阶段施工（比如拆旧→水电→泥工→木工→油漆），形成清晰的步骤清单。\nExecute（执行）：施工队按计划开工，先拆旧、再布水电，一步一步推进当前阶段的任务。\nReplan（重规划）：施工中发现原计划的承重墙不能拆，则设计师重新调整布局（比如把书房门改到另一侧），更新计划后继续施工，直到最终完工。\nPlan-Execute-Replan 就是让 Agent 模仿这个过程：遇到复杂任务不盲目动手，而是先设计方案，再按图施工，遇到问题时灵活调整方案，确保最终达成目标。\n技术场景例子：运维Agent的故障排查实战 假设某电商平台服务器凌晨突发**CPU使用率100%**告警，需要运维Agent自动排查根因。这是典型的多步骤运维任务，正好适合 Plan-Execute-Replan 模式：\nPlan：拆解排查步骤 Planner（规划器）接到排查CPU突增根因的目标后，结合运维经验生成结构化计划：\n步骤1：调用日志工具，查询服务器近1小时error/warn级别日志（重点看进程崩溃、资源争抢记录） 步骤2：调用监控工具，获取CPU使用率突增时段的进程占用排行（定位高耗CPU进程） 步骤3：调用历史工单手册，检索该进程过往CPU异常的处理方案（匹配已知问题） Execute：执行第一步查日志 Executor（执行器）按计划启动第一步，调用日志工具，参数为服务器IP=10.0.1.5，时间范围=近1小时，日志级别=error/warn，返回结果：\n日志中未发现error/warn记录，仅存在大量info级别的定时任务执行成功日志（无异常线索） Replan：评估结果并调整计划 Replanner（重规划器）分析执行结果：日志无异常，说明问题可能不在应用错误，需优先定位高耗CPU进程，于是调整计划顺序：\n【更新后计划】 步骤1：调用监控工具，获取CPU使用率突增时段（02:00-02:10）的进程占用排行（优先定位异常进程） 步骤2：调用日志工具，查询步骤1中高耗CPU进程的近1小时详细日志（针对性排查该进程行为） 步骤3：调用历史工单手册，检索该进程过往CPU异常的处理方案 继续执行与动态调整 Executor 执行更新后的步骤1，调用监控工具（如Prometheus），返回结果：\nCPU突增时段（02:00-02:10），进程「data-sync-service」占用率达95%（正常时段通常\u0026lt;10%） Replanner 再次评估 已定位异常进程，需进一步查该进程日志确认原因，计划无需调整，继续执行步骤2。\n执行查进程日志 Executor 调用日志工具，参数更新为进程名=data-sync-service，时间范围=近1小时，返回结果：\n日志显示02:00触发全量数据同步任务，遍历数据库1000万条记录，未做分页处理 Replanner 最终评估 已明确根因：全量同步任务未分页导致CPU过载，无需继续执行步骤3（因问题已定位，且历史工单中类似场景解决方案明确），终止任务并返回结论。\n最终输出结果 故障根因：服务器进程data-sync-service在02:00执行全量数据同步时，未做分页处理，遍历1000万条记录导致CPU使用率突增。 建议方案：优化同步逻辑，添加分页参数（如每次拉取1000条），并设置非高峰时段执行。 核心组件：三个智能角色的协作 就像装修需要 设计师+施工队+监理 ，Plan-Execute-Replan 也靠三个核心智能体（Agent）协同：\nPlanner（规划器）：任务拆解\n作用：把用户目标拆成可执行的步骤清单，确保每个步骤清晰、有序。\n关键能力：理解复杂目标的内在逻辑，生成结构化计划（类似项目甘特图）。\nExecutor（执行器）：步骤行动\n作用：严格执行计划中的当前第一步，调用工具（数据库、计算器、API 等）完成具体任务，返回执行结果。\n关键能力：准确调用工具、处理单步任务（不负责整体规划，只专注做好眼前事）。\nReplanner（重规划器）：进度监理\n作用：评估 Executor 的执行结果，判断是否需要调整计划\n若步骤完成且结果有效：推进到下一个步骤。 若结果缺失/错误（如数据不全、工具调用失败）：修改计划（补充步骤、调整顺序）。 若所有步骤完成：终止任务，返回最终结果。 关键能力：判断任务进度、识别执行问题、动态优化计划。\n工作流程：从「目标」到「结果」的闭环 Plan-Execute-Replan 的核心逻辑是「结构化推进+动态校准」，流程如下：\n📷 [图片 token=JC6nbtFSioe4KFxe9mycVkwMnGh（未能下载，见飞书原文）]\n与 ReAct 的核心区别：先规划 vs 边想边做 对比维度 Plan-Execute-Replan ReAct 核心思路 结构化计划（先拆步骤，按计划执行） 实时决策（边想边做，无固定步骤） 适用场景 复杂流程类任务（如报告生成、项目管理） 灵活探索类任务（如问答、解谜） 步骤特点 提前规划步骤序列（可动态调整） 动态生成下一步（无预设顺序） 优势 任务进度可控、步骤清晰有序 灵活应对未知情况 核心优势：让复杂任务可控又灵活 结构化拆解：把一团乱麻的复杂任务拆成步步可执行的步骤，降低认知负荷。\n动态适应变化：遇到执行问题（如工具故障、数据缺失）时，Replan 机制能及时调整计划，避免一条道走到黑。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9APlan-Execute-Replan%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/","summary":"在运维Agent替代人工处理告警排查的需求中，传统人工方式存在四大痛点，而Plan-Execute-Replan模式正是为解决这些问题而生：   1.  流程无结构化导致效率低下 ：人工排查依赖经验，新人易步骤混乱（如告警时不知先查日志还是","title":"架构设计：Plan-Execute-Replan设计模式核心原理"},{"content":" 📷 [图片 token=XYaXbYPnloRNw3x0LC1cRYmqnFq（未能下载，见飞书原文）]\n📷 [图片 token=Sj6cbg0z2oDT5RxCyAscyO8AnIg（未能下载，见飞书原文）]\n前言 本节我们来实现知识库Agent的上半部分，即将文件向量化后存储到数据库中。\n这部分代码在：app/services/vector_index_service.py\n📷 [图片 token=ZuIYbmkHfoSPmCxS1R5cFC59nid（未能下载，见飞书原文）]\n流程梳理 我们的目标是将文件向量化后存储到数据库中，具体步骤如下：\n读取文件\n切分文件\n索引（Embedding 和存储）\n读取文件 我们直接传入文件路径 file_path，使用 pathlib.Path 读取文件内容到内存：\n# index_single_file函数读取文件内容 content = path.read_text(encoding=\u0026#34;utf-8\u0026#34;) index_single_file 是索引单个文件的入口方法，完整实现如下：\ndef index_single_file(self, file_path: str): \u0026#34;\u0026#34;\u0026#34; 索引单个文件 (使用新的 LangChain 分割器) Args: file_path: 文件路径 Raises: ValueError: 文件不存在时抛出 RuntimeError: 索引失败时抛出 \u0026#34;\u0026#34;\u0026#34; path = Path(file_path).resolve() if not path.exists() or not path.is_file(): raise ValueError(f\u0026#34;文件不存在: {file_path}\u0026#34;) logger.info(f\u0026#34;开始索引文件: {path}\u0026#34;) try: # 1. 读取文件内容 content = path.read_text(encoding=\u0026#34;utf-8\u0026#34;) logger.info(f\u0026#34;读取文件: {path}, 内容长度: {len(content)} 字符\u0026#34;) # 2. 删除该文件的旧数据（如果存在） normalized_path = path.as_posix() vector_store_manager.delete_by_source(normalized_path) # 3. 使用文档分割器切分文档 documents = document_splitter_service.split_document(content, normalized_path) logger.info(f\u0026#34;文档分割完成: {file_path} -\u0026gt; {len(documents)} 个分片\u0026#34;) # 4. 添加文档到向量存储（自动完成 Embedding + 入库） if documents: vector_store_manager.add_documents(documents) logger.info(f\u0026#34;文件索引完成: {file_path}, 共 {len(documents)} 个分片\u0026#34;) else: logger.warning(f\u0026#34;文件内容为空或无法分割: {file_path}\u0026#34;) except Exception as e: logger.error(f\u0026#34;索引文件失败: {file_path}, 错误: {e}\u0026#34;) raise RuntimeError(f\u0026#34;索引文件失败: {e}\u0026#34;) from e 文件分块 文档分块使用 LangChain 提供的分割器，分为三个阶段：\n第一阶段：按 Markdown 标题（#、##）切分，将文档分割成多个章节\n第二阶段：对每个章节使用 RecursiveCharacterTextSplitter 进行二次分割，超过 chunk_size * 2 的章节会被拆分\n第三阶段：合并过小的分片（\u0026lt; 300 字符），避免过度碎片化，同时通过 chunk_overlap 保持分片间的上下文语义连贯\nDocumentSplitterService 初始化时会配置好这两个分割器：\nclass DocumentSplitterService: def __init__(self): self.chunk_size = config.chunk_max_size # 默认 800 self.chunk_overlap = config.chunk_overlap # 默认 100 # 第一阶段：Markdown 标题分割器（按 # 和 ## 切分） self.markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[ (\u0026#34;#\u0026#34;, \u0026#34;h1\u0026#34;), (\u0026#34;##\u0026#34;, \u0026#34;h2\u0026#34;), ], strip_headers=False, # 保留标题在内容中 ) # 第二阶段：递归字符分割器（用于二次分割） self.text_splitter = RecursiveCharacterTextSplitter( chunk_size=self.chunk_size * 2, # 加倍 chunk_size，减少分片数 chunk_overlap=self.chunk_overlap, length_function=len, is_separator_regex=False, ) Markdown 文档完整的三阶段分割逻辑：\ndef split_markdown(self, content: str, file_path: str = \u0026#34;\u0026#34;) -\u0026gt; List[Document]: \u0026#34;\u0026#34;\u0026#34;分割 Markdown 文档 (两阶段分割 + 合并小片段)\u0026#34;\u0026#34;\u0026#34; # 第一阶段：按标题分割 md_docs = self.markdown_splitter.split_text(content) # 第二阶段：按大小进一步分割 docs_after_split = self.text_splitter.split_documents(md_docs) # 第三阶段：合并太小的分片（\u0026lt; 300 字符） final_docs = self._merge_small_chunks(docs_after_split, min_size=300) # 添加文件路径元数据 for doc in final_docs: doc.metadata[\u0026#34;_source\u0026#34;] = file_path doc.metadata[\u0026#34;_extension\u0026#34;] = \u0026#34;.md\u0026#34; doc.metadata[\u0026#34;_file_name\u0026#34;] = Path(file_path).name logger.info(f\u0026#34;Markdown 分割完成: {file_path} -\u0026gt; {len(final_docs)} 个分片\u0026#34;) return final_docs 合并小分片的逻辑（_merge_small_chunks）：遍历所有分片，若当前分片小于 min_size 且合并后不超限，则将其追加到上一个分片中：\ndef _merge_small_chunks(self, documents: List[Document], min_size: int = 300) -\u0026gt; List[Document]: merged_docs = [] current_doc = None for doc in documents: doc_size = len(doc.page_content) if current_doc is None: current_doc = doc elif doc_size \u0026lt; min_size and len(current_doc.page_content) \u0026lt; self.chunk_size * 2: # 当前分片太小且合并后不会太大，则合并 current_doc.page_content += \u0026#34;\\n\\n\u0026#34; + doc.page_content else: # 保存当前文档，开始新文档 merged_docs.append(current_doc) current_doc = doc if current_doc is not None: merged_docs.append(current_doc) return merged_docs 文件索引（向量化和存储到数据库） Embedding 生成 DashScopeEmbeddings 实现了 LangChain 标准的 Embeddings 接口，通过阿里云 DashScope 的 OpenAI 兼容模式调用 text-embedding-v4 模型，生成 1024 维向量：\nclass DashScopeEmbeddings(Embeddings): def __init__(self, api_key: str, model: str = \u0026#34;text-embedding-v4\u0026#34;, dimensions: int = 1024): self.client = OpenAI( api_key=api_key, base_url=\u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34; ) self.model = model self.dimensions = dimensions 批量向量化文档（embed_documents）和单条查询向量化（embed_query）分别对应入库和检索场景：\ndef embed_documents(self, texts: List[str]) -\u0026gt; List[List[float]]: \u0026#34;\u0026#34;\u0026#34;批量嵌入文档列表，返回向量列表\u0026#34;\u0026#34;\u0026#34; response = self.client.embeddings.create( model=self.model, input=texts, dimensions=self.dimensions, encoding_format=\u0026#34;float\u0026#34; ) return [item.embedding for item in response.data] def embed_query(self, text: str) -\u0026gt; List[float]: \u0026#34;\u0026#34;\u0026#34;嵌入单个查询文本，返回单条向量\u0026#34;\u0026#34;\u0026#34; response = self.client.embeddings.create( model=self.model, input=text, dimensions=self.dimensions, encoding_format=\u0026#34;float\u0026#34; ) return response.data[0].embedding 向量存储到 Milvus VectorStoreManager 封装了 langchain_milvus.Milvus，将 LangChain Document 对象直接批量写入 Milvus，字段映射关系如下：\nLangChain 字段 Milvus Collection 字段 说明 page_content content 文本内容 向量（自动计算） vector 1024 维 float 向量 id（UUID） id 主键 metadata metadata JSON 元数据（含 _source、_file_name 等） 初始化时连接 Milvus：\nself.vector_store = Milvus( embedding_function=vector_embedding_service, # 自动调用 embed_documents collection_name=\u0026#34;biz\u0026#34;, connection_args={\u0026#34;host\u0026#34;: config.milvus_host, \u0026#34;port\u0026#34;: config.milvus_port}, auto_id=False, # 使用自定义 UUID drop_old=False, text_field=\u0026#34;content\u0026#34;, vector_field=\u0026#34;vector\u0026#34;, primary_field=\u0026#34;id\u0026#34;, metadata_field=\u0026#34;metadata\u0026#34;, ) 批量入库时，LangChain 会自动调用 embed_documents 完成向量化，无需手动循环处理每个分片：\ndef add_documents(self, documents: List[Document]) -\u0026gt; List[str]: \u0026#34;\u0026#34;\u0026#34;批量添加文档到向量存储（自动批量向量化）\u0026#34;\u0026#34;\u0026#34; import time, uuid start_time = time.time() # 为每个文档生成唯一 UUID（auto_id=False 时必须手动提供） ids = [str(uuid.uuid4()) for _ in documents] # LangChain Milvus 的 add_documents 自动调用 embedding_function 批量向量化并写入 result_ids = self.vector_store.add_documents(documents, ids=ids) elapsed = time.time() - start_time logger.info( f\u0026#34;批量添加 {len(documents)} 个文档完成, \u0026#34; f\u0026#34;耗时: {elapsed:.2f}秒, 平均: {elapsed/len(documents):.2f}秒/个\u0026#34; ) return result_ids 在重新索引同一个文件前，会先按 _source 路径删除旧数据：\ndef delete_by_source(self, file_path: str) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;删除指定文件的所有文档\u0026#34;\u0026#34;\u0026#34; collection = milvus_manager.get_collection() # metadata 是 JSON 字段，使用 JSON 路径查询语法 expr = f\u0026#39;metadata[\u0026#34;_source\u0026#34;] == \u0026#34;{file_path}\u0026#34;\u0026#39; result = collection.delete(expr) deleted_count = result.delete_count if hasattr(result, \u0026#34;delete_count\u0026#34;) else 0 logger.info(f\u0026#34;删除文件旧数据: {file_path}, 删除数量: {deleted_count}\u0026#34;) return deleted_count 总结 到这里，提问前数据准备的三个流程就讲完了。其实代码实现并不难，核心是要搞懂这 3 个步骤里面都做了什么事情，以及代码是怎么将流程串联起来的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ARAG%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%981%28Python%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;XYaXbYPnloRNw3x0LC1cRYmqnFq\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：RAG代码实战1(Python)"},{"content":" 📷 [图片 token=U8wubBj37oDyjoxkw5wccH2Tn3b（未能下载，见飞书原文）]\n📷 [图片 token=IlFpbAZgvor2eYxRxjEcISo6nxb（未能下载，见飞书原文）]\n注意，运行程序之前请先看：\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\n前言 这部分代码在：SuperBizAgent/internal/ai/agent/chat_pipeline\n运行的代码在：SuperBizAgent/internal/ai/cmd/chat_cmd/main.go\n📷 [图片 token=Xy4zbRr9KokA4HxUGYdcSlTAnOd（未能下载，见飞书原文）]\n流程梳理 对话Agent的核心目标是结合外部知识（RAG召回）与工具调用能力（ReAct模式），解决复杂问题。\n整体流程可概括为：\n用户输入 -\u0026gt; embedding -\u0026gt; 向量数据库召回\n构建带上下文(召回的内容)的 prompt\nReAct模式多轮交互\n最终输出答案\n我们继续使用eino的可视化编排插件，来进行流程的编排：\n首先在Goland里面安装eino-dev的插件\n打开插件，按照下图进行编排(或者使用右下角的导入功能，直接导入SuperBizAgent/internal/ai/cmd/chat_cmd/workflow.json)\n{ \u0026#34;name\u0026#34;: \u0026#34;EinoAgent\u0026#34;, \u0026#34;node_trigger_mode\u0026#34;: \u0026#34;AllPredecessor\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*UserMessage\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino/schema\u0026#34; }, \u0026#34;gen_local_state\u0026#34;: { \u0026#34;output_type\u0026#34;: {} }, \u0026#34;id\u0026#34;: \u0026#34;0ijYA1\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Graph\u0026#34;, \u0026#34;nodes\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Start\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 80, \u0026#34;y\u0026#34;: 682 } } }, { \u0026#34;id\u0026#34;: \u0026#34;end\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;end\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;End\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;end\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1988.99, \u0026#34;y\u0026#34;: 476.79 } } }, { \u0026#34;id\u0026#34;: \u0026#34;8AgKo6\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;InputToRag\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;UserMessageToQuery\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_schema\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*UserMessage\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;extra_property\u0026#34;: { \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;has_option\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;interaction_type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;invoke\u0026#34;, \u0026#34;stream\u0026#34;, \u0026#34;collect\u0026#34;, \u0026#34;transform\u0026#34; ] }, \u0026#34;option_package_path\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;option_type_name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;interaction_type\u0026#34;, \u0026#34;has_option\u0026#34; ] }, \u0026#34;extra_property_input\u0026#34;: \u0026#34;{\\\u0026#34;has_option\\\u0026#34;:true,\\\u0026#34;interaction_type\\\u0026#34;:\\\u0026#34;invoke\\\u0026#34;}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: true, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34; }, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 359.58, \u0026#34;y\u0026#34;: 808.45 } }, \u0026#34;node_option\u0026#34;: {} }, { \u0026#34;id\u0026#34;: \u0026#34;A7z9_b\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;ChatTemplate\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;ChatTemplate\u0026#34;, \u0026#34;component_schema\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;chatTemplate\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;ChatTemplate\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino/components/prompt\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;map[string]any\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;[]*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino/blob/main/components/prompt/chat_template.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;FormatType\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;schema.FormatType\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;uint8\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;FormatType\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;Config\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;{\\\u0026#34;FormatType\\\u0026#34;:1}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34; }, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 947.68, \u0026#34;y\u0026#34;: 516.3 } }, \u0026#34;node_option\u0026#34;: {} }, { \u0026#34;id\u0026#34;: \u0026#34;CDNTqO\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;ReactAgent\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;ReAct Agent\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_schema\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;react\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino/flow/agent/react\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;[]*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;slots\u0026#34;: [ { \u0026#34;component\u0026#34;: \u0026#34;ChatModel\u0026#34;, \u0026#34;field_loc_path\u0026#34;: \u0026#34;Model\u0026#34;, \u0026#34;multiple\u0026#34;: false, \u0026#34;required\u0026#34;: false, \u0026#34;component_items\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;openai\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;ChatModel\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;[]*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*schema.Message\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/blob/main/components/model/openai/chatmodel.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;APIKey\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;APIVersion\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;BaseURL\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ByAzure\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;FrequencyPenalty\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;LogitBias\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;additionalProperties\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;map[string]int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;map\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;MaxTokens\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Model\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;PresencePenalty\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;ResponseFormat\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;Type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.0.0-20250106073650-ed838398894a\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;openai.ChatCompletionResponseFormatType\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;Type\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.0.0-20250106073650-ed838398894a\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;openai.ChatCompletionResponseFormat\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Seed\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Stop\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;[]string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;slice\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Temperature\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Timeout\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;time\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;time\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;time.Duration\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int64\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;TopP\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;float32\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;User\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: true } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;ByAzure\u0026#34;, \u0026#34;BaseURL\u0026#34;, \u0026#34;APIVersion\u0026#34;, \u0026#34;APIKey\u0026#34;, \u0026#34;Timeout\u0026#34;, \u0026#34;Model\u0026#34;, \u0026#34;MaxTokens\u0026#34;, \u0026#34;Temperature\u0026#34;, \u0026#34;TopP\u0026#34;, \u0026#34;Stop\u0026#34;, \u0026#34;PresencePenalty\u0026#34;, \u0026#34;ResponseFormat\u0026#34;, \u0026#34;Seed\u0026#34;, \u0026#34;FrequencyPenalty\u0026#34;, \u0026#34;LogitBias\u0026#34;, \u0026#34;User\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;openai.ChatModelConfig\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;SwlhKV\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1715.05, \u0026#34;y\u0026#34;: 585.92 } } } ], \u0026#34;go_definition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.3.4\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino/components/model\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;model.ChatModel\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;interface\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, { \u0026#34;component\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;field_loc_path\u0026#34;: \u0026#34;ToolsConfig.Tools\u0026#34;, \u0026#34;multiple\u0026#34;: true, \u0026#34;required\u0026#34;: true, \u0026#34;component_items\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;duckduckgo\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/blob/main/components/tool/duckduckgo/search.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;DDGConfig\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;Cache\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Headers\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;additionalProperties\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;map[string]string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;map\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;MaxRetries\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Proxy\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Timeout\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;time\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;time\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;time.Duration\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int64\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;Headers\u0026#34;, \u0026#34;Proxy\u0026#34;, \u0026#34;Timeout\u0026#34;, \u0026#34;Cache\u0026#34;, \u0026#34;MaxRetries\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;ddgsearch.Config\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;MaxResults\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Region\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;ddgsearch.Region\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;SafeSearch\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;ddgsearch.SafeSearch\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;TimeRange\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;ddgsearch.TimeRange\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ToolDesc\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ToolName\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;ToolName\u0026#34;, \u0026#34;ToolDesc\u0026#34;, \u0026#34;Region\u0026#34;, \u0026#34;MaxResults\u0026#34;, \u0026#34;SafeSearch\u0026#34;, \u0026#34;TimeRange\u0026#34;, \u0026#34;DDGConfig\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;duckduckgo.Config\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;{}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;Hw6TpP\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1706.34, \u0026#34;y\u0026#34;: 789.11 } } }, { \u0026#34;name\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;extra_property\u0026#34;: { \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;interaction_type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;invoke\u0026#34;, \u0026#34;stream\u0026#34; ] } }, \u0026#34;required\u0026#34;: [ \u0026#34;interaction_type\u0026#34; ] }, \u0026#34;extra_property_input\u0026#34;: \u0026#34;{\\\u0026#34;interaction_type\\\u0026#34;:\\\u0026#34;invoke\\\u0026#34;}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;vrHZ5I\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1357.18, \u0026#34;y\u0026#34;: 788.41 } } }, { \u0026#34;name\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;extra_property\u0026#34;: { \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;interaction_type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;invoke\u0026#34;, \u0026#34;stream\u0026#34; ] } }, \u0026#34;required\u0026#34;: [ \u0026#34;interaction_type\u0026#34; ] }, \u0026#34;extra_property_input\u0026#34;: \u0026#34;{\\\u0026#34;interaction_type\\\u0026#34;:\\\u0026#34;invoke\\\u0026#34;}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;mEukD2\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1357.38, \u0026#34;y\u0026#34;: 974.77 } } }, { \u0026#34;name\u0026#34;: \u0026#34;googlesearch\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Tool\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/tool/googlesearch\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/blob/main/components/tool/googlesearch/google_search.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;APIKey\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;BaseURL\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Lang\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Num\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;SearchEngineID\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ToolDesc\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ToolName\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;APIKey\u0026#34;, \u0026#34;SearchEngineID\u0026#34;, \u0026#34;BaseURL\u0026#34;, \u0026#34;Num\u0026#34;, \u0026#34;Lang\u0026#34;, \u0026#34;ToolName\u0026#34;, \u0026#34;ToolDesc\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;googlesearch.Config\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;lEtK7G\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1710.54, \u0026#34;y\u0026#34;: 974.57 } } } ], \u0026#34;go_definition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.3.4\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino/components/tool\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;tool.BaseTool\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;interface\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } ], \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino/blob/main/flow/agent/react/react.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;MaxStep\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ToolReturnDirectly\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;additionalProperties\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;struct{}\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;map[string]struct{}\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;map\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;MaxStep\u0026#34;, \u0026#34;ToolReturnDirectly\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;react.AgentConfig\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;{\\\u0026#34;ToolReturnDirectly\\\u0026#34;:{\\\u0026#34;einoAdditionalPropertyInput\\\u0026#34;:[]},\\\u0026#34;MaxStep\\\u0026#34;:25}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34; }, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 1361.89, \u0026#34;y\u0026#34;: 450.95 } }, \u0026#34;node_option\u0026#34;: {} }, { \u0026#34;id\u0026#34;: \u0026#34;iunICK\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;MilvusRetriever\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;Retriever\u0026#34;, \u0026#34;component_schema\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;redis\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Retriever\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/retriever/redis\u0026#34;, \u0026#34;slots\u0026#34;: [ { \u0026#34;component\u0026#34;: \u0026#34;Embedding\u0026#34;, \u0026#34;field_loc_path\u0026#34;: \u0026#34;Embedding\u0026#34;, \u0026#34;multiple\u0026#34;: false, \u0026#34;required\u0026#34;: false, \u0026#34;component_items\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;openai\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Embedding\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;official\u0026#34;, \u0026#34;identifier\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/components/embedding/openai\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;[]string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;[][]float64\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/blob/main/components/embedding/openai/embedding.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;APIKey\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;APIVersion\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;BaseURL\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ByAzure\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;bool\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Dimensions\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;EncodingFormat\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.0.0-20250106073650-ed838398894a\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/libs/acl/openai\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;openai.EmbeddingEncodingFormat\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Model\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;Timeout\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;time\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;time\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;time.Duration\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int64\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;User\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: true } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;ByAzure\u0026#34;, \u0026#34;BaseURL\u0026#34;, \u0026#34;APIVersion\u0026#34;, \u0026#34;APIKey\u0026#34;, \u0026#34;Timeout\u0026#34;, \u0026#34;Model\u0026#34;, \u0026#34;EncodingFormat\u0026#34;, \u0026#34;Dimensions\u0026#34;, \u0026#34;User\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;openai.EmbeddingConfig\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;RvASK3\u0026#34;, \u0026#34;layoutData\u0026#34;: { \u0026#34;isSlotNode\u0026#34;: true, \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 654.92, \u0026#34;y\u0026#34;: 969.1 } } } ], \u0026#34;go_definition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v0.3.6\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;github.com/cloudwego/eino\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;github.com/cloudwego/eino/components/embedding\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;embedding.Embedder\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;interface\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } ], \u0026#34;config\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;github.com/cloudwego/eino-ext/blob/main/components/retriever/redis/retriever.go\u0026#34;, \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;Dialect\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;DistanceThreshold\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;float64\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;float64\u0026#34;, \u0026#34;isPtr\u0026#34;: true } }, \u0026#34;Index\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;ReturnFields\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;[]string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;slice\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;TopK\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;int\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;VectorField\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;isPtr\u0026#34;: false } } }, \u0026#34;propertyOrder\u0026#34;: [ \u0026#34;Index\u0026#34;, \u0026#34;VectorField\u0026#34;, \u0026#34;DistanceThreshold\u0026#34;, \u0026#34;Dialect\u0026#34;, \u0026#34;ReturnFields\u0026#34;, \u0026#34;TopK\u0026#34; ], \u0026#34;goDefinition\u0026#34;: { \u0026#34;libraryRef\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;pkgPath\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;typeName\u0026#34;: \u0026#34;redis.RetrieverConfig\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;struct\u0026#34;, \u0026#34;isPtr\u0026#34;: false } }, \u0026#34;config_input\u0026#34;: \u0026#34;{}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: false, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;input_type\u0026#34;: {}, \u0026#34;output_type\u0026#34;: {} }, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 657.12, \u0026#34;y\u0026#34;: 812.29 } }, \u0026#34;node_option\u0026#34;: { \u0026#34;output_key\u0026#34;: \u0026#34;documents\u0026#34; } }, { \u0026#34;id\u0026#34;: \u0026#34;lJ0-pN\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;InputToChat\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;UserMessageToVariables\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_schema\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component\u0026#34;: \u0026#34;Lambda\u0026#34;, \u0026#34;component_source\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;input_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;*UserMessage\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;output_type\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;map[string]any\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;extra_property\u0026#34;: { \u0026#34;schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;has_option\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;interaction_type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;invoke\u0026#34;, \u0026#34;stream\u0026#34;, \u0026#34;collect\u0026#34;, \u0026#34;transform\u0026#34; ] }, \u0026#34;option_package_path\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;option_type_name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;interaction_type\u0026#34;, \u0026#34;has_option\u0026#34; ] }, \u0026#34;extra_property_input\u0026#34;: \u0026#34;{\\\u0026#34;has_option\\\u0026#34;:true,\\\u0026#34;interaction_type\\\u0026#34;:\\\u0026#34;invoke\\\u0026#34;}\u0026#34; }, \u0026#34;is_io_type_mutable\u0026#34;: true, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34; }, \u0026#34;layoutData\u0026#34;: { \u0026#34;position\u0026#34;: { \u0026#34;x\u0026#34;: 355.65, \u0026#34;y\u0026#34;: 411.61 } }, \u0026#34;node_option\u0026#34;: {} } ], \u0026#34;edges\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;8AgKo6\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;InputToRag\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;lJ0-pN\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;start\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;InputToChat\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;CDNTqO\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;end\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;ReactAgent\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;end\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;8AgKo6\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;iunICK\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;InputToRag\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;MilvusRetriever\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;iunICK\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;A7z9_b\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;MilvusRetriever\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;ChatTemplate\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;lJ0-pN\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;A7z9_b\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;InputToChat\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;ChatTemplate\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sourceWorkflowNodeId\u0026#34;: \u0026#34;A7z9_b\u0026#34;, \u0026#34;targetWorkflowNodeId\u0026#34;: \u0026#34;CDNTqO\u0026#34;, \u0026#34;source_node_key\u0026#34;: \u0026#34;ChatTemplate\u0026#34;, \u0026#34;target_node_key\u0026#34;: \u0026#34;ReactAgent\u0026#34; } ], \u0026#34;branches\u0026#34;: [], \u0026#34;nodeCounter\u0026#34;: { \u0026#34;Lambda\u0026#34;: 8, \u0026#34;ChatModel\u0026#34;: 4, \u0026#34;Tool\u0026#34;: 3, \u0026#34;default\u0026#34;: 3 } } 最后点击生成代码，生成到目标目录 📷 [图片 token=Q8clbvraoo4qIFx3OJHcKIfRnXg（未能下载，见飞书原文）]\n生成完后，会在目标目录看到生成出来的这些组件，下面我们来逐个介绍\n📷 [图片 token=FgxMblVIco0FzixMn2qcEPhZnzg（未能下载，见飞书原文）]\n这部分代码在：SuperBizAgent/internal/ai/agent/chat_pipeline\n实战 注意，在运行代码之前，务必先看\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\nRunnable执行器 运行Agent看看输出 # 运行代码 cd SuperBizAgent/internal/ai/cmd/chat_cmd # 输出 (base) ➜ chat_cmd git:(main) ✗ go run main.go Q: 你好 A: 你好！我是你的对话小助手，很高兴为你服务！ 我可以帮助你： - 理解和处理对话上下文 - 搜索网络获取信息 - 查询数据库 - 检查系统警报状态 - 搜索内部文档 请告诉我你需要什么帮助，我会尽力为你提供支持！ 2025/11/30 21:46:45 Getting current time: 2025-11-30 21:46:45.704387 2025/11/30 21:46:45 Current time: Seconds=1764510405, Milliseconds=1764510405704, Microseconds=1764510405704387 ---------------- Q: 现在是几点 A: 现在是 2025年11月30日 21:46:45 如果需要更精确的时间，我可以提供毫秒或微秒级别的时间信息。 执行代码研究 好，执行完成后。我们来看看具体的代码实现是怎么样的。我们来重点看下面高亮的代码行：\n首先调用chat_pipeline.BuildChatAgent创建了一个runner执行器。\n调用runner的Invoke方法，入参是UserMessage。\nfunc main() { ctx := context.Background() id := \u0026#34;111\u0026#34; userMessage := \u0026amp;chat_pipeline.UserMessage{ ID: id, Query: \u0026#34;你好\u0026#34;, History: mem.GetSimpleMemory(id).GetMessages(), } runner, err := chat_pipeline.BuildChatAgent(ctx) if err != nil { panic(err) } // 第一次对话 out, err := runner.Invoke(ctx, userMessage) if err != nil { panic(err) } answer := out.Content fmt.Println(\u0026#34;Q: 你好\u0026#34;) fmt.Println(\u0026#34;A:\u0026#34;, answer) mem.GetSimpleMemory(id).SetMessages(schema.UserMessage(\u0026#34;你好\u0026#34;)) mem.GetSimpleMemory(id).SetMessages(schema.SystemMessage(out.Content)) // 第二次对话 userMessage = \u0026amp;chat_pipeline.UserMessage{ ID: id, Query: \u0026#34;现在是几点\u0026#34;, History: mem.GetSimpleMemory(id).GetMessages(), } out, err = runner.Invoke(ctx, userMessage) if err != nil { panic(err) } answer = out.Content fmt.Println(\u0026#34;----------------\u0026#34;) fmt.Println(\u0026#34;Q: 现在是几点\u0026#34;) fmt.Println(\u0026#34;A:\u0026#34;, answer) } BuildChatAgent研究 我们重点来看一下第一个返回值，r compose.Runnable[*UserMessage, *schema.Message]，这个返回值代表返回一个可以执行的执行器。\n其中[*UserMessage, *schema.Message] 对应着下面的Runnable接口的[I, O any]。\n这是一个泛型，I代表Input，输入；O代表output输出。\n总结一下BuildChatAgent：返回一个执行器，这个执行器的入参是我们自定义的一个UserMessage结构体，出参是框架定义的schema.Message。\nfunc BuildChatAgent(ctx context.Context) (r compose.Runnable[*UserMessage, *schema.Message], err error) { /// } // Runnable is the interface for an executable object. Graph, Chain can be compiled into Runnable. // runnable is the core conception of eino, we do downgrade compatibility for four data flow patterns, // and can automatically connect components that only implement one or more methods. // eg, if a component only implements Stream() method, you can still call Invoke() to convert stream output to invoke output. type Runnable[I, O any] interface { Invoke(ctx context.Context, input I, opts ...Option) (output O, err error) Stream(ctx context.Context, input I, opts ...Option) (output *schema.StreamReader[O], err error) } type UserMessage struct { ID string `json:\u0026#34;id\u0026#34;` Query string `json:\u0026#34;query\u0026#34;` History []*schema.Message `json:\u0026#34;history\u0026#34;` } 我们继续看看编排代码BuildChatAgent的其他流程。\n里面有很多AddEdge，这里面的点、边连接顺序，其实就是上面我们用eino-dev插件编排的顺序。\n📷 [图片 token=BKRpb8ZZUok71CxEV2acjC0anDa（未能下载，见飞书原文）]\n也就是说：BuildChatAgent返回的执行器，在调用后会按照这个顺序执行。\n下面我们继续来看看每个节点具体做了什么？\nfunc BuildChatAgent(ctx context.Context) (r compose.Runnable[*UserMessage, *schema.Message], err error) { _ = g.AddEdge(compose.START, InputToRag) _ = g.AddEdge(compose.START, InputToChat) _ = g.AddEdge(ReactAgent, compose.END) _ = g.AddEdge(InputToRag, MilvusRetriever) _ = g.AddEdge(MilvusRetriever, ChatTemplate) _ = g.AddEdge(InputToChat, ChatTemplate) _ = g.AddEdge(ChatTemplate, ReactAgent) r, err = g.Compile(ctx, compose.WithGraphName(\u0026#34;ChatAgent\u0026#34;), compose.WithNodeTriggerMode(compose.AllPredecessor)) return r, err } 召回组件1—InputToRag lambda node 观察lambda的输入和输出，user message是我们构造的，然后输出一个string，这个string就是用于RAG做召回使用的\n/ newInputToRagLambda component initialization function of node \u0026#39;InputToQuery\u0026#39; in graph \u0026#39;EinoAgent\u0026#39; func newInputToRagLambda(ctx context.Context, input *UserMessage, opts ...any) (output string, err error) { return input.Query, nil } 召回组件2—InputToRag* *-\u0026gt; Retriever 这里的详细在 《RAG召回实战2》讲了，这里就不赘述了\n需要注意的一点，在 orchestration.go 中，我们将输出放到了map[\u0026ldquo;documents\u0026rdquo;]中。从向量数据库中召回的内容，会放到一个map里面，并且其key为documents。value就是召回的内容\n_ = g.AddRetrieverNode(MilvusRetriever, Retriever, compose.WithOutputKey(\u0026#34;documents\u0026#34;)) 接收用户输入的组件—InputToChat* *lambda node 观察lambda的输入和输出，user message 是我们构造的。输出是一个map，这个map在chatTemplate里面会用到\n// newInputToChatLambda component initialization function of node \u0026#39;InputToHistory\u0026#39; in graph \u0026#39;EinoAgent\u0026#39; func newInputToChatLambda(ctx context.Context, input *UserMessage, opts ...any) (output map[string]any, err error) { return map[string]any{ \u0026#34;content\u0026#34;: input.Query, \u0026#34;history\u0026#34;: input.History, \u0026#34;date\u0026#34;: time.Now().Format(\u0026#34;2006-01-02 15:04:05\u0026#34;), }, nil } 动态拼接上下文与对话历史Prompt构建—ChatTemplate 我们先来观察一下ChatTemplate节点，它有两个输入，一个是InputToChat的输出，另一个是Retriever的输出。所以ChatTemplate的输入可以看作是前面两个节点输出的并集。分别是：\ncontent : input.Query \u0026mdash;用户的输入\nhistory : input.History \u0026mdash;历史的输入和输出\ndate : time.Now() \u0026mdash;当前时间\ndocuments : []*.schema.document \u0026mdash;召回的内容\n📷 [图片 token=FOZdbk95QoQTAYx2HZ6cqo4enpf（未能下载，见飞书原文）]\n有了这个map，怎么用呢？我们继续往下看，FormatType: schema.FString* *代表使用{}作为占位符。那么{date} {content} {documents} 这三个比较好理解，框架自动将这三个key的value填充进去。\nhistory是什么意思,为什么没有加{}占位符呢？是因为history的值是[]*schema.Message类型。本质是自定义结构体切片，比如：\n\u0026#34;history\u0026#34;: []*schema.Message{ {Role: \u0026#34;user\u0026#34;, Content: \u0026#34;what is eino?\u0026#34;}, {Role: \u0026#34;assistant\u0026#34;, Content: \u0026#34;eino is a great freamwork to build llm apps\u0026#34;} } MessagesPlaceholder的意思就是占位，如果map里面有history，直接将其value append进行即可。这里可能有点绕，什么是直接添加进去？\n注意这一行：Templates: []schema.MessagesTemplate{},说明Templates 本质也是一个自定义结构体切片。所有直接添加进去的意思就是，把Templates 和history 这两个切片合并起来。从2个小切片合并成一个大切片。\n// newChatTemplate component initialization function of node \u0026#39;ChatTemplate\u0026#39; in graph \u0026#39;EinoAgent\u0026#39; func newChatTemplate(ctx context.Context) (ctp prompt.ChatTemplate, err error) { config := \u0026amp;ChatTemplateConfig{ FormatType: schema.FString, Templates: []schema.MessagesTemplate{ schema.SystemMessage(systemPrompt), schema.MessagesPlaceholder(\u0026#34;history\u0026#34;, false), schema.UserMessage(\u0026#34;{content}\u0026#34;), }, } ctp = prompt.FromMessages(config.FormatType, config.Templates...) return ctp, nil } var systemPrompt = ` # 角色：对话小助手 ## 核心能力 - 上下文理解与对话 ## 互动指南 - 在回复前，请确保你： • 完全理解用户的需求和问题，如果有不清楚的地方，要向用户确认 ## 输出要求： • 易读，结构良好，必要时换行 ## 上下文信息 - 当前日期：{date} - 相关文档：|- ==== 文档开始 ==== {documents} ==== 文档结束 ==== ` 让Agent学会\u0026quot;思考-行动\u0026quot;循环——ReAct组件 📷 [图片 token=Ur0Cb0OqwoEQcYxARtdcgdtUnxc（未能下载，见飞书原文）]\n就跟编排图中的ReAct组件一样，它有几个插口，我们需要给它装一个model，以及多个tool工具。\n这个react.NewAgent是eino框架提供的接口，我们不需要从0到1实现ReAct，只需要使用eino提供的sdk即可。（但是原理要知道，前面重点介绍了原理！本质就是通过大模型的输出来判断要不要调用工具）\nfunc newReactAgentLambda(ctx context.Context) (lba *compose.Lambda, err error) { config := \u0026amp;react.AgentConfig{ MaxStep: 25, ToolReturnDirectly: map[string]struct{}{}} chatModelIns11, err := newChatModel(ctx) if err != nil { return nil, err } // 1. 安装模型 config.ToolCallingModel = chatModelIns11 mcpTool, err := tools.GetLogMcpTool() if err != nil { return nil, err } // 2. 安装工具 config.ToolsConfig.Tools = mcpTool config.ToolsConfig.Tools = append(config.ToolsConfig.Tools, tools.NewPrometheusAlertsQueryTool()) config.ToolsConfig.Tools = append(config.ToolsConfig.Tools, tools.NewMysqlCrudTool()) config.ToolsConfig.Tools = append(config.ToolsConfig.Tools, tools.NewGetCurrentTimeTool()) config.ToolsConfig.Tools = append(config.ToolsConfig.Tools, tools.NewQueryInternalDocsTool()) // 3. 创建ReAct Agent ins, err := react.NewAgent(ctx, config) if err != nil { return nil, err } lba, err = compose.AnyLambda(ins.Generate, ins.Stream, nil, nil) if err != nil { return nil, err } return lba, nil } 总结 至此，对话Agent的核心流程RAG召回与ReAct模式的代码就讲完了。如果你一篇一篇看下来，会发现其实代码实现真的不难，而且也不重要，框架帮我们做了很多事情。核心是要搞懂我们的设计原理：RAG、ReAct。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E5%AF%B9%E8%AF%9DAgent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Go%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;U8wubBj37oDyjoxkw5wccH2Tn3b\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2070\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：对话Agent代码实现(Go)"},{"content":" 📷 [图片 token=FbuMbyCWdo8Kruxzm1CcdBOqnCd（未能下载，见飞书原文）]\n注意，运行程序之前请先看：\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\n前言 关键代码：\nSuperBizAgent/internal/ai/agent/plan_execute_replan\nSuperBizAgent/internal/ai/cmd/ai_ops_cmd/main.go\n📷 [图片 token=VruUbot1io8JulxkbE7cv0H6nkc（未能下载，见飞书原文）]\n流程梳理 运维Agent的核心目标是 规划-\u0026gt;执行-\u0026gt;评估-\u0026gt;调整。整体流程就是三个步骤：\nPlaner：拆解排查步骤\nExecuter：执行计划第一步\nReplaner：评估结果并调整计划\n📷 [图片 token=QOBPbbkD8oVs08xn8XBcaLpenAb（未能下载，见飞书原文）]\n实战 注意，在运行代码之前，务必先看\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\nRunnable执行器 运行Agent看看输出 我们来分析一下执行步骤：\n首先Planner规划了7个执行步骤\nExecutor执行第一步，获取当前时间\nExecutor执行第二步，发现没有正在活动的告警，无法执行后续步骤，流程扭转到Replanner\nReplanner发现没有活动的告警，则执行退出操作\n# 运行代码 cd SuperBizAgent/internal/ai/cmd/ai_ops_cmd # 输出 (base) ➜ ai_ops_cmd git:(main) ✗ go run main.go ------------- Event ------------- name: Planner path: [{plan_execute_replan} {Planner}] answer: {\u0026#34;steps\u0026#34;: [ \u0026#34;step1：调用 get_current_time，获取所有时间敏感操作的当前时间戳\u0026#34;, \u0026#34;step2：调用 query_prometheus_alerts，从监控系统中获取所有活动警报\u0026#34;, \u0026#34;step3：对于步骤 2 中确定的每个活动警报，提取警报名称，并以警报名称为参数调用query_internal_docs，从内部文档中检索相应的处理程序\u0026#34;, \u0026#34;step4：对于需要根据内部文档进行日志分析的每个警报，调用相应的日志查询工具，并使用步骤 1 中的当前时间作为所需参数，包括区域和日志主题，以便进行基于时间的查询\u0026#34;, \u0026#34;step5：分析从内部文档中检索到的信息以及每个警报的任何附加日志数据，严格遵循内部文档中概述的程序，而不使用外部信息\u0026#34;, \u0026#34;step6：总结每个警报的分析结果，包括警报详情、文件中建议的处理步骤以及任何相关的日志发现\u0026#34;, \u0026#34;step7：编写一份综合汇总报告，将所有活动警报及其各自的分析汇总到最终的综合概览中\u0026#34; ]} ------------- Event ------------- name: Executor path: [{plan_execute_replan} {Planner} {execute_replan} {Executor}] answer: 我将按照计划执行第一步，获取当前时间戳用于后续的时间敏感操作。 tool name: get_current_time arguments: {} ------------- Event ------------- name: Executor ------------- Event ------------- name: Executor ------------- Event ------------- name: Executor ------------- Event ------------- name: Replanner path: [{plan_execute_replan} {Planner} {execute_replan} {Executor} {Replanner}] action: exit ----- Final Response ----- {\u0026#34;response\u0026#34;: \u0026#34;## 最终汇总报告：告警运维分析完成\\n\\n### 当前告警状态\\n系统正常，无活跃告警\\n\\n### 执行过程回顾\\n1. **时间获取**：成功获取当前时间戳用于时间敏感操作\\n2. **告警查询**：调用Prometheus监控系统查询所有活跃告警\\n3. **查询结果**：系统返回0个活跃告警，表明所有服务运行正常\\n\\n### 分析结论\\n根据Prometheus监控系统的权威查询结果，当前系统处于完全健康状态：\\n- 无任何活跃告警事件需要处理\\n- 所有服务组件运行正常\\n- 监控系统检测未发现异常\\n\\n### 关键洞察\\n1. **系统稳定性**：零告警状态表明系统运行稳定，运维压力较低\\n2. **监控有效性**：Prometheus监控系统正常运行，能够准确反映系统状态\\n3. **流程验证**：告警处理流程已正确执行，只是当前无需处理具体告警\\n\\n### 后续建议\\n- 继续保持对系统的监控，确保及时发现潜在问题\\n- 定期检查监控系统配置，确保告警规则的有效性\\n- 如后续出现告警，将按照既定内部文档流程进行处理\\n\\n**任务完成状态：** ✅ 目标已完全达成 - 已成功分析当前告警状态并得出明确结论\u0026#34;} ----- Final detail ----- 执行代码研究 好，执行完成后。我们先来研究一个prompt是怎么写的：\n首先，我们要求它先通过query_prometheus_alerts工具获取所有活跃的告警\n如果有告警，那么通过query_internal_docs查询告警的解决方案\n并且要求大模型必须基于内部文档的解决方案来执行，不能乱执行\nfunc main() { ctx := context.Background() query := ` \u0026#34;1. 你是一个智能的服务告警运维分析助手,首先调用工具query_prometheus_alerts获取所有活跃的告警。\u0026#34; \u0026#34;2. 分别根据告警的名称调用工具query_internal_docs，获取告警名对应的处理方案。\u0026#34; \u0026#34;3. 完全遵循内部文档的内容进行查询和分析,不允许使用文档外的任何信息。\u0026#34; \u0026#34;4. 涉及到时间的参数都需要先通过工具get_current_time获取当前时间,再结合用户的时间要求进行传参。\u0026#34; \u0026#34;5. 涉及到日志的查询,需要先通过日志工具获取相关日志信息，参数必须携带地域和日志主题。\u0026#34; \u0026#34;6. 分别将告警对应查询到的信息进行总结分析,最后汇总所有告警和总结。\u0026#34;` resp, detail, err := plan_execute_replan.BuildPlanAgent(ctx, query) if err != nil { panic(err) } fmt.Println(\u0026#34;----- Final Response -----\u0026#34;) fmt.Println(resp) fmt.Println(\u0026#34;----- Final detail -----\u0026#34;) fmt.Println(detail) } 观察程序输出的日志，可以看到Planner制定的计划：\n\u0026#34;step1：调用 get_current_time，获取所有时间敏感操作的当前时间戳\u0026#34;, \u0026#34;step2：调用 query_prometheus_alerts，从监控系统中获取所有活动警报\u0026#34;, \u0026#34;step3：对于步骤 2 中确定的每个活动警报，提取警报名称，并以警报名称为参数调用query_internal_docs，从内部文档中检索相应的处理程序\u0026#34;, \u0026#34;step4：对于需要根据内部文档进行日志分析的每个警报，调用相应的日志查询工具，并使用步骤 1 中的当前时间作为所需参数，包括区域和日志主题，以便进行基于时间的查询\u0026#34;, \u0026#34;step5：分析从内部文档中检索到的信息以及每个警报的任何附加日志数据，严格遵循内部文档中概述的程序，而不使用外部信息\u0026#34;, \u0026#34;step6：总结每个警报的分析结果，包括警报详情、文件中建议的处理步骤以及任何相关的日志发现\u0026#34;, \u0026#34;step7：编写一份综合汇总报告，将所有活动警报及其各自的分析汇总到最终的综合概览中\u0026#34; 紧接着Executor按照计划依次执行：\n调用get_current_time获取当前时间\n调用query_prometheus_alerts获取活跃中的告警\n然后Replan发现没有活跃中的告警，那么说明系统正常，则可以退出了。\n------------- Event ------------- name: Executor path: [{plan_execute_replan} {Planner} {execute_replan} {Executor}] answer: 我将按照计划执行第一步，获取当前时间戳用于后续的时间敏感操作。 tool name: get_current_time arguments: {} ------------- Event ------------- name: Executor path: [{plan_execute_replan} {Planner} {execute_replan} {Executor}] answer: ✅ **第一步完成：获取当前时间** 已成功获取当前系统时间 现在可以继续执行第二步：查询Prometheus活跃告警。我将调用query_prometheus_alerts工具来获取所有当前活跃的告警信息。 tool name: query_prometheus_alerts arguments: {} ------------- Event ------------- name: Executor path: [{plan_execute_replan} {Planner} {execute_replan} {Executor}] answer: ✅ **第二步完成：查询Prometheus活跃告警** 查询结果显示当前系统中有 **0个活跃告警**。 由于没有发现任何活跃告警，接下来的步骤将无法继续执行 ------------- Event ------------- name: Replanner path: [{plan_execute_replan} {Planner} {execute_replan} {Executor} {Replanner}] action: exit BuildPlanAgent研究 首先我们创建了3个Agent：NewPlanner、NewExecutor、NewRePlanAgent\n然后通过planexecute.New创建了一个协调器，最后执行\n这里的NewPlanner、NewExecutor、NewRePlanAgent、planexecute.New、adk.NewRunner 全部都是eino官方提供的sdk，我们直接使用sdk进行组装即可\nfunc BuildPlanAgent(ctx context.Context, query string) (string, []string, error) { // 1. 创建3个Agent planAgent, err := NewPlanner(ctx) executeAgent, err := NewExecutor(ctx) replanAgent, err := NewRePlanAgent(ctx) // 2. 组装planExecuteAgent planExecuteAgent, err := planexecute.New(ctx, \u0026amp;planexecute.Config{ Planner: planAgent, Executor: executeAgent, Replanner: replanAgent, MaxIterations: 20, }) r := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: planExecuteAgent, }) // 3. 执行 iter := r.Query(ctx, query) var lastMessage adk.Message var detail []string for { /// } return lastMessage.Content, detail, nil } func NewPlanner(ctx context.Context) (adk.Agent, error) { planModel, err := models.OpenAIForDeepSeekV31Think(ctx) if err != nil { return nil, err } return planexecute.NewPlanner(ctx, \u0026amp;planexecute.PlannerConfig{ ToolCallingChatModel: planModel, }) } func NewExecutor(ctx context.Context) (adk.Agent, error) { // alerts toolList = append(toolList, tools.NewPrometheusAlertsQueryTool()) // file toolList = append(toolList, tools.NewQueryInternalDocsTool()) // time toolList = append(toolList, tools.NewGetCurrentTimeTool()) execModel, err := models.OpenAIForDeepSeekV3Quick(ctx) if err != nil { return nil, err } return planexecute.NewExecutor(ctx, \u0026amp;planexecute.ExecutorConfig{ Model: execModel, ToolsConfig: adk.ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: toolList, }, }, MaxIterations: 999999, }) } func NewRePlanAgent(ctx context.Context) (adk.Agent, error) { model, err := models.OpenAIForDeepSeekV31Think(ctx) if err != nil { return nil, err } return planexecute.NewReplanner(ctx, \u0026amp;planexecute.ReplannerConfig{ ChatModel: model, }) } Planner **核心功能：**根据用户目标生成初始任务计划（结构化步骤序列）\n实现方式：\n通过 PlanTool 生成符合 JSON Schema 的步骤列表。\n或直接使用支持结构化输出的模型，直接生成 Plan 格式结果\n输出： Plan 对象，Plan 对象就是计划列表，存储在Session中，供其他Agent使用\n下面是Planner的system prompt，其核心就是要求大模型根据输入返回一个执行计划\nPlannerPrompt = prompt.FromMessages(schema.FString,schema.SystemMessage( 你是一位专业的规划代理。给定一个目标，创建一个全面的分步计划来实现该目标。 ## 你的任务 分析目标并生成一个战略计划，将目标分解为可管理、可执行的步骤。 ## 规划要求 你计划中的每一步都必须： - **具体且可操作**：清晰的指令，可以无歧义地执行 - **自包含**：包含所有必要的上下文、参数和要求 - **可独立执行**：可以在不依赖其他步骤的情况下执行 - **逻辑有序**：按最优顺序排列以实现高效执行 - **聚焦目标**：直接有助于实现主要目标 ## 规划指南 - 消除冗余或不必要的步骤 - 为每个步骤包含相关的约束、参数和成功标准 - 确保最后一步产生完整的答案或可交付成果 - 预见潜在挑战并包含缓解策略 - 构建步骤使其在逻辑上相互支撑 - 提供足够的细节以确保成功执行 ## 质量标准 - 计划完整性：是否涵盖了目标的所有方面？ - 步骤清晰度：每个步骤能否被独立理解和执行？ - 逻辑流程：步骤是否遵循合理的进展顺序？ - 效率：这是实现目标最直接的路径吗？ - 适应性：计划能否处理意外结果或变化？ Executer 核心功能：执行计划中的首个步骤，调用外部工具完成具体任务\n实现方式：\n从 Session 中获取当前 Plan 和已执行步骤\n提取计划中的第一个未执行步骤作为目标\n调用工具执行该步骤，将结果存储于 Session\n关键能力：本质就是一个魔改的ReAct设计模式的Agent，支持多轮工具调用，确保单步任务完成。\nExecutorPrompt = prompt.FromMessages(schema.FString, schema.SystemMessage() ---------------------------------------------- 你是一位认真细致的执行代理。遵循给定的计划，仔细且彻底地执行你的任务。 ## 目标 {input} ## 给定以下计划： {plan} ## 已完成的步骤和结果 {executed_steps} ## 你的任务是执行第一步，即： {step} Replanner 核心功能：评估执行进度，决定继续执行（生成新计划）或终止任务（返回结果）\n实现方式：通过 PlanTool（生成新计划）或 RespondTool（返回结果）输出决策\n决策逻辑：\n继续执行：若目标未达成，生成包含剩余步骤的新计划，更新 Session 中的 Plan\n终止任务：若目标已达成，调用 RespondTool 生成最终用户响应\nReplannerPrompt = prompt.FromMessages(schema.FString,schema.SystemMessage()) ———————————————————————— 你将审查实现目标的进展。分析当前状态并确定最优的下一步行动。 ## 你的任务 基于上述进展，你必须选择恰好一个行动： ### 选项 1：完成（如果目标已完全实现） 调用 \u0026#39;{respond_tool}\u0026#39;，包含： - 全面的最终答案 - 清晰总结目标如何达成的结论 - 执行过程中的关键洞察 ### 选项 2：继续（如果还需要更多工作） 调用 \u0026#39;{plan_tool}\u0026#39;，提供一个修订后的计划，该计划： - 仅包含剩余步骤（排除已完成的步骤） - 结合从已执行步骤中学到的经验 - 解决发现的任何差距或问题 - 保持逻辑步骤顺序 ## 规划要求 你计划中的每一步都必须： - **具体且可操作**：清晰的指令，可以无歧义地执行 - **自包含**：包含所有必要的上下文、参数和要求 - **可独立执行**：可以在不依赖其他步骤的情况下执行 - **逻辑有序**：按最优顺序排列以实现高效执行 - **聚焦目标**：直接有助于实现主要目标 ## 规划指南 - 消除冗余或不必要的步骤 - 基于新信息调整策略 - 为每个步骤包含相关的约束、参数和成功标准 ## 决策标准 - 原始目标是否已完全满足？ - 是否还有剩余的要求或子目标？ - 结果是否表明需要调整策略？ - 还需要哪些具体行动？ 总结 通过上面的分析，我们已经了解了Planner、Executer、Replanner的作用和相关prompt。但是你可能会有一种意犹未尽的感觉，因为我们在这里全部都是调用sdk，实际代码只是组装而已。不要慌，我们再回过头来看看Plan-Execute-Replan的流程。\n首先用Planner Agent生成了一份计划\n将计划发送给Executor Agent，让Executor按照计划执行\n每次执行完，都将计划和执行结果一起发送给Replanner评估\nReplanner评估后决定修改计划还是决定已完成\n其实 Planner、Executer、Replanner 之间的交互逻辑很简单，就是上面的4个步骤，只要你搞明白了这4个步骤。我们自己用代码实现这个workflow流程也很简单，其核心就是流程控制，与Plan对象在整个流程中的传递而已。\n所以无需担心，面试会问到的所有细节，在面试攻略篇章全部为你准备好了。（想想gorm，jdbc这些数据库sdk，我们也只是使用而已，会用即可，只要八股文准备的好，无需紧张～）\n📷 [图片 token=QQ7pbjK4Xoa7eRxeYVucQ5nVn7h（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E8%BF%90%E7%BB%B4Agent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Go%29/","summary":"\u0026lt;image token=\u0026ldquo;FbuMbyCWdo8Kruxzm1CcdBOqnCd\u0026rdquo; width=\u0026ldquo;2618\u0026rdquo; height=\u0026ldquo;2074\u0026rdquo; align=\u0026ldquo;center\u0026rdquo;/ 注意，运行程序之前请先看： - \u0026lt;mention-doc token","title":"源码分析：运维Agent代码实现(Go)"},{"content":"配置修改 配置文件路径：SuperBizAgent/manifest/config/config.yaml\n按照《环境准备教程-项目配置》的步骤，修改配置文件\n启动向量数据库milvus # 进入manifest/docker目录 cd manifest/docker # 一键启动所有依赖 docker compose up -d # 如果想要停止，则在这个目录使用 docker compose down 📷 [图片 token=ENYAbf4xZoZnNAxRxLXc16qInjb（未能下载，见飞书原文）]\n启动后端 go run main.go 📷 [图片 token=WFgjb05mzocH5lxrVyecvKAlnxg（未能下载，见飞书原文）]\n启动前端 cd SuperBizAgent/SuperBizAgentFrontend chmod +x start.sh ./start.sh 📷 [图片 token=HONvbgb5YoqLUTxjfJIcilgnnUc（未能下载，见飞书原文）]\n打开前端地址：http://localhost:8080/ （后续不会对前端进行讲解，前端是用 cursor 根据http://127.0.0.1:6872/api.json 写的） 📷 [图片 token=DduEbjWi2oobgWxHIndcMa6DnZe（未能下载，见飞书原文）]\n整体目录结构 先看看整体目录结构，分为几个大块：\nSuperBizAgent/internal/ai/agent：存放我们agent的目录\nSuperBizAgent/internal/ai/cmd：单独运行agent的目录，方便测试\nSuperBizAgent/internal/controller/chat：对外api接口的核心实现目录\nSuperBizAgent/manifest：存放配置文件和一键启动依赖的docker-compose\nSuperBizAgent/SuperBizAgentFrontend：前端代码目录，通过start.sh启动前端\n➜ SuperBizAgent git:(main) tree . ├── SuperBizAgentFrontend # 存放前端的目录 ├── api # 对外api接口定义的目录 ├── docs # 存放用户上传文件的目录 ├── internal │ ├── ai │ │ ├── agent │ │ │ ├── chat_pipeline # 对话agent核心代码目录 │ │ │ ├── knowledge_index_pipeline # 知识库agent核心代码目录 │ │ │ └── plan_execute_replan # 运维agent核心代码目录 │ │ ├── cmd │ │ │ ├── ai_ops_cmd # 运维agent独立运行目录，便于测试使用 │ │ │ ├── knowledge_cmd # 知识库agent独立运行目录，便于测试使用 │ │ │ └── recall_cmd # 召回知识库内容独立运行目录，便于测试使用 │ │ └── tools # 工具集目录 │ │ ├── get_current_time.go │ │ ├── mysql_crud.go │ │ ├── query_internal_docs.go │ │ ├── query_log.go │ │ └── query_metrics_alerts.go │ ├── controller │ │ └── chat │ │ ├── chat_v1_ai_ops.go # 运维api核心代码 │ │ ├── chat_v1_chat.go # 对话api核心代码 │ │ ├── chat_v1_chat_stream.go # 流式对话api核心代码 │ │ └── chat_v1_file_upload.go # 上传文档到知识库核心代码 ├── main.go # 项目启动处 ├── manifest │ ├── config │ │ └── config.yaml # 配置文件 │ └── docker │ ├── docker-compose.yml # 启动依赖的地方 单独运行某个Agent 注意，如果你想单独运行SuperBizAgent/internal/ai/cmd下的某个Agent，需要把config文件复制到对应目录下。\n📷 [图片 token=IzUWbXgbhob7opxCHXMcwWzFnhe（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE%E6%95%99%E7%A8%8B%28Go%29/","summary":"配置修改 配置文件路径： SuperBizAgent/manifest/config/config.yaml 按照《环境准备教程-项目配置》的步骤，修改配置文件  启动向量数据库milvus bash  进入manifest/docker目","title":"运行项目教程(Go)"},{"content":"看到很多同学在飞书群里会分享面经，风哥导师也会处理答疑，所以这个文档后面就会收录，群里发过的面经，方便大家后续复习。\n本文更倾向于记录面经与回答思路～\n2026-03-13 面试题目分享 我理解这属于分享，题库都有答案的\n📷 [图片 token=Xm2qbs8QQomTyYxbixHcBsHKnXd（未能下载，见飞书原文）]\n2026-03-14 面试题目分享 📷 [图片 token=BKbHbTDB8oY3ozx3Q9LcHMePnjt（未能下载，见飞书原文）]\n2026-03-18 📷 [图片 token=MVfVbF7HQot4hRxIFoXcw615nWf（未能下载，见飞书原文）]\n你的对话上下文是有几轮？阈值是怎么设置的？ 基于你面试说的场景看情况定一个阈值（轮数），比如10轮/70%上下文\n紧接着可能会调整你为什么是10轮/70%上下文。\n你就说之前看过xxx文章，看里面是这么做的，当上下文太多的时候需要压缩\n除了人工反馈，你打算用什么指标来评价这个On Call Agent的好坏的 进线率/拦截率 你的对话Agent prompt是如何调优的 题库里有\nprompt工程那一套，【减少大模型幻觉，你必须要掌握的 6 个方法！-哔哩哔哩】 https://b23.tv/G2O7fRC\n你说这个项目已经内部上线了，具体是哪个职能的团队在用？ 看自己的场景，最好说自己的组内 2026-03-19 📷 [图片 token=ZtmGbzbG5o5IUBxPkhIczxZ5nUH（未能下载，见飞书原文）]\n调用工具异常/失败怎么处理？ 调用工具失败，本质上就是函数error，可以for 1 \u0026lt; 3 retry ; 也可以直接返回失败。\n本来就是ReAct 、 Plan 模式，失败了大模型也可能会重试的，把这俩情况说一下我觉得就行了。\nagent执行异常/失败怎么处理？ agent执行异常，一般是无限调用Tool，循环。\n一般我们会设置一个最大Step，超过Step就强制中断了。\n第二个就是检查你的prompt，为什么会出现这个情况，是prompt写的有问题，还是模型选的太垃圾\n用户乱问问题怎么处理？我就往防止模型幻觉那块去靠，但是他后面又问了不了解query重写，应该是想让我答这个 如果你不了解query重写，那就往幻觉靠，然后明确你的项目立意，是能站住脚的，如果还挑战你，你就投降：面试官这一块我确实还没来得及了解，等面试结束我就去看\n针对乱问问题，意图识别也没用，一般都是prompt工程里做一些限制，比如“如果用户的提问与你的专业知识无关，则回复不知道。”\n📷 [图片 token=E21SbOFCaoVuRcxBvaecHSzunLg（未能下载，见飞书原文）]\n如果这个项目是IO密集型，那么CPU肯定打不满。这时候如果QPS增加了，该怎么优化这样的单机性能呢? 这个项目一定不是CPU密集型，打不满就打不满，就是在等大模型回复。\nQPS增加了那也是大模型的压力增加了，对我们的服务没有任何影响\n面试官的这个提问非常奇怪，建议和面试官对齐一下，他到底想问什么，单看这个问题我没Get到他想问什么\n2026-03-21 📷 [图片 token=SuSFbWk3IoG1BMxEsyYcrs7Bnab（未能下载，见飞书原文）]\n问我qps多少 首先我们项目标准的立意是日常值班使用的，所以QPS不会太高\n我觉得你这个回答是可以的，没问题。如果说QPS有100反而很奇怪\n从项目立意来说，QPS有1，2都不错了。使用并不频繁。\n2026-03-22 📷 [图片 token=FvPjb1N4FoGJyAxp3aNcEppJnDx（未能下载，见飞书原文）]\n能不能补充项目实际会出现的问题？ 你回答文档处理当然简单啊\u0026hellip; 这个问题其实需要往幻觉和项目迭代的方向引：\n刚开始简单粗暴塞文档，发现效果不好\n做RAG，效果好了，但是还有幻觉，胡说八道的情况\nReAct多步推理的好处是什么？\nprompt工程，参考题库那一套\n这个问题需要你具备讲故事的能力，而不是真的遇到了什么难点，既然是难点，你都解决了那还是难点吗？另外，后端增删改查真的有啥难点吗，如果面试官一直挑战难点，那就是故意的～\n面试官想挑刺的时候，要去讲故事规避和投降\n2026-03-23 📷 [图片 token=MM4obABfzodbe5xaOLycApq7nmf（未能下载，见飞书原文）]\n先不谈技术细节，你这个提问不换行，我要是面试官我就想给你挂了 这个需求是你自己提的，那你在中间方案设计和上线是怎么做的，怎么思考的？ 这个项目源于真实的痛点，参考题库1。\u0026mdash;-先把问题讲清楚\n第一版本是直接把整个文档传入上下文。\u0026mdash;-出现了什么问题\n然后采用ReAct、PlanExecutor\u0026mdash;\u0026mdash;解决了什么问题，他们的区别是什么\n最后就是怎么上线\u0026mdash;-看你公司是怎么发布的呗，这个每个人的回答都不一样\n面试官问这个问题，就是想看你对这个项目立意的思考，为什么要做这个项目，怎么做的，技术是怎么选的。\n如果你看过题库，基本都能回答出来。我认为这是一个考察沟通能力的问题\n看你能不能把你做的事情讲清楚\n我看你提到提升团队效率，怎么分析问题、解决问题。你当时是怎么想的？ 这个我觉得就是题库1，你为什么要做这个项目，好处是什么\n而且这个问题非常好，能体现出你自己的思考和主观能动性\n挺好回答的，就是有痛点，去解决痛点。\n你平常会有什么常用的skills 题库44\n我自己也没有常用的，建议把话题引到了，我去了解过skills，他和mcp的区别是什么，好处是什么\n多Agent的时候，如果某个Agent挂掉了，怎么保证它稳定性，怎么及时发现 这个问题其实就是可观测怎么做\n怎么发现：埋点，监控告警那一套东西\n📷 [图片 token=NxDQbyox5o1aIfxjk7Mc0U0Nn7k（未能下载，见飞书原文）]\n工作流的具体创建流程 其实就是chain、点和边连起来。可以通过代码实现，也可以通过低代码可视化实现 为什么使用图编排，和其他工作流有什么区别 这个就是对话Agent，在ReAct前面加了一个RAG召回的流程而已\n为什么使用？因为可视化编排很快，懒得写代码呗，写代码也可以实现\n和其他工作流有什么区别，没什么区别，只是后面用的是ReAct，其他工作流可能是ReAct，也可能直接是LLM了。\n面试官问题比较模糊，简历对齐问题。什么叫其他工作流，工作流编排不是随心所欲的吗，怎么定义其他两个字\n当用户咨询业务相关问题和处理故障问题的流程是怎么样的？基于 ReAct 模式实现了对话 Agent，实现业务咨询、告警自救、工单预处理等场景无缝切换，怎么实现的？ 直接看题库吧，一个是对话Agent，一个是运维Agent\n中间一些问题，这种题库有的，以后请不要问了\n项目中多个Agent怎么交互的？比如对话、知识库、运维Agent之间怎么交互的 对话、运维 Agent是独立的，他们不交互\n知识库不算Agent\n📷 [图片 token=Ttdcbf7WyokKbvxPid3cV9JYnqd（未能下载，见飞书原文）]\n大模型怎么处理幻觉 题库里有 大模型输出安全问题 这个问题太宽泛了，我猜测是想问敏感词怎么过滤，这个网上搜一下常见做法 2026-03-24 面试题目分享，题库里面都有 📷 [图片 token=MXtLbENvpowQ14xMcqBcekcZnwg（未能下载，见飞书原文）]\n2026-03-30 📷 [图片 token=Q0vLbFA4qo4b1hx2FN0cdu7Knxd（未能下载，见飞书原文）]\n你的向量数据库存了多少记录 这个问题要牢记你的项目立意，如果是值班场景，你的db里面其实就存了告警处理手册，不会很多。服务数量 * 20 来算，我感觉差不多了。谨慎牛逼吹太大和项目立意不符合 你是多路召回还是单路，有没有做关键词检索 能问出这个问题的，面试官还是听懂的，建议保守点回答\n单路召回，只用的余弦相似度\n有没有遇到过性能问题？ 没有遇到过\n我们这个项目只是内部使用，数据量并不大\n2026-04-01 📷 [图片 token=DRcxbvx2HooIaWxPAh7cQEX6nnd（未能下载，见飞书原文）]\n有了skill之后，你的项目那里可以改进 优化点就是RAG去掉，改成skill\n不同的问题就是不同的skill\n本身我们RAG就是想把怎么处理告警的SOP拿出来，现在有了skill后，天然的可以替换掉RAG了\n📷 [图片 token=FB8Ybo3uGocJs3x2R7Ucp30Bn4b（未能下载，见飞书原文）]\n公司内部的服务，为什么会通过腾讯云收集 好智障的问题啊，日志上传到cls或者sls上面，可以用云服务商的能力检索日志呀\n如果不用云，那就是自建ELK这种东西。现在基本都选择托管到云服务商了～自建ELK也需要存储和运维成本的\n日志是怎么上传到云服务的？ 在服务器上安装一个日志采集器，配置采集路径，就完事了\n建议去实操一把，1块钱都用不到。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/%E9%9D%A2%E7%BB%8F%E5%88%86%E4%BA%AB%E8%AE%B0%E5%BD%95/","summary":"看到很多同学在飞书群里会分享面经，风哥导师也会处理答疑，所以这个文档后面就会收录，群里发过的面经，方便大家后续复习。 本文更倾向于记录面经与回答思路～  2026-03-13  面试题目分享 我理解这属于分享，题库都有答案的 \u0026lt;image","title":"面经分享记录"},{"content":"OncallAgent 是一个本地优先 AIOps Agent 工作台。文档索引和智能诊断都可能持续数秒乃至更久，如果把它们直接塞进一次 HTTP 请求，浏览器断线、请求超时或后端重启都会让执行状态变得含糊。当前实现因此把“业务任务记录”和“可调度后台作业”分开：业务记录描述文档索引或诊断的领域状态，通用 background job 则负责排队、领取、租约、尝试次数、超时、取消与恢复。\n📷 [图片 token=FjxZbfeEwoGpSAxCI10ccKYHnmd（未能下载，见飞书原文）]\n这里的 durable 不是指引入了外部消息队列，而是把调度所需状态放进 SQLite。Worker 只是状态机的执行者；进程消失后，作业记录仍在，过期租约会在下一次领取时被回收。这个设计很适合本地优先工作台：部署简单，同时保留可观察、可恢复的执行语义。\n📷 [图片 token=FdEvbzKN3oyIvtxe9jYc4yG2nkh（未能下载，见飞书原文）]\n理解这套机制时要区分三件事：租约解决“谁现在有权完成作业”，重试解决“失败后何时再次执行”，owner scope 解决“谁能查看和控制作业”。三者落在不同层次，不能用其中一个替代另外两个。\n📷 [图片 token=UEctbJhaVoouoWxR5Sic46Kenfh（未能下载，见飞书原文）]\n学习目标 理解从业务 API 创建任务，到 durable job 入队，再到注册 handler 执行的完整链路。\n掌握 SQLite 租约的领取、心跳续租、过期恢复和 worker 身份校验。\n分清同一 job 的自动重试与创建新 job 的人工重试。\n识别协作式取消、超时、失败文本和 owner 隔离的真实边界。\n功能入口与完整调用链 通用观察入口位于 apps/backend/src/super_ai/api/app.py：GET /background-jobs 列出当前用户作业，GET /background-jobs/{job_id} 读取详情，POST /background-jobs/{job_id}:cancel 请求取消，POST /background-jobs/{job_id}:retry 为 failed 或 cancelled 作业创建新作业。四个路由都从认证依赖取得 user.id，再把它作为 owner_user_id 传给 Repository。\n📷 [图片 token=Vb0obEVjHoLywuxsZ0Gc6jLTnCg（未能下载，见飞书原文）]\n以文档索引为例，create_document_index_task 先创建业务层 DocumentIndexTaskRecord，随后由 DurableDocumentIndexTaskScheduler.schedule 查询同一 owner、resource type 和 resource ID 下是否已有 job。没有时，它入队一个 kind 为 document_index、resource type 为 document_index_task 的作业，然后启动共享 BackgroundJobRuntime。应用初始化时，create_app 已将 document_index 和 aiops_diagnosis 两个 kind 注册到对应 handler。\n📷 [图片 token=SLc2b5oCgomJGMxVMqscc4B1nDc（未能下载，见飞书原文）]\nWorker 循环调用 SQLiteBackgroundJobRepository.claim_next。领取成功后，Runtime 根据 kind 找到 handler，启动心跳任务，以 asyncio.wait_for 执行并施加作业自身的 timeout_seconds。成功调用 mark_succeeded；协作式取消调用 mark_cancelled；其他异常进入 handle_failure，由尝试次数决定重新排队还是进入 failed。\n📷 [图片 token=RQLpbox8Koc0IqxpNjSckjUXnnn（未能下载，见飞书原文）]\n受保护业务 API → 创建 owner-scoped 业务任务 → DurableDocumentIndexTaskScheduler.schedule → BackgroundJobRepository.enqueue → BackgroundJobRuntime.start → claim_next 获取租约并增加 attempt → kind 对应的 handler → succeeded / queued-for-retry / failed / cancelled 📷 [图片 token=Ro8jbxSdroDCjXxwcV5cPwOunnb（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/jobs/runtime.py BackgroundJobRuntime、BackgroundJobContext、JobCancelled Worker 并发循环、handler 注册、超时、心跳与协作取消。 apps/backend/src/super_ai/memory/repositories.py BackgroundJobRecord、BackgroundJobRepository 定义不暴露 SQLAlchemy 的持久化边界和作业数据形态。 apps/backend/src/super_ai/memory/extended_sqlite.py SQLiteBackgroundJobRepository 实现入队、领取、续租、事件、取消、完成、失败和人工重试。 apps/backend/src/super_ai/memory/models.py BackgroundJobModel、BackgroundJobEventModel 保存作业状态、租约字段以及按 sequence 排序的事件。 apps/backend/alembic/versions/202607110005_add_background_job_runtime.py upgrade 创建 background_jobs、background_job_events 及 owner、状态、租约索引。 apps/backend/src/super_ai/api/app.py DurableDocumentIndexTaskScheduler、后台任务路由、_document_index_job_handler 把领域任务接到通用 runtime，并暴露受保护控制面。 packages/api-contracts/src/background-jobs.ts BackgroundJobStatus、BackgroundJob 共享 queued、running、succeeded、failed、cancelled 及时间和尝试字段。 packages/api-contracts/src/openapi.ts /background-jobs 系列路径 描述列表、详情、取消和重试的 HTTP 合同。 apps/backend/tests/test_extended_capabilities.py test_background_runtime_recovers_leases_persists_events_and_retries 验证执行、事件、租约恢复、取消、新 job 重试和跨 owner 不可见。 openspec/specs/background-job-runtime/spec.md Durable owner-scoped background jobs 规定租约恢复、注册 handler、持久事件与取消重试语义。 📷 [图片 token=AoDhb9jaBoWlFoxBRG7c1hi2n0c（未能下载，见飞书原文）]\n代码调用流程图 这张图强调 durable job 的两个核心事实：作业状态先落 SQLite，worker 再凭租约领取；异常是否重试由 attempt 和 maxAttempts 决定。\n📷 [图片 token=Vcw9b8DRboyXbXxyefCcNqLfn1b（未能下载，见飞书原文）]\n📷 [图片 token=PioTbFPbuoI1QJx0Rprc2lywnFc（未能下载，见飞书原文）]\n关键实现拆解 租约领取与进程重启恢复 **看什么：**先看领域 scheduler 如何把稳定的业务 task ID 写进 durable job；这一步解释了 worker 重启后为何仍能重新定位原业务任务。\n📷 [图片 token=ZwnybhIT6otBDUx4cT0cFbGunYg（未能下载，见飞书原文）]\nasync def schedule(self, *, owner_user_id: str, task_id: str) -\u0026gt; None: repository = _background_job_repository_from_app(self._app) # 1. 去重键同时带 owner、resource type 和 resource id。 existing = await repository.find_for_resource( owner_user_id=owner_user_id, resource_type=\u0026#34;document_index_task\u0026#34;, resource_id=task_id, ) if existing is None: # 2. payload 只保存恢复 handler 所需的稳定 task ID。 await repository.enqueue( owner_user_id=owner_user_id, job_id=f\u0026#34;job_{uuid4().hex}\u0026#34;, kind=\u0026#34;document_index\u0026#34;, resource_type=\u0026#34;document_index_task\u0026#34;, resource_id=task_id, payload={\u0026#34;taskId\u0026#34;: task_id}, max_attempts=3, timeout_seconds=900, ) await _background_job_runtime_from_app(self._app).start() 📷 [图片 token=Zy1GbLOoEo5jI6xmrMhcoxEMnYe（未能下载，见飞书原文）]\n这段代码证明 HTTP 请求并不持有实际执行：请求只确保 owner-scoped job 已落库并启动共享 runtime。进程重启时可恢复的是这条持久记录；如果 enqueue 自身失败，业务 task 与通用 job 仍不是一个跨表原子事务。\n📷 [图片 token=ZAOCbrcuDoTt2IxV8axchTOenzf（未能下载，见飞书原文）]\n**看什么：**再看 claim_next 的同一事务怎样先回收过期租约，再按可用时间挑选一条 queued 记录，并把 worker 身份写回。\n📷 [图片 token=PBMMbGU15oBHppxziuAchlggnNb（未能下载，见飞书原文）]\nasync with self._session_factory() as session, session.begin(): # 1. 过期 running 记录先恢复为可领取状态。 await session.execute( update(BackgroundJobModel) .where( BackgroundJobModel.status == \u0026#34;running\u0026#34;, BackgroundJobModel.lease_expires_at.is_not(None), BackgroundJobModel.lease_expires_at \u0026lt; claimed_at, ) .values( status=\u0026#34;queued\u0026#34;, lease_owner=None, lease_expires_at=None, available_at=claimed_at, updated_at=claimed_at, ) ) # … 省略按 available_at、created_at 选取最早作业的代码 if row is None: return None # 2. 领取复用同一 job，并增加累计 attempt。 row.status = \u0026#34;running\u0026#34; row.attempt += 1 row.lease_owner = worker_id row.lease_expires_at = lease_expires_at row.started_at = row.started_at or claimed_at 📷 [图片 token=YKW7bMrFhoyoIGxQuIZcQko5nsL（未能下载，见飞书原文）]\n这里的恢复条件只依赖租约时间，不探测旧进程是否存活。worker 身份会继续被 renew_lease、完成和失败转换校验，因此失去租约的旧 worker 不能合法覆盖后来领取者；SQLite 事务提供的是本地实现的领取边界，不能自动外推到任意多节点数据库。\n📷 [图片 token=X8QCbvJthoI9U9x9MC5cgXYcnkg（未能下载，见飞书原文）]\nenqueue 创建作业时将状态设为 queued、attempt 设为 0、租约字段置空，并给出 available_at。claim_next 在一个数据库事务中先把租约已过期的 running 记录改回 queued，再选择最早可用作业。被选中的行切到 running，attempt 增加 1，写入 lease_owner 与 lease_expires_at，并只在首次领取时设置 started_at。\n📷 [图片 token=DVxjbSMpUofn9jxN1NqcFRYOnJc（未能下载，见飞书原文）]\n这意味着重启恢复不是“新建一次尝试记录”，而是保留同一个 job，释放过期租约后再次领取；attempt 会从 1 增至 2。当前表没有独立的 attempt 子表，历史尝试以累计次数和状态字段表达。因此文章或 UI 不应声称可以查看每次 attempt 的完整异常明细。\n📷 [图片 token=EEjzbUqKpovF3Vx7h8Mcfn4KnFb（未能下载，见飞书原文）]\nRuntime 将最小租约限制为 6 秒，默认 30 秒；心跳间隔是租约时长的三分之一。renew_lease 只有在 job 仍是 running 且 lease_owner 等于当前 worker ID 时才续租。类似地，完成和失败转换也校验 worker ID，过期 worker 无法覆盖后来领取者的终态。\n📷 [图片 token=SgMubWvJYocTlUxg1IAcSvrVnBf（未能下载，见飞书原文）]\n自动重试、人工重试与超时 **看什么：**下面把 Runtime 的三条终态分支放回重试主题中阅读，重点观察超时、取消和普通异常何时由 Repository 决定同一 job 的下一状态。\n📷 [图片 token=Um89b8GqhoSwjlxd9tacfh8dnDl（未能下载，见飞书原文）]\ntry: # 1. handler 前后检查协作式取消，并施加 job 自身超时。 await context.raise_if_cancelled() await asyncio.wait_for(handler(context), timeout=job.timeout_seconds) await context.raise_if_cancelled() except JobCancelled: await self._repository.mark_cancelled(job_id=job.id, worker_id=worker_id) # … 省略结构化取消日志 except Exception as exc: # 2. 自动重试复用当前 job，退避最多 30 秒。 retry_at = _utc_now() + timedelta(seconds=min(30, 2**job.attempt)) updated = await self._repository.handle_failure( job_id=job.id, worker_id=worker_id, error_message=_safe_error(exc), retry_at=retry_at, ) # … 省略结构化失败日志 else: # 3. 只有正常返回且未收到取消请求才标记成功。 await self._repository.mark_succeeded(job_id=job.id, worker_id=worker_id) 📷 [图片 token=PN2hbolqwoe9JpxtkVOcRRDjn1K（未能下载，见飞书原文）]\n这证明自动重试并不创建新 ID，人工 retry 才复制源作业形成 retry_of_job_id。所有终态写入仍要求匹配 worker_id；超时虽然能取消 await，但不能保证已经进入线程或外部系统的同步副作用被撤销。\n📷 [图片 token=PxpJbvYOFo4gT7xsw6yct28fnme（未能下载，见飞书原文）]\n每次领取都会增加 attempt。handler 抛出异常时，Runtime 计算最长 30 秒的指数退避：当前实现使用 min(30, 2 ** attempt) 秒。Repository 的 handle_failure 在 attempt 小于 max_attempts 且没有取消请求时，把同一 job 重新置为 queued 并更新 available_at；否则写入 failed 和 completed_at。默认 max_attempts 为 3。\n📷 [图片 token=QLSfb8bXnoBLmaxbhxccAyAanUm（未能下载，见飞书原文）]\n人工重试走另一条路径。SQLiteBackgroundJobRepository.retry 只接受 failed 或 cancelled 的源作业，复制 kind、resource、payload、最大次数与超时，生成新 job，并用 retry_of_job_id 连接来源。这使“自动重跑同一 job”和“用户明确创建一个新尝试”在数据上可区分。\n📷 [图片 token=VYHMb06XAoEV1nxpephcDz0onK3（未能下载，见飞书原文）]\nasyncio.wait_for 为 handler 提供硬超时边界。超时被转成固定文本 Background job timed out.。其他异常由 _safe_error 取字符串并截断到 1000 个字符；它并不是通用凭据脱敏器，所以 handler 仍必须抛出经过清理的领域错误，不能把上游原始响应直接带入异常。\n📷 [图片 token=XgFJbQjaFoiXKOxRA3RcTbdAnXf（未能下载，见飞书原文）]\n事件与协作式取消 **看什么：**看取消请求如何根据当前状态分流：queued 立即终止，running 只记录请求时间，等待 handler 在安全边界读取。\n📷 [图片 token=Mo03bCvCsoA7HkxMoMbcY5cCnAb（未能下载，见飞书原文）]\nasync with self._session_factory() as session, session.begin(): row = await session.get(BackgroundJobModel, job_id) # 1. 不存在或跨 owner 都不返回作业详情。 if row is None or row.owner_user_id != owner_user_id: return None if row.status == \u0026#34;queued\u0026#34;: # 2. 尚未领取的作业可以直接进入终态。 row.status = \u0026#34;cancelled\u0026#34; row.completed_at = now elif row.status == \u0026#34;running\u0026#34;: # 3. 已运行作业只设置协作式取消标记。 row.cancel_requested_at = now row.updated_at = now 📷 [图片 token=Vc8pbiNYIoqJVLxYCPAcO98vnrg（未能下载，见飞书原文）]\n这段实现没有强杀 worker。running job 只有在 Runtime 或业务 handler 再次调用取消检查时才会停下，因此一次正在进行的 embedding、Milvus 或其他不可取消调用可能继续到返回；owner 条件则阻止用户借 job ID 控制他人的任务。\n📷 [图片 token=IquTbaeQnoBzZPxQaZMcS9lpnVp（未能下载，见飞书原文）]\nBackgroundJobContext.append_event 将事件写入 background_job_events。Repository 在事务中读取当前最大 sequence，加一后写入，并通过 owner、job、sequence 索引支持 after_sequence 断点读取。AIOps handler 在每个诊断事件后持久化事件，因此浏览器断线不等于任务停止。\n📷 [图片 token=UCCsbrAGjoeIPaxzk87cnuMrnAg（未能下载，见飞书原文）]\n取消是协作式的。queued job 会立即变为 cancelled；running job 只写 cancel_requested_at。Runtime 在 handler 前后检查一次，具体 handler 也可在安全边界调用 raise_if_cancelled。AIOps handler 在事件循环中反复检查，文档索引 handler 则在进入索引服务前检查；它不会强行中断正在进行的一次 embedding 或 Milvus 调用。API 的 _cancel_background_resource 还同步更新对应的文档索引任务或诊断任务业务状态。\n📷 [图片 token=TLfTbClkcohotixcXO5c624vnGh（未能下载，见飞书原文）]\n用一次故障过程理解状态机 **看什么：**这张局部状态图只画通用 job 的持久状态；业务 task 的 running、failed、succeeded 在另一张表中更新，不能与这里误认为同一次提交。\n📷 [图片 token=FTobbJHafogzLtxkqpJcdujunIc（未能下载，见飞书原文）]\n📷 [图片 token=AOKsbAVYsoGAQ3x0zAEcO3G3ntd（未能下载，见飞书原文）]\n图中 running 回到 queued 有两种不同原因：失败退避会把 available_at 推迟，租约回收则在下一次领取事务中恢复可用。两条路径都保留同一 job；如果业务记录已先更新，短时间观察到两个表状态不一致是当前设计允许的瞬态。\n📷 [图片 token=COzab5NVmoddLixcuvfct8Vlnif（未能下载，见飞书原文）]\n可以用“文档索引首次调用 embedding 超时、第二次成功”来串起全部字段。API 创建业务索引任务后，scheduler 入队 job，此时 status 是 queued、attempt 是 0、available_at 是当前时间。某个 worker 领取后，status 变为 running，attempt 变为 1，写入 worker 专属租约。handler 内部索引服务会先把业务任务设为 running；embedding 超时后，业务任务先记录 failed，handler 再抛出异常给通用 runtime。\n📷 [图片 token=UYY2bhG70ovmhcxrkUqcwRBEnlc（未能下载，见飞书原文）]\n通用 runtime 捕获异常后不会创建新 job。只要 attempt 尚未达到 max_attempts，它会清空租约，把同一 job 放回 queued，并把 available_at 推到退避时间。下一次领取把 attempt 增至 2。文档索引 handler 再次执行时会重新读取同一 owner 下的业务任务和文档；当前服务允许它从 failed 业务任务重新进入 running。第二次成功后，文档成为 indexed，业务任务成为 succeeded，通用 job 最后成为 succeeded。这个顺序也说明短时间内可能观察到“业务记录已成功、通用 job 仍是 running”的瞬态，消费者不应把多个表假设为一次原子提交。\n📷 [图片 token=GnmubD7uPoLnWTxRsm1cbHQPnNg（未能下载，见飞书原文）]\n若进程在第二次执行中直接退出，SQLite 中仍保留 running 和租约到期时间。新进程启动 worker 后，第一次 claim_next 会先回收所有过期 running 记录，再选择可用 job。它不会依据旧进程是否真的死亡做网络探测，而是完全信任租约时间。因此机器时钟和数据库时间语义必须稳定；当前实现统一使用带 UTC 时区的 datetime，Repository 也把更新时间写回同一模式。\n📷 [图片 token=UzZabF1ckoBk3HxbMtrcxMChn49（未能下载，见飞书原文）]\n并发限制与调度公平性 **看什么：**看 Runtime 如何把 concurrency 直接展开成固定数量的 worker task；每个 worker 都要等当前 handler 完整结束后才领取下一条。\n📷 [图片 token=DlJobKsR2oxhGIxaNZLcq3wGn3d（未能下载，见飞书原文）]\nasync def start(self) -\u0026gt; None: # 1. 已有 worker 时直接返回，重复 start 不扩容。 if self._workers: return self._stopping.clear() self._workers = [ asyncio.create_task(self._worker_loop(index), name=f\u0026#34;background-worker-{index}\u0026#34;) for index in range(self._concurrency) ] # … 省略 stop 方法 async def _worker_loop(self, index: int) -\u0026gt; None: worker_id = f\u0026#34;{self._runtime_id}:{index}\u0026#34; while not self._stopping.is_set(): now = _utc_now() # 2. 单个 worker 一次只领取并执行一个 job。 job = await self._repository.claim_next( worker_id=worker_id, lease_expires_at=now + timedelta(seconds=self._lease_seconds), now=now, ) if job is None: await asyncio.sleep(self._poll_seconds) continue await self._execute(job, worker_id) 📷 [图片 token=ZgRMbwWwxoA6mzxiHKMczfrgn1c（未能下载，见飞书原文）]\n总并发上限就是 worker 数，默认 2；它不是按 owner 或 kind 分配的配额。停止时 task 会被取消而不是等待全部 handler 排空，因此未完成工作依赖租约恢复，外部副作用仍须由 handler 自己设计成可重入或可辨识重复执行。\n📷 [图片 token=OaqbbiqvBoUz5kx2DJTcvHC5nRg（未能下载，见飞书原文）]\nBackgroundJobRuntime 默认创建两个 worker task，构造参数可调整 concurrency，但至少为 1。每个 worker 都是“领取一个、完整执行、再领取下一个”的串行循环，因此总并发上限就是 worker 数。没有作业时，worker 按 poll_seconds 轮询，最小间隔为 0.05 秒。停止 runtime 会设置 stopping event 并取消 worker tasks；它不是优雅等待全部 handler 结束的排空协议，所以进程关闭期间未完成的工作依赖租约过期恢复。\n📷 [图片 token=QznCbiQvgolmBzxIX6QcqzHAnrg（未能下载，见飞书原文）]\nRepository 按 available_at 升序、created_at 升序领取，先到可用的作业优先。它没有按 owner 做轮转，也没有 kind 级队列、优先级或资源配额。如果一个用户短时间入队大量长作业，后续其他用户作业可能等待。当前本地单机定位和两个业务 kind 使简单策略可用，但将来扩大并发或多租户负载时，公平调度必须作为新的显式能力设计，不能从现有 owner 过滤推断已经具备。\n📷 [图片 token=UxQgbiWH0o7HM3x2shVcfFIrnXc（未能下载，见飞书原文）]\n同一个 runtime 的 start 是幂等的：已有 worker 列表时直接返回。scheduler 可以在每次任务创建后安全调用 start，而不会重复创建整组 worker。handler 注册则相反，同一 kind 重复注册会抛 ValueError，避免后注册逻辑悄悄覆盖原处理器。这一约束让应用装配阶段的问题尽早暴露。\n📷 [图片 token=VpRcb2WzFokpO4x6eWWck5N4nwc（未能下载，见飞书原文）]\n事件恢复与业务恢复不是一回事 **看什么：**用这张序列图区分“事件游标恢复”和“业务 handler 重跑”。前者只读取已提交 payload，后者由 job 状态与租约决定是否再次执行。\n📷 [图片 token=KGmWbVKd2oYn98xuzRzcmtMdnuh（未能下载，见飞书原文）]\n📷 [图片 token=WB0GbxbQcosrUSxoCUoc2eIUnKb（未能下载，见飞书原文）]\nsequence 只在单个 job 内单调递增，不能当作全局事件号。网络重连不会自动重放尚未提交的事件，也不会触发文档索引重新执行；而 handler 重跑可能产生新的业务副作用，却不保证所有 kind 都写逐步事件。\n📷 [图片 token=Z04jbqAFLogULqxup1hcX2UwnKe（未能下载，见飞书原文）]\nbackground job event 的 sequence 只在单个 job 内单调递增。list_events(after_sequence=n) 返回 n 之后的事件，适合订阅者保存游标后继续读取。事件 payload 是 JSON 字典，通用 runtime 不解释其业务含义；AIOps 可以写计划、工具、证据和报告事件，其他 kind 也能写自己的结构。事件恢复只保证曾经提交的 payload 仍可读，不会重新生成在进程崩溃前尚未提交的那一个事件。\n📷 [图片 token=EFpjbg8daoHjo0xgrK2cpUBznHh（未能下载，见飞书原文）]\n普通文档索引 handler 当前不调用 append_event，页面主要轮询 DocumentIndexTaskRecord；AIOps handler 才在流式诊断循环中持续保存事件。因此“runtime 支持持久事件”不等于每一种后台工作已经提供逐步骤事件流。通用 BackgroundJob 契约本身也只暴露任务字段，没有把事件列表合并进详情响应。\n📷 [图片 token=A71ibiG8DouK5rxrFssccpOInuh（未能下载，见飞书原文）]\n人工 retry 复制源 job 的 payload，但业务资源仍指向原 resource ID。对文档索引而言，页面通常使用领域级 retry API 新建一个业务 task，再由 scheduler 为这个新 task 建 job；通用 job retry 则直接重试同一 resource。两种入口都真实存在，产品层应根据是否需要新的业务 attempt 记录选择，不能无差别混用。\n📷 [图片 token=D2P3bL2skouEm4xwYeScbiaanUc（未能下载，见飞书原文）]\n迁移、索引与可观察字段 **看什么：**最后看 migration 中直接服务调度和恢复查询的索引，而不是只看 ORM 字段列表。\n📷 [图片 token=URE5bCvFmoDBuOxRllQcdM16nLe（未能下载，见飞书原文）]\n# 1. worker 按状态和 available_at 找可领取作业。 op.create_index( \u0026#34;ix_background_jobs_status_available\u0026#34;, \u0026#34;background_jobs\u0026#34;, [\u0026#34;status\u0026#34;, \u0026#34;available_at\u0026#34;] ) # 2. scheduler 的资源去重查询包含 owner scope。 op.create_index( \u0026#34;ix_background_jobs_resource\u0026#34;, \u0026#34;background_jobs\u0026#34;, [\u0026#34;owner_user_id\u0026#34;, \u0026#34;resource_type\u0026#34;, \u0026#34;resource_id\u0026#34;], ) # … 省略事件表字段定义 # 3. 事件断点读取按 owner、job、sequence 定位。 op.create_index( \u0026#34;ix_background_job_events_owner_job_sequence\u0026#34;, \u0026#34;background_job_events\u0026#34;, [\u0026#34;owner_user_id\u0026#34;, \u0026#34;job_id\u0026#34;, \u0026#34;sequence\u0026#34;], ) 📷 [图片 token=C3MZbiB9Qo5vuIxa5IAcSePJngu（未能下载，见飞书原文）]\n这些索引证明常用访问路径都把 owner 或状态条件落在持久层，但索引本身不是授权机制；Repository 仍必须在查询中显式带 owner。迁移还保留 payload 与 lease 内部字段，而公开契约刻意不返回它们，避免调度输入和 worker 身份泄露到客户端。\n📷 [图片 token=DuwUbqyoCongyhxipiZcQbghnTf（未能下载，见飞书原文）]\nAlembic migration 为 status 与 available_at 建联合索引，支持 worker 快速寻找可领取记录；owner 与 created_at 联合索引服务用户任务列表；owner、resource_type、resource_id 联合索引服务 scheduler 去重查找。租约 owner、租约到期时间和 retry 来源也有索引。事件表用外键关联 job，删除 job 时级联删除事件，并用 job_id 与 sequence 唯一约束保护单个事件位置。\n📷 [图片 token=JGY0bBJEkoEpktxsUGXc7JplnBc（未能下载，见飞书原文）]\n面向客户端的 BackgroundJob 有 attempt、maxAttempts、timeoutSeconds、availableAt、cancelRequestedAt、retryOfJobId、errorMessage 和完整生命周期时间。它不返回 payload，是为了避免把 handler 内部输入直接暴露；也不返回 lease 字段，避免把调度内部身份当成业务控制能力。用户能判断排队、执行、成功、失败或取消，却不能指定某个 worker 领取作业。\n📷 [图片 token=LSY8btISyoB77HxUictcVdUInib（未能下载，见飞书原文）]\n结构化可观察日志使用 background.job.started、background.job.completed、background.job.failed 和 background.job.cancelled 等事件，只记录 job ID、kind、是否最终失败和异常类别。runtime 没有记录 job payload 或 handler 输出。日志用于排查执行生命周期，SQLite 才是任务状态事实来源；进程日志丢失不会改变恢复语义。\n📷 [图片 token=D28PbuGriocD94xouvkcuIHdncb（未能下载，见飞书原文）]\n如果未来把 SQLite Repository 替换为其他数据库，业务 handler 和 Runtime 构造签名可以保持不变，但 claim_next 的并发领取实现必须重新证明原子性。当前“事务中先回收、再选择、再修改”适配本地 SQLite；多节点数据库可能需要行锁、跳过已锁行或原子更新返回。Repository abstraction 提供替换边界，并不自动保证任意实现都具备同样租约安全性。\n📷 [图片 token=Wp01btS82oVxbkxhRw7cPhu2nvf（未能下载，见飞书原文）]\n数据、契约与状态 BackgroundJobRecord 包含身份和路由字段 id、owner_user_id、kind、resource_type、resource_id；调度字段 status、attempt、max_attempts、timeout_seconds、available_at、租约；控制与追踪字段 cancel_requested_at、retry_of_job_id、error_message 和四类时间戳。共享 TypeScript 契约不向客户端暴露 lease owner 和 lease expiry，这是内部调度细节。\n📷 [图片 token=ZHy0bC8kSol8CXxGnQbcE5Xgnqb（未能下载，见飞书原文）]\n合法终态是 succeeded、failed、cancelled。queued 可以被取消、被领取或在失败退避后再次出现；running 可以成功、失败、收到取消请求，也可能因租约过期被回收到 queued。业务任务状态并不自动由数据库外键同步，而是 handler 与 API 显式维护。例如文档索引服务会更新 DocumentIndexTaskRecord，通用 runtime 再维护关联 background job。\n📷 [图片 token=K8G7bJybzoB593xMIlMcY2Dbnae（未能下载，见飞书原文）]\n权限、安全与失败边界 面向用户的 get、list、find、cancel 和 retry 都要求 owner_user_id。跨用户读取返回空，API 再映射为统一 AUTH_FORBIDDEN，不会暴露目标是否存在。事件追加先验证 job owner；事件列表也同时过滤 owner 与 job。系统 worker 的 claim_next 不带 owner，这是内部全局调度入口，但它返回记录后，handler 始终从 job 自身取得 owner 并继续向下传递。\n📷 [图片 token=YYJJbqr4ooA2R1xw3B9cqmpJnYe（未能下载，见飞书原文）]\n租约提供的是至少一次执行倾向，而不是分布式事务或恰好一次保证。进程可能在外部副作用完成后、终态提交前退出，租约过期后 handler 会再执行。因此具体 handler 需要使用范围删除、幂等写入或业务记录检查来降低重复执行风险。当前文档索引正是在插入前按 tenant、knowledge base、document 删除旧 chunks。\n📷 [图片 token=PaXdbrE7XoC8mixSaGucvDOCnPc（未能下载，见飞书原文）]\n运行中取消还有一个必须如实说明的竞态：通用取消 API 会立即把关联文档索引任务标成 cancelled，但文档 handler 在进入 run_task 后没有在 embedding 与 Milvus 步骤之间继续检查；索引任务 Repository 的完成转换也不要求前态。因此一次已在执行的索引可能稍后把业务任务改为 succeeded，而 runtime 在 handler 返回后的取消检查又把通用 job 改为 cancelled。当前代码没有把这两个记录包进同一条件事务，观察者需要分别看 job 与业务任务，不能假定它们在中途取消时始终同态。\n📷 [图片 token=OF6Mb6OKzoPMNHxQhyAct3PrnKe（未能下载，见飞书原文）]\n没有已注册 handler 的 kind 会进入失败处理，而不是被静默丢弃。事件表有 job 与 sequence 的唯一约束，但 append_event 使用“读取最大值再加一”，其正确性依赖 SQLite 事务串行化；若未来切换多节点数据库，需要重新审视高并发事件分配策略。\n📷 [图片 token=BQFvbLLbSoRLGHxyFSfcrCApn9c（未能下载，见飞书原文）]\n阅读顺序与小结 先读 BackgroundJobRecord 与 BackgroundJobRepository，建立状态字段词汇。\n再读 SQLiteBackgroundJobRepository.claim_next、renew_lease 和 handle_failure，理解状态转换的原子边界。\n随后读 BackgroundJobRuntime._worker_loop 与 _execute，观察 Repository 如何被执行器驱动。\n最后从 create_app 的路由、scheduler 和两个 handler 回看业务集成，并沿状态机核对失败、重试和取消边界。\n这套实现的核心不是“后台开一个 asyncio task”，而是把调度真相放进 SQLite：worker 可替换，租约可恢复，尝试可计数，控制操作有 owner 范围。与此同时，它明确保留了本地优先方案的现实边界：取消需要合作、外部副作用需要幂等、失败消息清理仍由各领域共同承担。读懂状态转换，比记住某个接口名称更重要。\n📷 [图片 token=ETocbrr3toCCsixYlvtcaPI5ngd（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/06.%20SQLite%20Durable%20Background%20Job%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/","summary":"OncallAgent 是一个本地优先 AIOps Agent 工作台。文档索引和智能诊断都可能持续数秒乃至更久，如果把它们直接塞进一次 HTTP 请求，浏览器断线、请求超时或后端重启都会让执行状态变得含糊。当前实现因此把“业务任务记录”和","title":"06. SQLite Durable Background Job 运行机制"},{"content":"背景 大家好，我是小林。\n凡事加入《智能 OnCall Agent 项目》的林友，可以直接找下小林进行** 1 对 1 offer选择**哈！\n如果你通过学习这个项目之后，拿到了多个offer，但是不知道如何选择，可以把 offer相关的信息汇总给小林，我会针对你的情况给予 offer 选择的意见哈。\n因为做训练营求职辅导的缘故，小林对实习/校招/社招的 offer 选择积累了 1000+ 的经验，所以知道哪些是好的机会，也知道哪些可能是个坑，通过我这几年积累的经验，尽量帮助大家选择一个更有价值的 offer。\n有 offer 选择困惑的同学，可以扫码下方的微信二维码直接加小林微信（或者微信搜索：xiaolincoding）：\n记得要备注【vip】，方便我知道你是课程会员同学\n然后先发oncall飞书群截图，方便我知道你是oncall的学员\n最后把offer信息发给我就行，看到之后会把我的想法和意见发给你。\n📷 [图片 token=UY22bER4QoUM88xwAfNcADLonif（未能下载，见飞书原文）]\n喜报分享 双非大二拿到字节实习offer，补上项目之后面试机会变多了 📷 [图片 token=YuWkb9Cc6o4ztexlxd4caJgynFn（未能下载，见飞书原文）]\n📷 [图片 token=Dm4dbJk7dokIiUxjyoEcvqwFnGb（未能下载，见飞书原文）]\n211 大二，用了 oncall agent 项目直接，拿到了两个大厂 offer 实习 📷 [图片 token=JeglbsoqKo24zqxA16JcQ8minlg（未能下载，见飞书原文）]\n📷 [图片 token=Ej3ybU5vAo0ahBxXxkbcWNpyn1b（未能下载，见飞书原文）]\n写上on call agent项目之后，面试机会多了很多，也顺利拿到了腾讯offer 📷 [图片 token=K5xYbXUATol1v7xfXedcdjUCnsd（未能下载，见飞书原文）]\n补上oncall agent项目，拿到5个大暑期实习 📷 [图片 token=Os0IbJR44olDFwx55Iycc2b5nQb（未能下载，见飞书原文）]\n📷 [图片 token=ARfRbZBaqojXYDxmcENcEsOEngg（未能下载，见飞书原文）]\n拿到大厂ai算法岗位！ 📷 [图片 token=PGGebfPAboE4kMxNVnScGjYHnfc（未能下载，见飞书原文）]\n3年社招java后端补上oncall agent项目，拿到腾讯agent开发+字节后端offer 📷 [图片 token=Eh8jb7ZiXoxSp6xNShYcjsD8ndf（未能下载，见飞书原文）]\n字节+京东2个大厂 ai 业务的offer 📷 [图片 token=QLy7b5FMSoOBtuxwQHocohe8n7g（未能下载，见飞书原文）]\n腾讯t8社招大佬，认可oncall agent项目！ 📷 [图片 token=GSx2bjPKco2Z4sxgo55cdB3xnwt（未能下载，见飞书原文）]\n拿到京东大厂实习+蚂蚁 agent开发实习offer 📷 [图片 token=XF95bhMonoSJ4yx14SEccSklnOb（未能下载，见飞书原文）]\njava后端补上oncall agent项目，转python agent 成功！ 📷 [图片 token=QCJIbMONYodbzNxahPEcQuionSb（未能下载，见飞书原文）]\n社招 Java ，包装到了简历，拿到了两个 20-30k 薪资范围 offer 📷 [图片 token=JeT3bfGyDoabFbxfE9icGXPCnUH（未能下载，见飞书原文）]\n拿到大厂实习，项目的面试遇到的问题，文档也都覆盖了 📷 [图片 token=AD5MbDRRVoflRFx5IMEcOyBSncb（未能下载，见飞书原文）]\nJava后端校招，补上oncall agent项目，拿到中厂ai实习 📷 [图片 token=Osisbkotko1e6yxRwgYcSi5jnVe（未能下载，见飞书原文）]\n找到 AI 应用开发实习，实习300/天，堪比大厂实习薪资 📷 [图片 token=OEzTbAgV8oO8FRx9dxjcS0Acnyg（未能下载，见飞书原文）]\n📷 [图片 token=JqGwbbo2lo1w1HxpmDpcd1lbnbf（未能下载，见飞书原文）] 📷 [图片 token=P09ZbvIx3o1kB0xuhazczaKxnhF（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B9%9D%E7%AB%A0%20_%20%E9%9D%A2%E8%AF%95%E6%B1%82%E8%81%8C%E5%85%A8%E6%94%BB%E7%95%A5/1v1%20offer%E9%80%89%E6%8B%A9%EF%BC%88%E9%99%84%E5%96%9C%E6%8A%A5%EF%BC%89/","summary":"背景 大家好，我是小林。 凡事加入《智能 OnCall Agent 项目》的林友，可以直接找下小林进行   1 对 1 offer选择 哈！ 如果你通过学习这个项目之后，拿到了多个offer，但是不知道如何选择，可以把 offer相关的信息","title":"1v1 offer选择（附喜报）"},{"content":"tasks.md 是从规划进入执行的导航图。proposal 已经确定范围，delta spec 已经定义行为，design 已经说明方案，tasks 再把这些信息转换成可以逐项实现、验证和勾选的工作。\n它既不是简单待办清单，也不是项目进度表截图。每个任务都应该指向一个可交付结果，并能用代码、测试或命令证明完成。Codex 的 apply 流程会读取未完成项，按顺序推进，并在每项完成后立即更新复选框。\n📷 [图片 token=VdJyb1xLsoqOAIxhOJAcVDcYnuc（未能下载，见飞书原文）]\n典型组织结构 ## 1. Embedding 批处理兼容 - [ ] 1.1 将默认客户端单批数量限制为 10 - [ ] 1.2 增加客户端批量行为单元测试 ## 2. 文档索引回归覆盖 - [ ] 2.1 增加超过 10 个 chunk 的索引回归测试 - [ ] 2.2 运行 Ruff、Pyright、Pytest 和 OpenSpec 验证 一级分组按照纵向交付范围组织，而不是机械按“后端、前端、测试”分开。主案例第一组完成 Provider 兼容性，第二组证明整个索引流程没有丢数据。编号让讨论和更新更精确，复选框保存执行状态。\n📷 [图片 token=XX1FbGg7UoeMXgxIDv9cyipwnye（未能下载，见飞书原文）]\n任务粒度怎样才合适 过粗的任务，例如“完成 Embedding 修复”，无法判断包含哪些代码、测试和验收；过细的任务，例如“打开 provider.py”“输入一行常量”，又会把执行过程切得支离破碎。\n合适的任务通常对应一个可以独立说明的结果：建立一项约束、同步一个契约、增加一组测试、完成一次迁移或运行一组门禁。它不一定等于一次 Git commit，但应该能够在完成后马上验证。\n主案例的四项任务非常紧凑：实现批量上限；验证默认客户端的配置与行为；验证大文档索引完整；运行质量门禁。每一项都能与 proposal、design 和 spec 建立对应关系。\n📷 [图片 token=RQvzboKGQo81k0xj1B9cxQw1nto（未能下载，见飞书原文）]\n为什么测试和验证必须写进 tasks 如果 tasks 只列“写代码”，Codex 很容易在实现结束时提前宣布完成。把测试与验证写成显式任务，意味着交付定义从“文件已修改”提升为“行为已有证据”。\nOncallAgent 的检查要按影响范围选择。Python 后端通常涉及 Ruff、strict Pyright 和 Pytest；前端涉及 TypeScript 类型检查、Vitest 和构建；API/SSE 变化至少还要检查共享契约和双方消费；数据库变化要验证 Alembic upgrade；OpenSpec 结构运行 openspec validate --all；WIKI 变化运行 npm run docs:build。\n不能为了勾选任务而删除测试、弱化断言或跳过类型检查。没有环境或外部服务时，应把验证标为未执行并说明原因，而不是把计划命令写成通过结果。\n📷 [图片 token=Ui6xbVvcvoA6ByxWYyKcH5Hen9g（未能下载，见飞书原文）]\napply 怎样使用 tasks openspec-apply-change 先读取 openspec status 和 openspec instructions apply 返回的 contextFiles，再了解总任务数、已完成数和动态指令。它不能只读 tasks 而忽略 proposal、design 和 specs，因为同一句任务需要这些上游产物解释意图和边界。\n执行循环是：选择一个 pending task；完成最小且聚焦的修改；运行相关验证；把 - [ ] 改为 - [x]；继续下一项。需求不清、实现暴露设计问题或验证失败时，应暂停并回到产物修正，而不是为了清空清单继续猜。\n📷 [图片 token=LFItboLTQo57XnxdJRLcJhPYnJe（未能下载，见飞书原文）]\n复选框能证明什么，不能证明什么 [x] 表示该 change 的历史记录声称任务已经完成。它有助于恢复进度和归档检查，但不能独立证明当前代码仍然通过测试。代码可能在后续 change 中演进，运行环境也可能改变。\n主案例 tasks 的四项均已勾选，这是归档时的历史状态。本次教学整理没有重新运行 Ruff、Pyright、Pytest 和 OpenSpec CLI，所以不能把历史勾选表述成本轮验证结果。\n📷 [图片 token=XWjAbtyyzo0ZMvxJ3lHc9ALVnJe（未能下载，见飞书原文）]\n怎样从 spec 反推 tasks 可以逐个 Scenario 问：需要哪一层实现、哪一层测试、是否有配置或迁移、是否影响契约和 UI。主案例三个 Scenario 对应：\n小输入单批与大输入 10+1 拆分，需要 Provider 行为测试；保持向量顺序，需要对输出顺序断言；大文档 succeeded 且全部 chunk 落库，需要索引服务回归测试；所有变化还要经过静态检查和 OpenSpec 验证。\n这种映射能发现漏项。如果 spec 有越权拒绝场景，而 tasks 没有授权测试；或者 design 决定增加迁移，而 tasks 没有 upgrade 测试，说明任务清单还不完整。\n📷 [图片 token=TWCTbh9ZEo3RXyxFUrsctT4enqc（未能下载，见飞书原文）]\n常见错误 **先写 tasks，再补 spec 和 design。**任务会被当前直觉绑架，容易遗漏行为和边界。\n**用“完成全部开发”作为单一任务。**无法追踪，也无法在中断后恢复。\n**只勾框，不保存验证依据。**复选框是进度，不是测试日志。交付说明仍需列出实际运行命令和结果。\n**把无关重构塞进任务。**tasks 必须受 proposal Impact 和 design Non-Goals 约束。\n📷 [图片 token=HJkRbS6njoyaVzxY9bBcmvernWd（未能下载，见飞书原文）]\n面试表达 tasks 不是把需求切成“后端一项、前端一项”，而是把规格与设计转换成可验收的纵向步骤。apply 每完成一项就验证并勾选，verify 再检查任务、Scenario 和实现证据。复选框保存历史进度，但我不会把它当成当前测试通过的替代品。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/OpenSpec%20%E6%A0%B8%E5%BF%83%E4%BA%A7%E7%89%A9/tasks.md%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8%E4%BB%8B%E7%BB%8D/","summary":"tasks.md  是从规划进入执行的导航图。proposal 已经确定范围，delta spec 已经定义行为，design 已经说明方案，tasks 再把这些信息转换成可以逐项实现、验证和勾选的工作。 它既不是简单待办清单，也不是项目进","title":"tasks.md文件作用介绍"},{"content":" [!NOTE] 这篇属于项目之外的读物，不学也不影响学习后续的 oncall agent项目的学习。 目的是帮助大家理解「Prompt 工程、Context 工程、Harness 工程」\n最近圈子里又冒出来一个新词，叫「Harness Engineering」，翻译过来叫「驾驭工程」。\n三天一概念，五天一炸裂，一周一个新名词。\n甚至每次 AI 圈新诞生一个很火的概念，面试就大概率会被问到，就比如有林友跟我反馈面试就被问到了「Harness 工程」\n📷 [图片 token=BAyCb75HQoP8GRxmlwycaJoJn7f（未能下载，见飞书原文）]\n你是不是已经麻了？\n但麻归麻，我还是去把这个新词的来龙去脉认认真真扒了一遍。\n扒完之后我发现，这次还真不是又一个换皮的概念，它确实是在解决一个之前那两个词都解决不了的问题。\n所以今天这篇文章，我想把这三个词一次性给你讲透，主要回答三个问题：\nHarness Engineering 到底是个啥？\n它跟 Prompt Engineering、Context Engineering 是什么关系？\nOpenAI 和 Anthropic 实现 Harness 遇到了什么难点？\n文章会有点长，但我尽量讲得通俗一点，你泡杯茶慢慢看。\n这个新词到底从哪儿冒出来的？ 在开讲之前，我想先带你去「追个源」。\n因为我发现很多人在跟风聊 Harness Engineering 的时候，压根没搞清楚这个词最早是谁提出来的。等你知道了它是谁先喊出来的，你就会明白为啥它这次真的能火，而不是又一个换皮概念。\nHarness Engineering 这个词最早被正式叫响，是在 2026 年 2 月 5 号一篇博客里。作者叫 Mitchell Hashimoto。\n📷 [图片 token=TX5BbjtNhoOMdxxX8q1cVmyWnAd（未能下载，见飞书原文）]\n标题叫《My AI Adoption Journey》，中文大概是「我接纳 AI 的历程」。\n讲的就是他作为一个老派工程师，是怎么一步一步从「不信 AI」到「每天让 Agent 替我干活」的心路历程。\n他把整个过程拆成了 6 个 Step，前面几步都挺反直觉的，我挑几个有意思的给你讲讲。\n第一步，直接扔掉对话框。他说一上来别用 ChatGPT 那种聊天界面，要用就用能自主执行任务的 Agent。为啥？因为聊天界面会给你一种「我在和 AI 讨论」的幻觉，但实际上你啥也没干，Agent 才是真正让 AI 动起来的形态。只有把 Agent 放开让它自己执行，你才能真正感知到它的能力边界在哪儿。\n第二步更狠，他强迫自己用 Agent 去重做一部分自己早就已经手写过的代码。听起来是不是有点傻？明明自己两下就能写完的活，非要等 Agent 慢慢磨。他原话说这个过程「painful」，折磨人。但目的很明确：只有你亲自用 AI 去重做你已经熟得不能再熟的任务，你才能精准校准「它到底哪里行、哪里不行」。\n然后到了第五步，他给自己这套做法起了个名字，就叫 Engineer the Harness，打磨这套马具。\n📷 [图片 token=LGSybm4Oroy0nXxfamsc2l8pnhb（未能下载，见飞书原文）]\n他给的定义特别简洁，我把原话翻译过来：\n每次当你发现 Agent 犯了一个错误，就花点时间去工程化一个解决方案，让它永远不会再犯同样的错误。\n你品品这个思路。绝大多数人遇到 Agent 犯错是怎么办的？骂两句，手动改掉，祈祷下次它别再犯。\n但 Mitchell 不是这么干的，他每次 Agent 犯错，都会停下来问自己：我能不能把这个错误永久性地修到环境里，让它下次在结构上就不可能再犯？\n可能是给 AGENTS.md 加一条规则，可能是加一个 linter，可能是补一个自动化测试，也可能是搞一个 Git Hook。反正关键是：这个修补必须沉淀到环境里，而不是留在人脑子里。\n这套做法的威力在于它是「复利」的。\n每一次 Agent 犯错，环境就会变强一点；环境变强一点，Agent 下一次就更少犯错；犯错变少，你改进的速度就更快。时间一长，你的 Harness 会越来越坚固，Agent 在你这个项目里会越跑越稳。\n📷 [图片 token=TV22blJAooqTY6xmj8vcqA1vnPc（未能下载，见飞书原文）]\n博客发出来一周后，OpenAI 紧接着也发布了一篇官方博客，标题就叫《Harness engineering: leveraging Codex in an agent-first world》。\n📷 [图片 token=CA1Fb1axLoA3c2xgbtvcMbzdnjc（未能下载，见飞书原文）]\n这篇博客讲的是他们内部一个从 3 人起步的小团队，从一个空仓库出发，用 5 个月时间，靠 Agent 写出了 100 万行代码、合并了 1500 个 PR，全程没人手动写过一行代码。\n这篇博客一出，Harness Engineering 这个概念直接被推上了 AI 圈的风口浪尖。所以你能看明白这个词的路径了吧：\n一个基础设施圈的老法师先喊出来 → OpenAI 几天后发文背书 → 一周内整个 AI 圈开始刷屏。\n这种出身决定了它不会像很多 AI 新词一样「炒一波就凉」，它更像是一个在真实工程土壤里长出来的东西。\n好，交代完来源，咱们进入正题。\n从提示词到 Harness 要真正讲清楚 Harness Engineering 在解决啥问题，咱们不能一上来就讲它。因为它不是凭空冒出来的新概念，而是 AI 工程这几年一步一步「被逼出来」的结果。\n你去复盘一下过去两年 AI 圈的变化就会发现一件特别有意思的事：AI 工程的重心，其实已经悄悄换过三次了。\n📷 [图片 token=F2F2bdNPdoib1bxaqK1cMujEndd（未能下载，见飞书原文）]\n一开始大家在聊的是 Prompt Engineering，提示词工程。那个时候所有人都相信一件事：模型不是不会，只是你没把话讲明白，只要提示词写得够好，啥都能搞定。\n后来大家发现事情不对劲。Agent 火起来之后，模型要做的事情越来越复杂，光「把话讲清楚」根本不够，它还得「知道」该用什么信息才行。于是 Context Engineering，上下文工程，接棒成为 AI 工程圈的新主角。\n但这事到这儿还没完。当大家把上下文管得越来越精细之后，又发现了一个更扎心的问题：信息都给对了，模型在长链路任务里还是会跑偏。\n提示词没毛病、上下文也没毛病，可它就是做不稳。正是在这个背景下，Mitchell Hashimoto 的那篇博客和 OpenAI 的那个百万行代码实验几乎同时出现，Harness Engineering 就这么被推到了台前。\n你有没有感觉出这条脉络？这三个阶段不是谁取代谁，而是每一步都在上一步撞到天花板之后，继续往前啃出来的：\nPrompt Engineering 解决的是：怎么让模型「听懂」你想干啥\nContext Engineering 解决的是：怎么让模型「知道」该用什么信息\nHarness Engineering 解决的是：怎么让模型在真实执行里「持续做对」一连串的事\n📷 [图片 token=SoW2b6jEMoP4tQxmrvdcHcnBnXO（未能下载，见飞书原文）]\n听起来三句话好像差不多对吧？但你一定要相信我，这三件事的难度和工作量，是层层放大的。\n把一句话讲明白难吗？不难。把一整套信息在合适的时机送给模型就开始有点门槛了。而要搭出一整套能让模型「持续稳定交付」的运行环境，那完全是另一个量级的工程活。\n这也顺便解释了一个很多人感到困惑的现象：同样是用 Claude 或者 GPT 这几个大模型，为啥有的团队做出来的 Agent 又稳又能打，到别人手里却一跑就跑偏？\n差的往往不是模型，而是围绕模型搭起来的那套东西，到底有没有做到位。\n📷 [图片 token=QrVWbMAFLo6n71xFNimcsWc8nWg（未能下载，见飞书原文）]\n所以接下来，我就按照「从提示词到 Harness」这条路径，带你一个阶段一个阶段往下走。\n先看看 Prompt Engineering 当年解决了啥问题、又撞上了啥天花板；\n然后看看 Context Engineering 是怎么接棒的、又在哪儿遇到了新的瓶颈；\n最后再来讲 Harness Engineering 到底是怎么在这两个天花板之上，又往前顶了一大截。\n走完这条路，你对 Harness 到底是个啥，心里就会有一个非常清晰的底。\n第一阶段：Prompt Engineering，先让模型「听懂」你 要讲清楚 Prompt Engineering，得先聊一个更基础的问题：大模型到底在干啥？\n你平时用 ChatGPT 也好，用 Claude 也好，看到的都是一个对话框，输入一句话，下面给你输出一段话。但如果把这层外壳扒掉，里面其实就一个东西：一个特别大的参数文件，存在硬盘上，加载到显卡的内存里。\n📷 [图片 token=OPZXb30wOoymElxtAwOchSjYnOh（未能下载，见飞书原文）]\n这个文件本身啥也不是。给它配一个 HTTP 接口，它就成了所谓的「大模型 API 服务」。给这个 API 套一个聊天界面，它就成了 ChatGPT；套一个代码编辑器，它就成了 Cursor、Trae 这种 AI IDE。\n那这个参数文件本身是怎么工作的呢？说白了就一件事：根据你输入的内容，预测下一个字最有可能是啥。\n注意我说的是「预测」，不是「思考」，这个区别很重要。它本质上是在猜你想要什么，然后把它觉得最可能的字一个一个往外吐。你给它的输入越模糊，它猜的范围就越大；你给它的输入越具体，它猜的范围就越收敛。\n📷 [图片 token=Sscpb374WoJ1ODxMCP8cXD60ntA（未能下载，见飞书原文）]\n知道了这个原理，你就能理解为啥同一个问题，换个说法效果能差十倍。\n举个例子。你丢一段代码给大模型，说「加个排序」。它会怎么回？大概率是给你回一段排序的代码片段，告诉你「就这么写」。但你拿到这段代码一看，傻眼了：它没说放在哪儿，也没说要不要改其他函数，更没把完整的代码给你。\n那如果你换一种说法呢？「这是我的完整代码，请帮我加上对 users 列表按年龄从大到小的排序，注意保留原有的所有逻辑，最后输出完整代码」。这次它给的结果一下就靠谱多了。\n为啥？因为大模型本质上是一个对上下文极度敏感的概率生成器。\n什么叫敏感？就是你给它什么样的输入，它就会沿着那个方向生成。\n你给它一个角色身份，比如「你是一个资深 Java 工程师」，它就会偏向用 Java 工程师的思路回答；你给它几个示例，比如「按照下面这个例子的格式来」，它就会沿着那个范式补全；你强调什么样的约束，比如「不要修改我已有的代码」，它就会把这个约束当成重点。\n你会发现，写提示词的本质，根本不是在「命令」模型，而是在塑造它的概率空间。你给它的每一句话，都在悄悄地把它的输出往某个方向推。\n正因为这种敏感性，大家慢慢摸索出了一套「提示词配方」。一个稍微正式一点的提示词，一般会包含这么几个部分：\n📷 [图片 token=LltxbuPY2oEX3jxMN59cTm3Ln4d（未能下载，见飞书原文）]\n角色设定：你是谁？比如「你是一个有十年经验的后端工程师」\n背景信息：当前在干啥？比如「我正在用 Go 写一个订单系统」\n参考资料：相关的文档、历史对话、代码片段\n明确任务：具体要你干啥\n约束条件：哪些不能做，比如「不要修改其他文件」\n输出格式：希望以什么形式返回，JSON、Markdown 还是表格\n把这些东西按一定的结构组装起来，就是一份比较完整的提示词。这种有意识地去设计和调整提示词、让模型稳定输出你想要的内容的技术活，就叫 Prompt Engineering，提示词工程。\n它解决的核心问题，其实就一个：模型不是不会，而是你没把问题说明白。\n提示词工程在大模型刚火起来的那一两年，作用是非常大的。因为那个时候大家做的事情都很简单：聊天、写文案、翻译、问答。这种任务的特点是「短链路」，一句问一句答就完事了，提示词写好了基本就搞定。\n但很快，大家想做的事情变复杂了，提示词工程就开始撑不住了。\n举个例子。你让大模型帮你「分析一下我们公司去年的财报」，它再聪明，没看过你的财报，它能分析啥？你让它「按照公司内部的代码规范帮我写一个新功能」，它没看过你们的规范，它怎么知道该怎么写？你让它「在我们已有的几十个 API 之间做一些调用组合」，它根本不知道你有哪些 API。\n📷 [图片 token=L5Blbusw2orfdFxoOFHcwYVEnlb（未能下载，见飞书原文）]\n这个时候你就会发现一个很尴尬的事实：提示词写得再漂亮，也替代不了「事实」本身。\n提示词擅长的，是把任务表达清楚、把约束讲明白，激发模型已有的能力。但它不擅长「凭空补出模型不知道的知识」，也不擅长「管理一大堆动态变化的信息」，更不擅长「在长链路任务里维持稳定的状态」。\n说白了，提示词工程解决的是「表达」的问题，不是「信息」的问题。\n于是第二阶段就来了：Context Engineering。\n第二阶段：Context Engineering，让模型「知道」该用什么信息 为什么 Context Engineering 会突然火起来？因为大家做的产品形态变了。\n之前大家做的是聊天机器人，问一句答一句，链路短、状态少。\n但后来 Agent 火了，模型不再只是「回答问题」，而是要进到真实环境里去「干活」。它要多轮对话，要调用工具，要写代码，要查数据库，要在多个步骤之间传递中间结果，还要根据外部反馈不断修正自己的计划。\n📷 [图片 token=FgHLbFQEnoGTnAx7ptrcx4xJnYb（未能下载，见飞书原文）]\n这个时候问题就完全变了。系统面对的已经不是「这一次回答对不对」，而是「整条链路能不能跑通」。\n举个例子。如果你不是简单地问一句「帮我总结这篇文章」，而是让大模型做一个更真实的任务，比如：「帮我分析这份需求文档，找出潜在风险，结合我们之前几次评审的意见，给出改进建议，最后生成一份发给产品经理的反馈稿」。\n你会发现，光靠一句提示词，根本完成不了这个任务。模型至少需要拿到：\n当前的需求文档\n历史的评审记录\n公司的相关规范\n当前任务的具体目标\n之前已经分析出来的中间结论\n收件人是谁、希望用什么语气\n这些东西全部加起来，才叫一个完整的「任务环境」。\n正是基于这个观察，才慢慢形成了一个新的概念：Context（上下文）。\n什么叫上下文？我用一句最直白的话解释：\n每一次发给大模型的所有信息加在一起，就叫上下文。\n而我们之前聊的提示词，只是上下文里很小的一部分。一个完整的上下文还包括：用户输入、历史对话、检索到的资料、工具返回的结果、当前的任务状态、中间产物、系统规则、安全约束等等等等。\n📷 [图片 token=VI0IbKu75og2hexzqz7cbDvynVh（未能下载，见飞书原文）]\n理解了这一层，你就能理解 Context Engineering 是在干啥了。它的核心思想就一句话：\n模型未必知道，所以系统必须在合适的时机，把正确的信息送进去。\n你可能会想：那我把所有相关的资料一股脑全塞进去不就完事了吗？\n这里就有一个让人头疼的现实：大模型一次能处理的上下文是有上限的。这个上限叫「上下文窗口」。\n📷 [图片 token=VsGqb1SMvos3eUxfJYKcPkPinXe（未能下载，见飞书原文）]\n你可以把它想象成大模型的「短期记忆容量」。再聪明的模型，一次也只能记住这么多东西。\n更要命的是，就算窗口够大，大模型的注意力也不是均匀分布的。研究发现，当上下文塞得太满的时候，模型会出现一种叫「上下文腐化」的现象：它开始记不住前面的内容，开始前后矛盾，开始忽略你最初定下的规则。\n我自己感觉这个特别像一个被信息淹没的人：你给他太多东西要看，他反而抓不住重点。\n那怎么办？这就是 Context Engineering 要解决的核心问题：怎么在上下文窗口有限的前提下，把最相关的信息，以最合理的方式，送给模型。\n实践下来，它基本上可以拆成三个步骤：\n📷 [图片 token=AaSybwkknolijzxRPqwcE7UDntb（未能下载，见飞书原文）]\n第一步，召回。说白了就是「找信息」。从一大堆资料里找出跟当前任务最相关的那部分。这里面就涉及到大家熟悉的 RAG 技术：把你的所有文档切成小块，转成向量存起来，每次有问题进来，先去向量数据库里搜出最相关的几块。\n第二步，压缩。找到的信息可能还是太多，那就压缩。怎么压？常见的做法是先让模型对每一段做个摘要，然后只把摘要送进最终的上下文。或者，对历史对话，把太老的对话压缩成一句话总结，只保留最近几轮的原文。\n第三步，组装。压完之后还要按一定的顺序、一定的格式组装起来。为啥顺序很重要？因为大模型对「靠后的信息」更敏感。所以重要的指令、当前的任务，往往要放在靠后的位置。\n📷 [图片 token=ML0sbdy0Foj5LCxPwUdc8AkYn4e（未能下载，见飞书原文）]\n不同的 AI 工具，这三步的实现各不相同。\n这就是为啥同样是用 Claude 模型，你用 Claude Code、Cursor、Trae 这些不同的 AI IDE，效果会差挺多。模型是同一个，但每家的上下文工程策略不一样，所以最后的体验也不一样。\n讲到这儿，我想专门聊一下 Anthropic 最近搞的 Agent Skills，因为它特别能体现「上下文工程不是塞得越多越好」这个理念。\n很多人做 Agent 的时候，会犯这么一个错误：把所有可能用到的工具说明、所有 SOP、所有参考资料，一上来就一股脑塞进上下文。理论上，模型知道得越多越好，对吧？\n但实际上效果往往更糟糕。\n为啥？还是那句话：上下文窗口是稀缺资源，信息一多注意力就会被稀释。模型看到的东西一多，反而抓不住当前任务真正需要的那部分。\n📷 [图片 token=Fa6UbJ2FBoqL45x0uF7cbQFonmf（未能下载，见飞书原文）]\nAgent Skills 提出的思路叫「渐进式披露」：一开始只给模型看每个能力的「目录」，告诉它「我有这些工具、这些 SOP、这些参考」。\n等模型真的判断「我现在需要用某个工具」的时候，再把那个工具的详细说明、参数定义、使用示例动态加载进来。\n这个思路其实非常重要，它告诉我们：上下文优化的本质不是「给得更多」，而是「按需给、分层给、在正确的时机给」。\n讲到这儿，Context Engineering 的核心思想基本就讲完了。它在 Prompt Engineering 的基础上又往前走了一大步：从「把任务讲清楚」升级到了「把信息送对」。\n但你以为这就是终点了吗？\n不是。\n第三阶段：Harness Engineering，让模型「做对」一连串的事 后来大家又发现了一个更麻烦的问题。\n你把提示词写得再漂亮，把上下文管理得再完美，模型在「单步」上的表现确实越来越好了。但只要任务的链路一长，模型就还是会出问题。\n什么问题？比如：\n计划做得很好，但执行的时候突然跑偏了\n调用工具调对了，但理解错了工具返回的结果\n在一个很长的任务链里，已经悄悄偏离初衷了，但系统完全没察觉\n跑着跑着突然忘了自己最初要干啥\n📷 [图片 token=Tfmvbsww4opDqLxwa83c1XQvnRg（未能下载，见飞书原文）]\n你仔细品品这个问题。提示词工程优化的是「意图的表达」，上下文工程优化的是「信息的供给」，但这两个其实都还停留在「输入侧」。\n而当模型真正开始「连续行动」的时候，会出现一个全新的问题：谁来监督它？谁来约束它？谁来在它跑偏的时候把它拉回来？\n这就是第三阶段要解决的事情。\nHarness 这个英文词，直译过来叫「马具」，或者说「缰绳」。\n📷 [图片 token=QRtcbPLFHorM5AxdI87cPWSbnPd（未能下载，见飞书原文）]\n为啥用这么一个词来命名一个 AI 工程概念？想象一下你骑马的场景：马本身有强大的力量，能跑能跳能驮东西，但如果没有缰绳和马具，这股力量就是失控的。马可能往悬崖上跑，可能甩你下来，可能跑去吃草不回来了。马具的作用，就是让这股力量为你所用。\n放在 AI 系统里，这个比喻就特别贴切了。当模型从「回答问题」走向「执行任务」，系统就不能只负责喂信息，还要能「驾驭」整个执行过程。这就是 Harness Engineering 的出发点。\n如果说前两代工程关注的是「怎么让模型更会想」，那 Harness 关注的就是「怎么让模型不跑偏、跑得稳、出了错还能爬起来」。\n讲到这儿，我用一个特别通俗的例子，把这三个阶段的区别一次性给你讲清楚。\n假设你是一个销售经理，要派一个刚入职的新人去做一次很重要的客户拜访。\n你会做哪些事情呢？\n第一件事，把任务讲清楚。你会告诉他：「见到客户先寒暄，然后介绍方案，再问需求，最后确认下一步」。这就是 Prompt Engineering，重点是把话说明白，不让新人懵。\n第二件事，把资料准备齐全。光把流程讲清楚不够，新人还得知道：客户是谁？之前聊过啥？产品报价多少？竞品啥情况？这次会议的目标是啥？这些资料你都得给他。这就是 Context Engineering，重点是把信息供给到位。\n第三件事呢？如果这是一个特别重要的客户，你光把流程讲清楚、资料给齐了，你心里还是不踏实对吧？你还会做更多的事情：\n让他带一份 checklist 去，每个关键节点都要打勾\n让他在关键节点实时给你汇报\n让他录音，回来你要复盘\n如果发现他拜访过程中跑偏了，马上电话纠正\n拜访完成后，要按照明确的标准去验收成果\n这一整套东西，就是 Harness。它的重点已经不是「把话讲清楚」「把资料给齐」了，而是「有没有一整套机制能持续观测、持续纠正、最终验收」。\n是不是一下就清楚了？\n关于 Harness 的边界，圈子里流传着一个特别简洁的等式：\nAgent = Model + Harness\n📷 [图片 token=Rl7hbOkYooSN1gxnicHc423Znrf（未能下载，见飞书原文）]\n翻译成人话：在一个 AI Agent 系统里面，除了模型本身之外，几乎所有决定它能不能稳定交付的东西，都属于 Harness。\n所以你也可以反过来推：\nHarness = Agent − Model\n这个公式我特别喜欢，因为它一下就把 Harness 的边界划清楚了。\n讲到这儿你可能会问：那是不是 Harness Engineering 出来了，前面那两个就过时了？\n不是的。这三者根本不是替代关系，而是包含关系。\n📷 [图片 token=StcXbI51XoO9MFxoH0kccs0vnDw（未能下载，见飞书原文）]\nPrompt 是对「指令」的工程化\nContext 是对「输入环境」的工程化\nHarness 是对「整个运行系统」的工程化\n它们的边界一层比一层大。Prompt 是 Context 的一部分，Context 是 Harness 的一部分。当你做 Harness 的时候，里面一定包含 Context 工程，Context 工程里又一定包含 Prompt 工程。\n所以 Harness Engineering 不是来取代谁的，它是站在更大的系统视角上，把前面这两个都包进去了。\n好，到这里 Harness Engineering 是啥、和前面两个啥关系，就基本讲完了。但只懂概念肯定不够，下一章我要把它拆开，看看一个成熟的 Harness 到底包含哪些东西。\nHarness 拆开看，里面到底装了些啥？ 如果你去看 OpenAI、Anthropic、LangChain 这些做 Agent 的顶级团队，会发现一件挺有意思的事：他们的产品形态不一样、技术栈不一样、服务对象也不一样，但你把他们的 Harness 掀开看内部结构，里面的组件居然惊人地相似。\n这不是巧合。而是因为「让一个 Agent 在真实世界里稳定工作」这个命题，天然就会推着所有人往同一个方向收敛。一个成熟的 Harness 大致可以拆成六块核心组件，我把它们叫做六层，每一层都在解决「驾驭」这件事的一个独立维度。\n📷 [图片 token=BEWPbS3wqoqwpYxwQ0oc8lQ9nGj（未能下载，见飞书原文）]\n这六层看起来有点多，但其实可以按「它在干啥」分成三组：\n输入侧（让模型看到正确的东西）：上下文精细化管理 + 记忆与状态管理\n动作侧（让模型做出正确的事）：工具系统 + 任务执行编排\n校验侧（让模型知道做没做对 + 出错能爬起来）：评估观测 + 约束恢复\n你发现没有？这三组其实对应了一个工程师在真实环境里干活的三个必要条件：看得准 → 做得对 → 错了能兜底。任何一个成熟的 Harness，你去拆一拆，都能找到这三组六层的影子。\n在真正展开六层之前，我想先把每一层对应的那个「要解决的核心问题」用一张表提前摆到你面前。这张表你可以当成整章的路线图，后面每读完一层，回头瞄一眼，就知道自己读到哪儿、哪些还没读：\nMBeZDm 层 这一层在解决的一句话问题 上下文精细化 模型这一轮该看到什么？ 工具系统 模型用什么动手？ 执行编排 模型下一步该干啥？ 记忆与状态 模型跨轮该记住什么？ 评估与观测 模型做得好不好有没有尺子？ 约束与恢复 模型出错了能不能爬起来？ 你看这六个问题，它们根本不是什么技术理论的包装，而是一个真实工程师在真实环境里做事情的时候，本能会问自己的那六件事。这也是为啥 OpenAI、Anthropic、LangChain 这些完全不同的团队，最后都不约而同地收敛到差不多的结构上。这六层不是谁凭空发明的，是从现实里长出来的。\n为了让这六层不至于变成抽象的概念堆，我决定挑一个贯穿整章的具体例子。后面每讲一层，我都会回到这个例子里给你一个具体画面。例子是这样的:\n🔧 假设你要做一个 Agent，任务是：每天定时帮你扫一遍 GitHub 上你关注的仓库的新 PR，从里面挑出值得你特别关注的那几个，对每一个生成一段简短的摘要和点评，最后把结果发到你的 Slack。\n好，带着这个 PR Review Agent，我们一层一层往下看。\n第一层，上下文的精细化管理 先说输入侧的第一件事：模型每次被调用的时候，到底该看到什么？\n这里有个特别关键的修饰词，「每次被调用的时候」。我专门强调它是因为这一层很容易和第四层（记忆与状态）搞混。简单区分一下：\n📷 [图片 token=DMdobTWzTo3qYuxl6kEcPo6dn5f（未能下载，见飞书原文）]\n第一层（上下文的精细化）管的是「空间」：这一轮发给模型的那一坨上下文，长啥样、装了些啥、怎么排布\n第四层（记忆与状态）管的是「时间」：上一轮发生过的事情，怎么流动到下一轮\n你可以把它想成一台相机：第一层是「取景框」，第四层是「胶卷」。两件事都重要，但看的方向完全不同。\n回到我们的 PR Review Agent。它每处理一个 PR 的时候，到底该看到什么？最粗暴的做法是把整个 PR 的 diff 全扔给它，这恰恰是最容易犯的错。它真正需要的其实是一组精挑细选的信息：\nPR 的标题和描述\ndiff 里真正被改动的那几个文件\n这些文件在仓库里对应的模块说明\n作者最近几次提交的风格偏好\n仓库的 code review 惯例\n你塞给它越多无关信息，它的注意力就越散。这个现象 Anthropic 在他们的博客里专门命名了，叫「context rot」（上下文腐化）。他们给的解法是「just-in-time retrieval」，也就是让 Agent 边干活边按需抓信息，而不是一上来就把所有可能有用的东西一股脑塞进去。\n📷 [图片 token=O88ibU6Fpolt9rxQJIXcMIein5b（未能下载，见飞书原文）]\n所以这一层的核心工作可以浓缩成三件事：\n第一，把角色和目标钉死。模型得知道自己是一个「PR 审查助手」，当前任务是「挑出值得关注的 PR 并生成摘要」，成功标准是「我挑出来的真的都是该被关注的」。大部分 Agent 跑偏，根源就是这一步没说清楚。\n第二，动态筛选而不是一次塞满。只把当前这个 PR 相关的那几块信息拉进来，其余的留在文件系统里，等需要了再取。\n第三，结构化组织。固定规则（code review 惯例）放一处，动态证据（当前 PR 的内容）放一处，中间结论（我对这个 PR 的初步打分）放一处，三者要分开。否则模型会「自我污染」，也就是用前面错的中间结论去影响后面的判断。\n第二层，工具系统的可控调用 聊完「看到什么」，再聊「能做什么」。\n没有工具的大模型本质上还是一个文本预测器。它能告诉你一个 PR 应该怎么审，但它没法真的去 GitHub 上读那个 PR；它能写出一段 Slack 消息的文本，但它没法真的把这段消息发到你的频道。\n📷 [图片 token=FyUIbUmuko8nsfx5Q5VckFW3nQb（未能下载，见飞书原文）]\n接上工具之后，PR Review Agent 才真正活过来。但工具不是接得越多越好。OpenAI 在做 Codex 早期踩过这个坑：他们一开始给 Agent 接了一堆工具，想着「选择多总是好的」，结果 Agent 频繁用错工具、用错时机。后来砍掉一大半，效果反而上去了。\n所以这一层你要回答三个问题：\n**给它哪些工具？**对 PR Review Agent 来说，至少需要四件：\ngh 命令行工具：拉 PR 列表、看 diff\n读文件工具：看仓库里的相关代码\n代码搜索工具：查某个函数在哪儿被用到\nSlack 发送工具：把结果发出去\n别的工具，比如「重跑 CI」「直接给 PR 打标签」，除非真的必要，先别加。\n**什么时候用哪个工具？**该查的时候要查，不该查的时候别瞎查。比如 Agent 判断「这个 PR 改的函数是不是核心逻辑」的时候应该去代码搜索，而不是凭感觉猜。反过来，明明 diff 已经在上下文里了，再去重新拉一次 PR 纯属浪费。\n**工具结果怎么喂回模型？**这条最容易被忽略。比如 Agent 调用代码搜索，拿到 30 条匹配。你是不是要把 30 条原文原样塞回去？不是。你要先做一层提炼，比如只留核心模块的那几条，再喂回去。否则这 30 条原文一进来，上下文又被污染了。\n你最近听得很多的 MCP（Model Context Protocol），本质上就是在做工具层的标准化，让任何工具都能用同一种方式接到任何 Agent 上，大家不用再各自重复造轮子。\n📷 [图片 token=B6X1b5FtHoClIgx6mK4c8gBgnKg（未能下载，见飞书原文）]\n第三层，任务执行的全局编排 工具接上了，模型就能动手了。但能动手不等于能做成事。\n这里我直接把一句你需要记住的话摆出来：Agent 的本质，说白了就是一个 for 循环。思考一步 → 行动一步 → 观察结果 → 再思考下一步。这个循环结构有个很经典的名字叫 ReAct（Reasoning + Acting）。\n📷 [图片 token=A4cLbidaQoSATkxRkuccmnW5n7b（未能下载，见飞书原文）]\n听起来很朴素对吧？但魔鬼就藏在这个循环里。\nAgent 经常翻车的场景是：每一步它都会做，但把所有步骤串起来之后就不会了。它会拉 PR 列表，会读 diff，会写摘要，但它不知道应该先拉全列表再逐个分析，还是应该边拉边评，最后交付给你经常就是一堆半成品。\n这就是第三层的职责：给模型一条明确的工作轨道。\n回到 PR Review Agent。它的工作轨道应该是这样：\n1. 拉取仓库当前所有开放的 PR 列表 2. 对每一个 PR： a. 读 diff 和描述 b. 判断涉及的是核心模块还是边缘模块 c. 核心模块的 PR 做深度分析 d. 给出一个重要性打分（1-5） 3. 按重要性排序，选前 3 个 4. 为每一个生成摘要和点评 5. 汇总发到 Slack 6. 检查发送是否成功，失败则重试 有了这条轨道，Agent 就知道「我现在在哪一步，下一步该干啥」。它不会再瞎跑。\n除了 ReAct 之外，还有几个业界常见的编排模式你可以记一下名字：Plan-and-Execute（先规划完整计划再执行，适合长链路任务）、Reflexion（每次失败都让 Agent 反思一下再重试）、Tree of Thoughts（同时探索多条思路再选最好的）。不同场景会用不同的编排策略。\n📷 [图片 token=Kgjbb33MgoGhGPxLdgtc1nVxnqf（未能下载，见飞书原文）]\n第四层，记忆与状态的分层管理 现在聊时间侧：Agent 怎么在「跨轮」之间保持连贯？\n没有状态管理的 Agent，每一轮调用之间都是失忆的。\n你的 PR Review Agent 今天跑了一遍，明天再跑的时候完全不记得「这个 PR 昨天已经审过了」，于是又审一遍再发一次消息，Slack 频道很快就会变成重复消息的坟场。\n这就是为啥 Harness 必须管状态。\n这里要回扣我们开头讲的 Mitchell Hashimoto 和 Anthropic 的一个核心洞察：Agent 的状态不应该放在上下文窗口里，而应该外化到文件系统。\nAnthropic 在《Effective harnesses for long-running agents》里给出的具体做法是让 Agent 维护一个 claude-progress.txt 日志、一个 init.sh 启动脚本、加上完整的 git history，作为「长期记忆介质」。下一轮换一个全新的、干净的上下文窗口接手时，从这些文件里一读，立刻就知道「现在到哪一步了」。\n📷 [图片 token=ARvxbGJUno7s6kxKleScowzdn7e（未能下载，见飞书原文）]\n放到 PR Review Agent 上，你可以做同样的事。它需要管的状态至少有三类，必须分层存：\n任务状态：今天已经处理到哪个 PR 了？还剩几个？每个的打分是多少？这类信息写在一个 today-progress.json 里，当天任务跑完就归档\n会话中间结果：当前这一轮里 Agent 对某个 PR 做出的初步判断。这类信息随会话结束就可以丢，不用持久化\n长期记忆和用户偏好：你喜欢关注什么类型的 PR？你特别看重哪些模块？这类信息写在常驻的 user-preferences.md 里，每次调用都注入\n你发现没有？这三类记忆的生命周期完全不同：任务状态活到任务结束，会话中间结果活到当轮结束，长期记忆跨所有任务存在。混在一起就乱了，分清楚才能用好。\n顺便说一句，Claude Code、Cursor、Trae 这些 AI IDE 里的规则文件（CLAUDE.md / .cursorrules），就是「长期记忆」这一类的典型实现。\n每次调用都自动注入，Agent 永远「记得」项目的核心约束。这一层我们在后面的落地章节还会具体动手写。\n第五层，独立的评估与观测体系 这一层是最容易被跳过、但跳过之后就进退两难的一层。\n先看一个真实场景。我见过太多团队，做出一个 Agent 之后高高兴兴上线，结果跑了两周才发现：这玩意儿的实际成功率只有 50%。\n不是它不出结果，而是它每次都出结果，但一半时候是错的。问题在于这两周里没人发现，因为根本没有机制能告诉团队「它这次到底做得对不对」。\n📷 [图片 token=XOOkbMs3ZoTwq0xvMYtcZeVonFg（未能下载，见飞书原文）]\n这就是没有第五层的下场。\n那第五层到底要做什么？我把它拆成两件事：一件是有个尺子，另一件是能看到每一次的量。\n尺子：Eval 集（这一层真正的核心）\nEval 集（evaluation set）是做 Agent 开发的业界标准做法，也是这一层的灵魂。\n简单说就是：你手写一批典型任务，每一个都标注好「正确答案长啥样」，然后每次你对 Harness 做了任何改动（比如改了 CLAUDE.md、加了一个新工具、调整了编排流程），都让 Agent 把这批任务再跑一遍，对比成功率。\n对 PR Review Agent 来说，一个最小可用的 Eval 集可能是这样：\n从过去三个月挑 20 个真实 PR\n每一个都标注「是不是重要」「摘要应该怎么写」\n每次改完 Agent 就跑一遍这 20 个，看它挑对了几个、写对了几个\n没有这个 Eval 集，你对 Agent 好不好的判断永远停留在「我感觉这次变好了」的玄学阶段。\nLangChain 把他们的 Terminal Bench 成绩从 52.8 推到 66.5 分、直接从榜单 30 开外冲到前 5，靠的就是基于 Eval 集做 trace 回放和迭代。\nAnthropic 和 OpenAI 内部同样有大量的 Eval 基础设施。这不是锦上添花的可选项，是工程级 Agent 的必需品。\n量：Trace + 日志 + 指标\n有了尺子还不够，你还得看到 Agent 每一次的真实足迹，也就是它每一步做了什么决策、调了哪个工具、拿到什么返回、花了多少 token。\n这就是 LangSmith、Langfuse 这类 trace 系统存在的意义。能看到 trace，你才能定位失败那一步发生了什么，才能往 Harness 里补上对应的修复。\n这一层做到位之后，你对 Agent 的调试就从「猜」变成了「看」。\n第六层，约束校验与失败恢复机制 最后一层。在真实环境里，失败不是例外，是常态。\n你的 PR Review Agent 真跑起来之后，一定会遇到各种奇形怪状的失败：GitHub API 限流、某个 PR 的 diff 太大把上下文冲爆、Slack webhook 过期、Agent 误判了一个无关 PR 然后疯狂读代码把 token 耗光一半预算……如果没有恢复机制，每次失败都是从头再来。\n📷 [图片 token=Imxub86XKodEVvxyjnZcXNiUnK2（未能下载，见飞书原文）]\n这一层要做三件事：\n约束：定义「什么事 Agent 不能做」\n对 PR Review Agent 来说，约束可以包括：「一次最多分析 20 个 PR」「不能对已 closed 的 PR 再评论」「不能直接修改 PR 本身」「token 用量超过 10 万就立刻停下」。这些约束最好硬编码到代码或 linter 规则里，而不是写在提示词里靠 Agent 自己遵守。OpenAI 在 Codex 项目里把资深工程师的经验固化成他们叫做「Golden Principles」的机制（我们在下一章会专门展开讲），就是这种思路的极致版。\n校验：在每一步输出前后都做自动检查\n比如 Agent 给出摘要后先跑一道格式校验（是不是 Markdown？几个段落都在？），发送到 Slack 前先检查频道名是不是在白名单里。校验不是审美品味，是硬规则。\n恢复：失败之后有预案\nGitHub 限流 → 等一段时间后重试；Slack 发送失败 → 先落到本地队列，下次重试；token 快耗光 → 立即停下并保存进度，下一轮继续。每一种典型失败都应该有一条明确的恢复路径，而不是一股脑全挂掉。\n这三件事加起来，才能让 Agent 从「能跑」升级到「能在生产环境跑」。\n讲到这儿，六层就全部展开完了。现在你可以再回去翻一眼章节开头那张「六问对应表」，把每一层和它对应的那个问题在心里重新对一遍。\n📷 [图片 token=X2VbbUbIQoZR5Qx0XyocyVq7n6c（未能下载，见飞书原文）]\n这一轮你对这六个问题的理解，应该和一开始看的时候完全不一样了，那些问题背后的门道你都见过了。\n还记得开头那一章我们讲过 Mitchell Hashimoto 的「复利效应」吗？他说 Harness 的核心是「每次 Agent 犯错，都把修复沉到环境里」。那个修复到底沉到哪儿？答案就是这六层的其中某一层：\n你发现 Agent 总是漏掉某个上下文信息 → 去改第一层\n你发现它总是用错工具 → 去改第二层\n你发现它步骤乱 → 去改第三层\n你发现它跨天记不住进度 → 去改第四层\n你发现你没法判断它做得好不好 → 去搭第五层\n你发现它一失败就崩溃 → 去强化第六层\n所以这六层不是一张「必须一次搭完」的任务清单。它是一张路标，告诉你下一次 Agent 犯错时，你的修复应该落到哪里。\n随着时间推移，这六层的每一层都会被你一点一点填充、加固、打磨，你的 Harness 就是这样一寸一寸长大的。\n讲到这儿，Harness 装了些啥你心里应该有谱了。\n但你肯定还有疑问：这些东西听起来都对，但大厂真的是这么做的吗？具体是怎么落地的？\n下一章我就来讲这个：抓 5 个真实的工程难题，看看大厂是怎么解的。\nHarness 有什么难点？ 讲完六层能力的骨架，你可能会觉得这套东西好像挺有体系的。但真要动手做，各大公司都会告诉你一个共同的感受：概念清晰是一回事，落地是另一回事。\n下面我挑 5 个大厂在真实项目里踩过的、特别典型的坑给你看看。\n你会发现 Harness 真正的难度根本不在蓝图，而在这些具体的细节里。每个难题我都会用同一种结构来讲：先看现象是啥，再看大厂是怎么反常识地解的，最后提炼出一条你可以直接记住的原则。\n难题一，Agent 跑久了为啥会越走越偏？ 这是几乎所有做长链路 Agent 的团队都会遇到的问题。\n现象是这样的：一开始 Agent 表现挺好，目标清晰，步骤明确。但跑着跑着，你会发现它开始「忘」。它忘了最初定的目标，忘了之前已经做过的决定，开始重复劳动，开始偏离主线。\n📷 [图片 token=RFjsb6XQPo3RsTxKfWec5C3jnLc（未能下载，见飞书原文）]\n更诡异的是，Cognition（就是做 Devin 那家公司）在用 Claude Sonnet 4.5 重做 Devin 的时候，观察到一个特别有意思的现象，他们把它叫做「上下文焦虑」（Context Anxiety）。\n什么意思呢？就是模型自己好像也能感觉到「我快撑不住了」。当它觉得上下文窗口快用完的时候，模型不仅开始丢细节，还会出现一种奇怪的行为：它开始着急地想收尾。它会突然简化方案、跳过验证步骤、急匆匆地宣布「任务完成」。\n更神奇的是，研究发现模型对自己「还剩多少上下文」的估计非常不准，经常以为自己快没空间了，其实还剩一大半。\n📷 [图片 token=RNkibOs80oXbSIxb2Abc9tH9nbH（未能下载，见飞书原文）]\n你品品这个现象，是不是特别像一个被任务压垮的人？\n很多团队遇到这个问题，第一反应是做「上下文压缩」（Context Compaction）：把前面的历史压成摘要，腾出空间继续跑。\n这个思路对不对？对，但 Anthropic 在另一篇《Harness design for long-running application development》博客里挑明了一个更扎心的观察：光压缩根本不够。\n他们确认了 Sonnet 4.5 确实存在前面说的那种「上下文焦虑」倾向，一旦这种状态上来了，只压缩历史、不把整个上下文窗口彻底换掉，那种「已经累了」的负担感模型还是带着，Agent 还是会在长链路任务里慢慢失焦。\n真正解开这个结的关键动作，Anthropic 把这个做法叫做 Context Reset：直接把旧的上下文窗口整个丢掉，换一个干净的接手。\n那 Context Reset 具体怎么落地？\nAnthropic 在他们的另一篇《Effective harnesses for long-running agents》博客里给出了一套具体做法：让 Agent 跨多轮接力跑，状态全部外化到文件系统。\n📷 [图片 token=B72Yb1KD2oRChFxG0Lpc0hfTnPg（未能下载，见飞书原文）]\n具体怎么做？整个系统其实只有一个 Agent，系统提示词、工具集、整套 harness 全都一样。\n真正在变的只是每一轮的初始 user prompt：\n第一轮用一个专门的「初始化」prompt，让 Agent 把环境信息、项目状态、约束条件整理好，写到一份 claude-progress.txt 日志、一份 init.sh 启动脚本、一个初始 git commit 里；\n后续每一轮用一个专门的「增量推进」prompt，让它做一点进展，然后把新状态再写回这几份文件。\n关键就在这里：每一轮开始的时候，Agent 都面对一个完全干净的上下文窗口，它根本不记得上一轮的对话历史，它靠的完全是读取文件系统里这几份「交接文档」来恢复「我现在在哪一步」。\nAnthropic 在博客里为了好讲，把这两种 prompt 启动的 Agent 分别叫 initializer agent 和 coding agent，但他们也在脚注里专门澄清了：这其实是同一个 Agent，只是首轮和后续轮次用了不同的 user prompt 启动而已。真正被「换掉」的不是 Agent，是上下文窗口。\n你品品这个架构。它的关键不在「压缩上下文」，而在把状态外化到文件系统。文件系统变成了真正的长期记忆，上下文窗口本身只负责处理当前这一轮，处理完就可以整个丢掉，而整个任务的进度完全不丢。\n这特别像我们在工程里遇到内存泄漏的时候是怎么做的。你不会拼命去优化内存，你会直接重启进程，把状态从磁盘恢复出来。Agent 的长期运行，用的是同一套思路。\n原则一：重启胜过修补，状态沉到文件里，Agent 随时可以在一个干净的上下文窗口里接力继续。\n难题二，让 Agent 自己给自己打分，为啥总偏乐观？ 这是另一个特别隐蔽的问题。\n很多人做 Agent 的时候是这样的：让 Agent 干活，干完之后再让它自己评估「做得怎么样」。看起来挺合理对吧？让它有一个自我反思的环节。\n但实际效果呢？Agent 永远觉得自己干得不错。\n📷 [图片 token=RjHwbaH9zoELUKxXqsEc2i4snoc（未能下载，见飞书原文）]\n特别是在那些没有标准答案的任务上，比如「设计一个用户界面」「写一篇有说服力的文案」「评估这段代码的可读性」，自评的偏差会特别明显。它会自动忽略自己做得不好的部分，然后给自己打一个还不错的分。\n为啥会这样？想想咱们人就理解了。让一个人自己给自己打绩效，他会公平吗？很难。\nAnthropic 后来想明白了一件事：让干活的和验收的，必须是不同的人。\n所以他们在另一篇《Harness design for long-running application development》里搞出了一个三角分工：\n📷 [图片 token=E4S0bhUCQoE0uExRseGcmvG5ntc（未能下载，见飞书原文）]\nPlanner（规划者）：负责把模糊的需求扩展成完整的规格说明\nGenerator（生成者）：负责一步一步去实现\nEvaluator（验收者）：负责像 QA 一样真实地测试\n更关键的是，Evaluator 不是简单地看一眼代码就完事，它必须真的去操作页面、看具体的交互、检查实际的运行结果。这就保证了它的验收是有「真实环境」托底的，而不是抽象地 review。\n只要这三个角色足够独立，系统就能形成一个真正有效的循环：规划 → 生成 → 验收 → 修复 → 再验收。这才是闭环。不要让一个 Agent 既当运动员又当裁判，也别让它既是厨师又是食客。\n原则二：生产和验收必须分离，而且验收方必须能摸到真实世界。\n难题三，Agent 总是失败，工程师到底该干啥？ 这个难题比前两个都更深，因为它动的不是解法，是工程师自己的角色定位。\n先说现象。当 Agent 反复失败的时候，一般人遇到的本能反应只有两个：要么再调调提示词，要么换个更强的模型。\n但 OpenAI 在做 Codex 项目的时候，实践告诉他们：工程师本能反应的这两招，其实都是错的方向。\n他们干了一件在传统程序员看来非常离谱的事：在 Codex 那个百万行代码的项目里，人类工程师几乎不写一行代码，全部由 Agent 来写。\n📷 [图片 token=Oewqb6GR9oZjvXxiavLcmHzinUf（未能下载，见飞书原文）]\n那人类工程师到底在干啥？从他们这套实践里，你能看到工程师的工作重心其实都压在了三件事上面：\n把产品目标拆解成 Agent 能力边界内的小任务，确保每一件事都是 Agent 接得住的\n当 Agent 反复失败时，不是去催它「再努力一点」，而是去看它「环境里缺了什么能力」，然后把那个能力补进环境里\n建立反馈链路，让 Agent 真正能看到自己工作的结果，而不是两眼一抹黑地瞎跑\n你看出第二条的意思了吗？这其实是一次思维方式的根本转变。\n以前遇到 Agent 写代码有 bug，传统做法是加一句提示词「请仔细检查代码不要有 bug」，然后祈祷模型这次听话。\n而 Codex 团队的做法是：给 Agent 接上 lint、单测、运行环境，让它自己写完自己跑，看见 bug 自己改。同样一个问题，前者是在求模型发挥，后者是在改造环境，彻底决定了 Agent 下次会不会再犯。\n📷 [图片 token=CAlnbCnk7oVAK2xWw3QcAAJEngd（未能下载，见飞书原文）]\n所以你看，在 Codex 这种 Agent 主导的工程体系里，工程师的价值不再是「我一天能写多少行代码」，而是「我能为 Agent 设计多好的一套运行环境」。这才是未来程序员真正的技术含量所在。\n原则三：Agent 反复失败的时候，别问模型能不能更努力，要问环境还缺什么。\n难题四，规范文件越写越长，为啥 Agent 反而更糊涂？ 这个是 OpenAI 自己亲自踩过的坑，说出来挺打脸的。\nOpenAI 早期做 Codex 的时候，搞了一个特别大的 AGENTS.md 文件，把所有的规范、所有的约定、所有的最佳实践全部塞进去。他们当时的想法是：把规则写得越全越好，Agent 就越不会出错。\n📷 [图片 token=NzdEbJc1soQwI5xWCOTcFPk3ntf（未能下载，见飞书原文）]\n结果呢？Agent 更糊涂了。\n为啥？因为上下文窗口是稀缺资源。这个文件被当作系统提示词每次都注入进去，当它变得越来越长的时候，模型的注意力被严重稀释。它看到的东西太多，反而抓不住当前任务真正需要的那部分。\n这其实就是我们前面讲过的「上下文腐化」在规则文件这个场景的具体表现。\nOpenAI 后来怎么改的？他们把 AGENTS.md 从一本「百科全书」改成了一个「目录页」：\n主文件只保留 100 行左右的核心索引。你没看错，OpenAI 原文博客里特地写明了这个数字：整个 AGENTS.md 控制在大约 100 行，它不告诉 Agent 每条细节规则，只告诉 Agent「你想看什么，去哪儿看」\n详细的内容拆到具体的子文档里：架构文档一份、设计原则一份、产品规格一份、执行计划一份、质量评分一份，每份都有清晰的主题\nAgent 平时只看目录，只有真的需要某一部分的时候，才钻进对应的子文档\n📷 [图片 token=Px9vboHNJo8H0axkNCUcR0Fpnkh（未能下载，见飞书原文）]\n这套做法其实就是我们前面讲的渐进式披露（Progressive Disclosure）。\n我特别喜欢这个思路，因为它和我们做软件设计里的「按需加载」「懒加载」一脉相承：上下文优化的本质，不是「给得越多越好」，而是「该给的时候给，不该给的时候藏起来」。\n这一点其实和我们前面讲的 Agent Skills 是同一回事，前后呼应上了。如果你现在在写 CLAUDE.md 或者 Cursor Rules 这种文件，强烈建议你回头看看自己写的有没有「百科全书化」。如果有，赶紧拆。\n原则四：规则文件宁缺毋滥，给模型看的东西少即是多。\n难题五，Agent 写的代码越堆越烂，技术债怎么还？ 前面四个难题都还算比较抽象，这一个特别具体、特别接地气，是 OpenAI 团队在《Harness engineering》博客里亲口承认的一个「我们一开始做错了」的故事。\n先说现象。当 Agent 负责写绝大多数代码之后，会发生一件很自然但也很糟糕的事：Agent 会疯狂模仿仓库里已有的代码模式。\n好的模式会被复制，坏的模式也会被复制。一旦早期某段代码写歪了，Agent 会把那个歪的写法当成「惯例」，越堆越多，越堆越歪，最后整个代码库开始「腐烂」。这个现象 OpenAI 团队给它起了一个很扎心的名字，叫 AI slop，AI 代码泔水。\n📷 [图片 token=TrxxbY4Xloe9SHxHuKccP71xnXd（未能下载，见飞书原文）]\nOpenAI 团队一开始是怎么解决这个问题的？用最朴素的办法：靠人工清理。\n他们每周拿出整个周五（也就是一周 20% 的时间）让人类工程师去手工打扫 AI slop。你想想这个画面：一群 OpenAI 的高级工程师每周五不干别的，专门给 Agent 擦屁股。\n然后呢？这个方案失败了。原因很直接：Agent 产出代码的速度太快，人类工程师清理的速度根本跟不上，周五清一天，周一一早又堆满了新的。这是一个典型的「人力怎么都追不上机器」的尴尬局面。\n那 OpenAI 最后是怎么改的？他们用一个非常 Harness 的思路解决了这个问题。做法分两步：\n📷 [图片 token=COLdb12eYoAUOoxe8yicgRkNn2g（未能下载，见飞书原文）]\n第一步，把人类工程师关于\u0026quot;什么是好代码\u0026quot;的经验，写成一套「Golden Principles」（黄金原则）沉进仓库。比如「优先用共享工具包而不是手写 helper」「不要瞎猜数据格式，必须校验边界或用带类型的 SDK」，这些都是有经验的工程师脑子里的隐性知识，以前只存在 code review 的讨论里，现在被显式地写成了规则。\n第二步，让一批后台 Agent 按固定节奏自动跑。这些 Agent 定期去扫描仓库，对比 Golden Principles，找出偏离的地方，然后自动开修复 PR。大部分修复 PR 可以在 1 分钟内被人类审完，直接 auto merge。\n你品品这个做法。它其实把「还技术债」这件事从一周一次的人工集中清扫，变成了每天持续进行的自动偿还。OpenAI 原文里有一句话我特别喜欢：\n技术债就像一笔高利息贷款，几乎永远应该每天小额还一点点，而不是攒着等某一天集中还。\n这太准确了。你想想我们平时写代码是不是也这样？谁都知道技术债要清，但总是说「等这个季度忙完了统一重构」，结果永远也没有「不忙的季度」，技术债越滚越大。Agent 帮人类解决这个老毛病的办法，居然是把经验固化成规则，然后用 Agent 自己去对付 Agent。\n原则五：技术债不是攒一堆集中还，而是每天让后台 Agent 自动偿还一点。\n顺便提一个反直觉的发现：Agent 用「老技术」反而更稳 讲完五个难题，我想再顺手补一个 OpenAI 博客里特别反直觉的小观察，虽然它不算难题，但值得你记住。\n很多人做 AI 编程的时候有一个想当然的认知：AI 是最前沿的东西，那当然应该配上最前沿的技术栈，什么新框架都给 Agent 上一套。但 OpenAI 在实践中发现，事情正好反过来：\nAgent 对那些被人类称为「boring」的老技术反而掌握得最好。\n为啥？OpenAI 原文给了三个原因：组合性好、API 稳定、训练数据里出现得多。\n📷 [图片 token=TH1cbGs73oJpyTx1YOAc0UZ1n2d（未能下载，见飞书原文）]\n你想想这个逻辑就通了：AI 训练数据里，一个出现了十几二十年的老库，相关的代码示例、StackOverflow 问答、博客文章多如牛毛，AI 对它的各种用法了如指掌。而一个昨天才出的新框架，AI 只看过零星几篇文档，很容易张冠李戴。\n所以 OpenAI 在 Codex 项目里，甚至会主动选择那些看起来很土的、甚至业界已经有点嫌弃的技术栈，就因为 Agent 能把它们用得更稳。\n有一个特别极端的例子：有时候他们宁愿让 Agent 自己实现一个小工具函数，也不去引入一个流行的 npm 包，因为自己写的代码 Agent 能 100% 理解和控制，而第三方包里藏着 Agent 看不懂的黑盒行为。\n这个发现给你的实际启发是：如果你要做一个让 Agent 跑得稳的项目，在选技术栈的时候，不要一味追新。越老、文档越齐全、在开源社区里沉淀了越久的技术，Agent 反而越容易帮你做对。\n好，五个难题 + 一个反直觉发现，这一章就讲完了。\n你可能已经感觉到一件事：这些难题的解法全都有一个共同点，它们都不是在调模型，而是在设计模型外面的那一整套环境。这就是 Harness Engineering 的灵魂所在。\n我也帮你把这五条原则打包成一个小口诀，带着它进入下一章：\n重启胜过修补，生产验收分家，与其催模型不如改环境，规则宁缺毋滥，技术债天天还。\n这五条原则贯穿了大厂所有的实践，也回答了「Harness 真正难在哪」。\n写在最后 写到这儿，我想给你把整篇文章的思路再梳理一遍。\n📷 [图片 token=GcbFb8Q69omPj0xbAcgc9q62nih（未能下载，见飞书原文）]\nAI 工程的重心，过去两年换过三次：\nPrompt Engineering 解决的是「怎么把任务讲清楚」。它的核心是塑造模型的概率空间，让模型「听懂」你想干啥。\nContext Engineering 解决的是「怎么把信息送对」。它的核心是动态管理大模型的上下文，让模型「知道」该用什么信息。\nHarness Engineering 解决的是「怎么让模型在真实执行中持续做对」。它的核心是设计一整套包裹模型的运行环境，让模型「做对」一连串的事。\n这三个东西不是替代关系，而是层层包含的关系。\nPrompt 是 Context 的一部分，Context 是 Harness 的一部分。\n当任务还是简单的单轮对话的时候，Prompt 就够用；当任务开始需要外部知识的时候，Context 就关键了；但当模型真正进入「长链路、可执行、低容错」的真实场景，Harness 几乎是不可避免的。\n到了今天这个阶段，整个 AI 圈也越来越清楚一件事：\nAI 落地的核心挑战，正在从「让模型看起来更聪明」，转向「让模型在真实世界里稳定地工作」。\n模型决定了一个 Agent 的天花板，但 Harness 决定了它能不能落地、能不能稳定交付、能不能真正跑在生产环境里。这就是为啥同样的模型，在不同的产品里效果差距会这么大。\n📷 [图片 token=Tvz4bUW26oBLqZxHiFictEoHnrh（未能下载，见飞书原文）]\n如果你最近也在做 Agent 相关的事情，我特别建议你：别再把所有精力都花在调模型、调提示词上了。\n回过头来看看你的 Harness 长啥样，看看你有没有规则文件、有没有校验闭环、有没有任务编排、有没有评估机制、有没有失败恢复。这些东西，每一项都能让你的 Agent 上一个台阶。\n顺便说一个值得你深思的趋势。如果你真的把 Harness 这件事认真做几个月，你会发现一个很微妙的变化：你花在「亲手写代码」上的时间越来越少，花在「写规则、写流程、设计环境」上的时间越来越多。\nOpenAI Codex 团队那几个工程师能撬动百万行代码，靠的就不是键盘敲得快，而是把 Agent 的运行环境设计到位。\n写代码这件事本身会越来越多地交给 Agent，但设计一个能让 Agent 高质量产出的环境，这件事在很长一段时间里都得是人来做。这可能就是未来程序员真正的主战场。\n最后，再用开头那个在圈子里被广泛引用的等式给你收个尾：\nAgent = Model + Harness\n你的 Agent 想变得更好，要么换更强的模型，要么写更好的 Harness。在模型迭代速度逐渐放缓的今天，Harness 这部分的提升空间，可能比你想象的大得多。\n这就是今天想跟你分享的全部内容。\n我们下篇文章见。\n参考资料\nMitchell Hashimoto，《My AI Adoption Journey》, https://mitchellh.com/writing/my-ai-adoption-journey\nOpenAI，《Harness engineering: leveraging Codex in an agent-first world, https://openai.com/index/harness-engineering/\nAnthropic，《Effective harnesses for long-running agents》, https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents\nAnthropic，《Harness design for long-running application development》, https://www.anthropic.com/engineering/harness-design-long-running-apps\nAnthropic，《Effective context engineering for AI agents》, https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents\nLangChain，《Improving Deep Agents with harness engineering》, https://blog.langchain.com/improving-deep-agents-with-harness-engineering/\nCognition，《Rebuilding Devin for Claude Sonnet 4.5: Lessons and Challenges》, https://cognition.ai/blog/devin-sonnet-4-5-lessons-and-challenges\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AFHarness%20Engineering%EF%BC%9F/","summary":"!NOTE  这篇属于项目之外的读物，不学也不影响学习后续的 oncall agent项目的学习。  目的是帮助大家理解「Prompt 工程、Context 工程、Harness 工程」 最近圈子里又冒出来一个新词，叫「 Harness E","title":"什么是Harness Engineering？"},{"content":"把 OpenSpec 写进简历，重点不在于证明自己记住了多少命令，而在于说明：你能够把模糊需求变成可追踪的工程规范，并让 AI 按照明确边界完成实现和验收。招聘方真正关心的不是工具名称本身，而是你是否解决了需求漂移、跨模块协作、验收困难和文档失真等工程问题。\n[!SUCCESS] 简历可以写得有分量，但每一句都应该经得起追问。没有实际完成过的流程、没有统计依据的效率数据，以及并不存在的项目能力，都不要写成既成事实。\n先把 OpenSpec 翻译成招聘方能理解的能力 如果简历上只写“熟悉 OpenSpec”，它与“了解 Git”“使用过 Docker”没有本质区别，很难体现你的工程价值。更有效的表达方式，是先说明 OpenSpec 解决了什么问题，再说明你采取了什么动作，最后给出可以核验的交付物。\n📷 [图片 token=CSogbeCKjoTsVbxcqTDcmndxnOg（未能下载，见飞书原文）]\n问题： AI Coding 容易把需求理解成一次性提示词，随着对话变长，功能边界、异常分支和验收标准会逐渐丢失。\n动作： 使用 OpenSpec 把变更拆分为 Proposal、Design、Tasks 和规格增量，先明确目标与非目标，再进入代码实现。\n工程约束： 将共享契约、数据库迁移、租户隔离、失败状态、前后端联动和测试要求写入任务及验收标准，约束 Codex 的执行范围。\n可验证结果： 需求、设计、任务、实现、测试与归档在仓库中形成对应关系，后续开发者能够根据规格继续维护，而不是重新翻阅聊天记录猜测背景。\n把这四部分连起来，OpenSpec 就不再是一个孤立工具，而会体现为规范驱动开发、需求建模、工程治理和 AI 协作能力。\n📷 [图片 token=X9o9bflE8oX4W7xFcI0c6StOnvg（未能下载，见飞书原文）]\nOpenSpec 应该写在简历的什么位置 OpenSpec 可以出现在技能概述和项目经历中，但两处承担的任务不同。技能概述只负责让招聘方快速发现关键词，项目经历则必须说明你怎样使用它完成真实工作。\n技能概述中的写法：\nAI Native 工程实践：熟悉 Codex 与 OpenSpec 协作流程，能够完成需求澄清、规格设计、任务拆解、实现验证和变更归档。\n这句话适合放在技术技能末尾。它只能作为入口，不能代替项目证据。真正有区分度的内容，应写在一个你能够完整讲解的项目下面。\n📷 [图片 token=A1kDbpls6o2xPcxKXjncMn8Tn3c（未能下载，见飞书原文）]\n🔥结合 OncallAgent 的可直接使用的项目描述 智能 OnCall Agent 平台｜AIOps 智能运维工作台 **项目介绍：**面向研发与运维团队构建的一体化 AI Agent 平台，整合知识库、智能对话与 AIOps 故障诊断能力，实现从业务咨询、告警分析、工具取证到诊断报告和案例沉淀的自动化闭环，降低 OnCall 人工检索与故障排查成本。\n**技术栈：**FastAPI、Vue 3、TypeScript、LangChain、LangGraph、RAG、BM25、RRF、Rerank、SQLite、MCP、SSE、OpenSpec、Codex\n个人职责：\n负责 AI Agent 总体架构设计，基于 LangChain、LangGraph 构建知识索引 Agent、工具调用式 Chat Agent 和 Plan-Execute-Replan AIOps Agent，实现知识检索、任务规划、工具执行与报告生成等能力。\n负责 RAG 知识库系统设计，将单路向量检索升级为“Milvus 向量召回 + BM25 关键词召回 + RRF 融合 + Qwen Rerank 精排”的混合检索链路，并实现文档分块、异步索引、权限过滤和答案引用。\n负责 AIOps 诊断链路开发，接入真实腾讯 CLS 日志查询等 MCP 工具，实现“告警分析—知识检索—诊断规划—工具取证—根因分析—报告生成—案例入库”的完整闭环，并持久化诊断步骤与证据。\n引入 OpenSpec 规范驱动开发流程，将跨前端、后端、数据库和向量存储的需求拆解为 Proposal、Design、Tasks 与 Delta Specs，明确 API/SSE 契约、tenant 隔离、异常分支和验收标准，推动 Codex 按规格完成实现与验证。\n项目亮点：\n设计模块化 Agent 工作流，由 Agent 根据上下文自主选择知识检索及 MCP 工具；AIOps 采用有界 Planner—Executor—Replanner—Report 图编排，避免无约束执行并保证诊断过程可追踪。\n构建可解释的混合检索链路，同时保留 Vector、BM25、RRF 和 Rerank 各阶段分数与引用来源；检索及精排前严格应用 user、tenant、知识库和文档权限过滤。\n将文档索引和 AIOps 诊断升级为持久化后台任务，支持服务重启恢复、客户端断线续跑、超时、重试和取消；结合 SSE 实时输出对话内容、工具状态与诊断进度。\n通过 OpenSpec 建立“需求—设计—实现—测试—归档”的可追溯工程闭环，将 AI Coding 的临时对话上下文转化为仓库内长期规格，降低跨模块开发中的需求遗漏和契约不一致风险。\nOpenSpec 相关项目职责（任选一条写即可）： 引入 OpenSpec 规范驱动开发流程，将功能需求沉淀为 Proposal、Design、Tasks 和 Delta Specs，建立从需求分析、技术设计到实现验收的可追踪链路。\n使用 OpenSpec 管理知识检索、MCP 工具治理和 AIOps 诊断等跨模块变更，明确功能边界、异常分支、tenant 数据隔离与完成标准。\n将大需求拆分为可独立验收的纵向闭环，协调共享 HTTP/SSE 契约、FastAPI 后端、Vue 前端、SQLite/Milvus 持久化和自动化测试同步演进。\n结合 Codex 按 Tasks 执行实现，并通过 OpenSpec 校验、类型检查、后端测试、前端测试和文档构建核对实现与规格的一致性。\n在变更完成后归档 OpenSpec 产物，使已经落地的行为进入长期规格，保留设计背景、验收依据和后续维护入口。\n基于 OpenSpec 建立规范驱动开发流程，将需求拆解为 Proposal、Design、Tasks 与 Delta Specs，形成“需求—设计—实现—测试—归档”的可追溯闭环。\n用一个真实功能讲清 OpenSpec 的价值 面试官通常不会停留在“你用过什么命令”，而会继续追问 OpenSpec 如何影响实际开发。此时可以用 OncallAgent 的混合检索链路举例。\n📷 [图片 token=B1AibI0zxoWRszxy6buc6FXenQg（未能下载，见飞书原文）]\n实现知识检索时，我没有把需求只写成“增加 RAG 功能”，而是先在规格中明确向量检索、BM25L、RRF 融合和 Rerank 各自承担的职责，同时规定检索必须携带当前用户、tenant、知识库和文档范围。随后把工作拆分为共享契约、后端检索、Milvus 过滤、引用返回、前端展示和测试验证。Codex 按任务执行后，再根据验收项核对权限过滤、无命中结果和失败状态。这样交付的不只是一个能够演示的接口，而是一条边界清楚、可以验证和继续维护的检索链路。\n📷 [图片 token=LjNvbTs3poTsDJx4rqhcc8vhnhf（未能下载，见飞书原文）]\n这段回答的重点，是展示你能够从功能名称继续下钻到边界、契约、失败处理和验收，而不是堆砌模型或框架名。如果你对混合检索不熟，也可以换成自己真正参与过的 MCP 连接、流式 Chat、知识文档索引或 AIOps 诊断变更。\n📷 [图片 token=QC0YbVlJCooSsHxfQRFcnL0nnuf（未能下载，见飞书原文）]\n面试时怎样回答“为什么要用 OpenSpec” 可以先从没有规范时的问题讲起，再说明 OpenSpec 如何改变协作方式：\n单纯在聊天窗口里给 Codex 描述需求，早期看起来很快，但功能一旦涉及前后端、持久化、权限和测试，模型就容易遗漏约束。OpenSpec 的价值是把聊天中的临时上下文转成仓库内的长期事实来源。我会先通过 Proposal 说明为什么做和不做什么，通过 Design 确定关键边界，再把实现拆进 Tasks 和规格增量。完成后根据测试及验收项验证，最后归档进入主规格。这样 AI 负责提高执行速度，人仍然负责方向、边界和工程决策。\n📷 [图片 token=KuNWbItKeoS2LZxkQ5AcVs3Cnqh（未能下载，见飞书原文）]\n回答时不必把所有 OpenSpec 命令背一遍。面试官更希望确认你理解为什么先定义行为、为什么要保留非目标、为什么任务必须可验收，以及规格与实际代码不一致时应该怎样处理。\n不要把工具经历包装成无法证明的成绩 下面几类描述看起来醒目，但很容易在追问中失分：\n“精通 OpenSpec，研发效率提升 300%”——除非有明确的统计范围、基准和记录。\n“使用 OpenSpec 自动生成完整项目”——OpenSpec 负责规格与变更管理，不替代工程判断。\n“独立研发 OpenSpec 框架”——这TM是开源的，千万不要说是你做的，只是使用而已。\n“通过 OpenSpec 保证代码零缺陷”——规范和测试能够降低风险，但不能承诺零缺陷。\n只罗列 Proposal、Design、Tasks 等名词，却无法解释它们如何改变一次真实交付。\n📷 [图片 token=Swn2bZkUyonyvBxW1l0cIA07nyg（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/03%EF%BD%9COpenSpec%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5%EF%BC%9A%E4%BB%8E%E5%8F%98%E6%9B%B4%E5%BB%BA%E6%A8%A1%E5%88%B0%E7%9F%A5%E8%AF%86%E6%B2%89%E6%B7%80/%E6%80%8E%E4%B9%88%E6%8A%8AOpenSpec%E8%9E%8D%E5%85%A5%E7%AE%80%E5%8E%86%E6%8F%8F%E8%BF%B0/","summary":"把 OpenSpec 写进简历，重点不在于证明自己记住了多少命令，而在于说明：你能够把模糊需求变成可追踪的工程规范，并让 AI 按照明确边界完成实现和验收。招聘方真正关心的不是工具名称本身，而是你是否解决了需求漂移、跨模块协作、验收困难和文","title":"怎么把OpenSpec融入简历描述"},{"content":"使用场景 知识库Agent本质是就是RAG的过程。RAG（检索增强生成）是构建智能客服、企业知识库、产品问答助手的核心技术。当你需要让AI回答特定领域问题（如公司产品手册、内部文档）时，直接将长文本发送给模型，会受限于模型的上下文窗口大小，导致成本高、速度慢、准确率低。RAG通过先检索相关内容再生成答案的方式，完美解决了这些问题，广泛应用于企业知识管理、智能客服等场景 。在对话Agent和运维Agent里，都会使用到RAG。\n📷 [图片 token=VRe4baN4DouGZbxEIxycLrGInzF（未能下载，见飞书原文）]\n📷 [图片 token=XFX3b8NILolwucxe0ToceXDknQc（未能下载，见飞书原文）]\n核心流程 RAG的整体流程分为两部分：\n提问前（数据准备）：分片 -\u0026gt; embedding -\u0026gt; 存储\n提问后（回答生成）：召回 -\u0026gt; 重排 -\u0026gt; 生成\n📷 [图片 token=HDyZb38BKoGf2bxVAUnc6r56nDb（未能下载，见飞书原文）]\n提问前链路（数据准备） 分片：将原始文档（如业务告警处理手册）切割为多个语义完整的片段。\n索引：\n用Embedding模型将每个片段转为向量。 将片段文本和向量存入向量数据库。 完成后，知识库即构建完毕，等待用户提问。 分片：把文档拆成片段 分片是将完整文档切割为多个片段的过程，目的是让后续检索更精准。常见方式包括：\n按字数（如1000字/段）、按段落、按章节或页码拆分\n核心原则：确保每个片段语义完整，避免信息割裂\n例如，一本1000页的产品手册可能被拆分为500个独立片段，每个片段聚焦一个具体功能或问题。\n索引：将片段编成向量 索引是将分片后的文本转换为 向量 并存储的过程，分为两步：\nEmbedding（向量化）：用专门的Embedding模型将文本片段转化为向量。**核心：语义相近的文本，向量距离更近。 **\n存储到向量数据库：将文本片段及其对应向量存入向量数据库（如Milvus），方便后续快速查询。\n向量数据库不仅存储向量，还保留原始文本，因为最终生成答案需要的是文本内容，向量只是用于相似度计算。\n向量是数学中的基础概念，代表有大小、有方向的量，用数组表示（比如 [1, 2, 3] 是三维向量），维度=数组长度。低维向量（1-3维）可画在坐标轴上，高维向量（几百/几千维）虽无法可视化，但高维向量包含的信息更丰富，能更细腻地表达文本特征。 通过Embedding模型将文本片段转化为向量后，就能用数学方法计算语义相似度。比如 小林写python 和 小林写golang 的向量会非常接近。\nEmbedding 简单说就是把文字转成向量的过程，意思相近的句子向量也相近 。 比如 小林写python 和 小林写golang 这两句话意思相近，经过 Embedding 后会变成两个非常相似的向量（比如 [0.8, 0.2, -0.5] 和 [0.78, 0.22, -0.48]），而 牛牛玛特 的向量则会和它们相差很远。大模型本身看不懂文字，只能处理数字。通过 Embedding，文字的语义被转化成向量后，计算机就能通过计算向量之间的相似度来判断两句话是否相关。\n向量数据库核心作用：\n存储向量与文本：每个文档片段经过 Embedding 模型转换成向量后，会和原始文本一起存在向量数据库里（比如 小林coding 这句话，会存成 [content: \u0026ldquo;小林coding\u0026rdquo;, vector: [0.12, 0.34, \u0026hellip;]]）\n**相似性查询：**当用户提问时，问题会先转成向量，向量数据库通过计算向量相似度，快速从海量片段中找出最相关的结果（比如从 小林写python 能关联到 小林写golang ）\n提问后链路（回答生成） 召回：用户问题-\u0026gt;Embedding模型-\u0026gt;向量-\u0026gt;向量数据库-\u0026gt;Top 10相关片段。\n重排：Top 10片段-\u0026gt;Cross Encoder模型-\u0026gt;Top 3最相关片段。\n生成：Top 3片段+用户问题-\u0026gt;大模型-\u0026gt;最终答案。\n召回：快速捞出相关片段 当用户提问后，第一步是从向量数据库中召回相关片段：\n将用户问题通过Embedding模型转化为向量。\n用向量相似度算法计算问题向量与数据库中所有片段向量的相似度，挑出Top N（如10个）最相关的片段。\n特点：速度快、成本低，但准确率有限，适合初步筛选（类比 从1000份简历中挑出10份合格的 ）。\n向量相似度算法：\n余弦相似度 原理：计算两个向量夹角的余弦值，范围在-1到1之间。夹角越小，余弦值越接近1，相似度越高。\n特点：只关注方向，不考虑向量长度，适合文本语义匹配。\n欧式距离 原理：计算两个向量在空间中的直线距离，距离越小相似度越高。\n特点：受向量长度影响较大，适合需要考虑数值大小的场景。\n重排：给片段排优先级 召回的10个片段可能仍有冗余或相关性不足，需要进一步精筛：\n使用专门计算文本对相似度的模型，逐对计算用户问题与每个召回片段的语义相关性。\n从10个片段中选出Top K（如3个）最相关的片段。\n为什么不直接召回3个？召回用向量相似度（快但准度低），重排用Cross Encoder模型（慢但准度高），二者结合实现 先广撒网再精挑细选 ，效果优于一步到位。\n生成：让AI 写出答案 将重排后的3个相关片段与用户问题一起投喂给大模型，模型基于这些片段生成准确、简洁的回答。\n优势：避免模型幻觉 ，答案严格基于检索到的事实性内容，且输入内容少（仅3个片段），成本和速度都更优。\n总结 RAG通过 **分片-索引-召回-重排-生成 **的链路，解决了长文本问答的痛点，让AI既能懂知识又能说人话 。无论是企业知识库还是智能客服，掌握RAG技术就能搭建高效、可靠的问答系统 。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9ARAG%E5%85%A8%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90/","summary":"使用场景 知识库Agent本质是就是RAG的过程 。RAG（检索增强生成）是构建 智能客服、企业知识库、产品问答助手 的核心技术。当你需要让AI回答特定领域问题（如公司产品手册、内部文档）时，直接将长文本发送给模型，会受限于模型的上下文窗口","title":"架构设计：RAG全流程解析"},{"content":" 📷 [图片 token=ODG0baag2oN1Qyx95pJcXIT2nQq（未能下载，见飞书原文）]\n📷 [图片 token=KS16baPLioXEo1xBXg9cS7TAnqg（未能下载，见飞书原文）]\n前言 上一节我们实现了知识库Agent的上半部分，这一节我们来实现知识库的召回功能。\n核心代码：SuperBizAgent/src/main/java/org/example/service/VectorSearchService.java\n📷 [图片 token=Lowcb5oMoofPIax2BwRcEj2knRg（未能下载，见飞书原文）]\n召回 我们之前是将文档存储到了Milvus向量数据库里面，所以召回的时候也是从这个数据库去查询。\n将查询文本向量化\n相似度查询\n/** * 搜索相似文档 * * @param query 查询文本 * @param topK 返回最相似的K个结果 * @return 搜索结果列表 */ public List\u0026lt;SearchResult\u0026gt; searchSimilarDocuments(String query, int topK) { try { logger.info(\u0026#34;开始搜索相似文档, 查询: {}, topK: {}\u0026#34;, query, topK); // 1. 将查询文本向量化 List\u0026lt;Float\u0026gt; queryVector = embeddingService.generateQueryVector(query); // 2. 构建搜索参数 SearchParam searchParam = SearchParam.newBuilder()build(); // 3. 执行搜索 R\u0026lt;SearchResults\u0026gt; searchResponse = milvusClient.search(searchParam); // 4. 解析搜索结果 SearchResultsWrapper wrapper = new SearchResultsWrapper(searchResponse.getData().getResults()); List\u0026lt;SearchResult\u0026gt; results = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; wrapper.getRowRecords(0).size(); i++) { /// results.add(result); } return results; } } 首先我们对问题进行向量化，按照Spring AI 的sdk要求，拼接请求参数，然后调用\n/** * 生成向量嵌入 * 调用阿里云 DashScope Text Embedding API * * @param content 文本内容 * @return 向量嵌入（浮点数列表） */ public List\u0026lt;Float\u0026gt; generateEmbedding(String content) { try { // 构建请求参数 TextEmbeddingParam param = TextEmbeddingParam .builder() .model(model) .texts(Collections.singletonList(content)) .build(); // 调用 API TextEmbeddingResult result = textEmbedding.call(param); // 检查结果 List\u0026lt;Float\u0026gt; floatEmbedding = getFloats(result); return floatEmbedding; } } 然后我们使用Milvus的sdk，进行相似度，获取相似的向量数据\n// 2. 构建搜索参数 SearchParam searchParam = SearchParam.newBuilder() .withCollectionName(MilvusConstants.MILVUS_COLLECTION_NAME) .withVectorFieldName(\u0026#34;vector\u0026#34;) .withVectors(Collections.singletonList(queryVector)) .withTopK(topK) .withMetricType(io.milvus.param.MetricType.L2) .withOutFields(List.of(\u0026#34;id\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;metadata\u0026#34;)) .withParams(\u0026#34;{\\\u0026#34;nprobe\\\u0026#34;:10}\u0026#34;) .build(); // 3. 执行搜索 R\u0026lt;SearchResults\u0026gt; searchResponse = milvusClient.search(searchParam); 总结：对问题先进行向量化，然后根据向量，调用数据库的向量查询接口进行查询。\n总结 至此，RAG的分片、索引、召回功能我们都实现完了。后续会介绍其他Agent是怎么使用知识库，怎么结合召回来与大模型进行交互的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ARAG%E5%8F%AC%E5%9B%9E%E5%AE%9E%E6%88%982%28Java%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;ODG0baag2oN1Qyx95pJcXIT2nQq\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：RAG召回实战2(Java)"},{"content":" 📷 [图片 token=H4unbividoGVHAx7sJCcjMJAnor（未能下载，见飞书原文）]\n📷 [图片 token=BUXgbjV4OoIuZIxzZBBcX92Onng（未能下载，见飞书原文）]\n前言 这部分代码在：SuperBizAgent/src/main/java/org/example/controller/ChatController.java\n📷 [图片 token=KFu3b1Fuyo3tJsx3DSjcBldinsi（未能下载，见飞书原文）]\n流程梳理 对话Agent的核心目标是结合外部知识（RAG召回）与工具调用能力（ReAct模式），解决复杂问题。\n整体流程可概括为：\n用户输入 -\u0026gt; embedding -\u0026gt; 向量数据库召回\n构建带上下文(召回的内容)的 prompt\nReAct模式多轮交互\n最终输出答案\n实战 消息召回 这里留一个预召回的TODO给同学完成，代码实现非常简单，在召回实战章节其实已有介绍(searchSimilarDocuments)\n构建prompt /** * 构建系统提示词（包含历史消息） * @param history 历史消息列表 * @return 完整的系统提示词 */ public String buildSystemPrompt(List\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; history) { StringBuilder systemPromptBuilder = new StringBuilder(); // 基础系统提示 systemPromptBuilder.append(\u0026#34;你是一个专业的智能助手，可以获取当前时间、查询天气信息、搜索内部文档知识库，以及查询 Prometheus 告警信息。\\n\u0026#34;); systemPromptBuilder.append(\u0026#34;当用户询问时间相关问题时，使用 getCurrentDateTime 工具。\\n\u0026#34;); systemPromptBuilder.append(\u0026#34;当用户需要查询公司内部文档、流程、最佳实践或技术指南时，使用 queryInternalDocs 工具。\\n\u0026#34;); systemPromptBuilder.append(\u0026#34;当用户需要查询 Prometheus 告警、监控指标或系统告警状态时，使用 queryPrometheusAlerts 工具。\\n\u0026#34;); systemPromptBuilder.append(\u0026#34;当用户需要查询腾讯云日志时，请调用腾讯云mcp服务查询,默认查询地域ap-guangzhou,查询时间范围为近一个月。\\n\\n\u0026#34;); // 添加历史消息 if (!history.isEmpty()) { systemPromptBuilder.append(\u0026#34;--- 对话历史 ---\\n\u0026#34;); for (Map\u0026lt;String, String\u0026gt; msg : history) { String role = msg.get(\u0026#34;role\u0026#34;); String content = msg.get(\u0026#34;content\u0026#34;); if (\u0026#34;user\u0026#34;.equals(role)) { systemPromptBuilder.append(\u0026#34;用户: \u0026#34;).append(content).append(\u0026#34;\\n\u0026#34;); } else if (\u0026#34;assistant\u0026#34;.equals(role)) { systemPromptBuilder.append(\u0026#34;助手: \u0026#34;).append(content).append(\u0026#34;\\n\u0026#34;); } } systemPromptBuilder.append(\u0026#34;--- 对话历史结束 ---\\n\\n\u0026#34;); } systemPromptBuilder.append(\u0026#34;请基于以上对话历史，回答用户的新问题。\u0026#34;); return systemPromptBuilder.toString(); } 创建ReAct Agent 因为我们这里使用了Spring AI alibaba框架，所以不需要我们自己从0到1去实现，只需要按照sdk的要求使用，即可返回给我们一个可以执行的Agent\nAPI使用文档：https://java2ai.com/docs/frameworks/agent-framework/tutorials/agents\npublic ReactAgent createReactAgent(DashScopeChatModel chatModel, String systemPrompt) { return ReactAgent.builder() .name(\u0026#34;intelligent_assistant\u0026#34;) .model(chatModel) .systemPrompt(systemPrompt) .methodTools(buildMethodToolsArray()) .tools(getToolCallbacks()) .build(); } 执行ReAct Agent // 执行对话 String fullAnswer = chatService.executeChat(agent, request.getQuestion()); /** * 执行 ReactAgent 对话（非流式） * @param agent ReactAgent 实例 * @param question 用户问题 * @return AI 回复 */ public String executeChat(ReactAgent agent, String question) throws GraphRunnerException { logger.info(\u0026#34;执行 ReactAgent.call() - 自动处理工具调用\u0026#34;); var response = agent.call(question); String answer = response.getText(); logger.info(\u0026#34;ReactAgent 对话完成，答案长度: {}\u0026#34;, answer.length()); return answer; } 总结 至此，对话Agent的核心流程RAG召回与ReAct模式的代码就讲完了。如果你一篇一篇看下来，会发现其实代码实现真的不难，而且也不重要，框架帮我们做了很多事情。核心是要搞懂我们的设计原理：RAG、ReAct。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E5%AF%B9%E8%AF%9DAgent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Java%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;H4unbividoGVHAx7sJCcjMJAnor\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2070\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：对话Agent代码实现(Java)"},{"content":" 📷 [图片 token=KYx3br0ueo9g0axOLKVcTr7NnKe（未能下载，见飞书原文）]\n前言 关键代码：SuperBizAgent/src/main/java/org/example/service/AiOpsService.java\n📷 [图片 token=LwtAbnjmHo7qrwxHl6acnYEonmd（未能下载，见飞书原文）]\n流程梳理 运维Agent的核心目标是 规划-\u0026gt;执行-\u0026gt;评估-\u0026gt;调整。整体流程就是三个步骤：\nPlaner：拆解排查步骤\nExecuter：执行计划第一步\nReplaner：评估结果并调整计划\n📷 [图片 token=I7jFbBmF7oxsfOxyoYScsNNtnNh（未能下载，见飞书原文）]\n实战 创建Plan、Executer Agent 首先我们先使用Spring AI创建两个ReAct类型的Agent\nReplanner可以创建新的Agent，也可以复用Plan Agent。因为他们两个做的事情都是规划，我们代码简单点，复用Plan\n/** * 构建 Planner Agent */ private ReactAgent buildPlannerAgent(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) { return ReactAgent.builder() .name(\u0026#34;planner_agent\u0026#34;) .description(\u0026#34;负责拆解告警、规划与再规划步骤\u0026#34;) .model(chatModel) .systemPrompt(buildPlannerPrompt()) .methodTools(buildMethodToolsArray()) .tools(toolCallbacks) .outputKey(\u0026#34;planner_plan\u0026#34;) .build(); } /** * 构建 Executor Agent */ private ReactAgent buildExecutorAgent(DashScopeChatModel chatModel, ToolCallback[] toolCallbacks) { return ReactAgent.builder() .name(\u0026#34;executor_agent\u0026#34;) .description(\u0026#34;负责执行 Planner 的首个步骤并及时反馈\u0026#34;) .model(chatModel) .systemPrompt(buildExecutorPrompt()) .methodTools(buildMethodToolsArray()) .tools(toolCallbacks) .outputKey(\u0026#34;executor_feedback\u0026#34;) .build(); } 构建 Supervisor Agent Plan- Execute设计模式本质上就是多个Agent进行协作，这里我们使用框架里的Supervisor来完成。\nMulti-agent：https://java2ai.com/docs/frameworks/agent-framework/advanced/multi-agent\n📷 [图片 token=FawUbTdQyoQhIIxFV3acuWUyn4g（未能下载，见飞书原文）]\n使用框架的Supervisor Agent能力，可以自动的帮助我们管理Plan Agent和Executor Agent之间的执行扭转\n// 构建 Supervisor Agent SupervisorAgent supervisorAgent = SupervisorAgent.builder() .name(\u0026#34;ai_ops_supervisor\u0026#34;) .description(\u0026#34;负责调度 Planner 与 Executor 的多 Agent 控制器\u0026#34;) .model(chatModel) .systemPrompt(buildSupervisorSystemPrompt()) .subAgents(List.of(plannerAgent, executorAgent)) .build(); // 执行 String taskPrompt = \u0026#34;你是企业级 SRE，接到了自动化告警排查任务。请结合工具调用，执行**规划→执行→再规划**的闭环，并最终按照固定模板输出《告警分析报告》。禁止编造虚假数据，如连续多次查询失败需诚实反馈无法完成的原因。\u0026#34;; return supervisorAgent.invoke(taskPrompt); Plan Agent Prompt /** * 构建 Planner Agent 系统提示词 */ private String buildPlannerPrompt() { return \u0026#34;\u0026#34;\u0026#34; 你是 Planner Agent，同时承担 Replanner 角色，负责： 1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。 2. 分析 Prometheus 告警、日志、内部文档等信息，制定可执行的下一步步骤。 3. 在执行阶段，输出 JSON，包含 decision (PLAN|EXECUTE|FINISH)、step 描述、预期要调用的工具、以及必要的上下文。 4. 调用任何腾讯云日志/主题相关工具时，region 参数必须使用连字符格式（如 ap-guangzhou），若不确定请省略以使用默认值。 5. 严格禁止编造数据，只能引用工具返回的真实内容；如果连续 3 次调用同一工具仍失败或返回空结果，需停止该方向并在最终报告的结论部分说明\u0026#34;无法完成\u0026#34;的原因。 ## 最终报告输出要求（CRITICAL） 当 decision=FINISH 时，你必须： 1. **不要输出 JSON 格式** 2. **直接输出完整的 Markdown 格式报告文本** 3. **报告必须严格遵循以下模板**： 告警分析报告 \u0026quot;\u0026quot;\u0026quot;; }\n## Executor Agent Prompt ```java /** * 构建 Executor Agent 系统提示词 */ private String buildExecutorPrompt() { return \u0026#34;\u0026#34;\u0026#34; 你是 Executor Agent，负责读取 Planner 最新输出 {planner_plan}，只执行其中的第一步。 - 确认步骤所需的工具与参数，尤其是 region 参数要使用连字符格式（ap-guangzhou）；若 Planner 未给出则使用默认区域。 - 调用相应的工具并收集结果，如工具返回错误或空数据，需要将失败原因、请求参数一并记录，并停止进一步调用该工具（同一工具失败达到 3 次时应直接返回 FAILED）。 - 将日志、指标、文档等证据整理成结构化摘要，标注对应的告警名称或资源，方便 Planner 填充\u0026#34;告警根因分析 / 处理方案执行\u0026#34;章节。 - 以 JSON 形式返回执行状态、证据以及给 Planner 的建议，写入 executor_feedback，严禁编造未实际查询到的内容。 输出示例： { \u0026#34;status\u0026#34;: \u0026#34;SUCCESS\u0026#34;, \u0026#34;summary\u0026#34;: \u0026#34;近1小时未见 error 日志，仅有 info\u0026#34;, \u0026#34;evidence\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;nextHint\u0026#34;: \u0026#34;建议转向高占用进程\u0026#34; } \u0026#34;\u0026#34;\u0026#34;; } Supervisor Agent Prompt /** * 构建 Supervisor Agent 系统提示词 */ private String buildSupervisorSystemPrompt() { return \u0026#34;\u0026#34;\u0026#34; 你是 AI Ops Supervisor，负责调度 planner_agent 与 executor_agent： 1. 当需要拆解任务或重新制定策略时，调用 planner_agent。 2. 当 planner_agent 输出 decision=EXECUTE 时，调用 executor_agent 执行第一步。 3. 根据 executor_agent 的反馈，评估是否需要再次调用 planner_agent，直到 decision=FINISH。 4. FINISH 后，确保向最终用户输出完整的《告警分析报告》，格式必须严格为： 告警分析报告\\n---\\n# 告警处理详情\\n## 活跃告警清单\\n## 告警根因分析N\\n## 处理方案执行N\\n## 结论。 5. 若步骤涉及腾讯云日志/主题工具，请确保使用连字符区域 ID（ap-guangzhou 等），或省略 region 以采用默认值。 6. 如果发现 Planner/Executor 在同一方向连续 3 次调用工具仍失败或没有数据，必须终止流程，直接输出\u0026#34;任务无法完成\u0026#34;的报告，明确告知失败原因，严禁凭空编造结果。 只允许在 planner_agent、executor_agent 与 FINISH 之间做出选择。 \u0026#34;\u0026#34;\u0026#34;; } 总结 通过上面的分析，我们已经了解了Planner、Executer、Replanner的作用和相关prompt。但是你可能会有一种意犹未尽的感觉，因为我们在这里全部都是调用sdk，实际代码只是组装而已。不要慌，我们再回过头来看看Plan-Execute-Replan的流程。\n首先用Planner Agent生成了一份计划\n将计划发送给Executor Agent，让Executor按照计划执行\n每次执行完，都将计划和执行结果一起发送给Replanner评估\nReplanner评估后决定修改计划还是决定已完成\n其实 Planner、Executer、Replanner 之间的交互逻辑很简单，就是上面的4个步骤，只要你搞明白了这4个步骤。我们自己用代码实现这个workflow流程也很简单，其核心就是流程控制，与Plan对象在整个流程中的传递而已。\n所以无需担心，面试会问到的所有细节，在面试攻略篇章全部为你准备好了。（想想gorm，jdbc这些数据库sdk，我们也只是使用而已，会用即可，只要八股文准备的好，无需紧张～）\n📷 [图片 token=KsjGbgtTOo3Pdlxlex6cb7lVn0c（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E8%BF%90%E7%BB%B4Agent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Java%29/","summary":"\u0026lt;image token=\u0026ldquo;KYx3br0ueo9g0axOLKVcTr7NnKe\u0026rdquo; width=\u0026ldquo;2618\u0026rdquo; height=\u0026ldquo;2074\u0026rdquo; align=\u0026ldquo;center\u0026rdquo;/  前言 关键代码：SuperBizAgent/src/main/ja","title":"源码分析：运维Agent代码实现(Java)"},{"content":"配置修改 配置文件路径：SuperBizAgent/src/main/resources/application.yml\n按照《环境准备教程-项目配置》的步骤，修改配置文件\n一键式启动 输入make help可以看到启动命令，建议使用make init一键启动\n注意，在启动之前务必将配置文件中的api key填好。mcp端口也要填好（或者直接注释掉，走mock）\n📷 [图片 token=ECkmbB6Tco5raXxsj2zc79fCnfN（未能下载，见飞书原文）]\n📷 [图片 token=FtLobIIKdomUqMxBIZEc25oJnod（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE%E6%95%99%E7%A8%8B%28Java%29/","summary":"配置修改 配置文件路径： SuperBizAgent/src/main/resources/application.yml 按照《环境准备教程-项目配置》的步骤，修改配置文件  一键式启动 输入 make help 可以看到启动命令，建议使","title":"运行项目教程(Java)"},{"content":"在 OncallAgent 这个本地优先 AIOps Agent 工作台中，知识文档不是“上传完就可以检索”。一次成功上传只说明文件通过校验、可索引文本已提取、元数据已写入 SQLite；只有后续后台索引完成，chunk 向量才进入 Milvus，文档才真正具备检索能力。\n📷 [图片 token=JiukbqqNOo8oKAxCA1acepX1nOe（未能下载，见飞书原文）]\n当前链路刻意把上传、预览和索引拆开。上传负责建立可重复处理的事实记录，预览复用与索引相同的 chunking 实现，索引任务则异步执行切分、embedding、范围删除与批量写入。这样既能让 API 快速返回，也能把真实失败持久化到任务和文档状态中。\n📷 [图片 token=ZZyNbpuWMopAhXxaCV0cgK4hnnb（未能下载，见飞书原文）]\n对 AI Native 开发者而言，这条链路最重要的启示是：页面上的“正在索引”不能只靠一个前端布尔值模拟，成功也不能以“请求已接受”代替。文档状态、任务状态和 durable job 状态是三个相互关联但不同的层次。\n📷 [图片 token=SddqbCrsnolDYMxboPrc1qTlnJd（未能下载，见飞书原文）]\n学习目标 理解 Markdown、PDF 上传校验和文本提取的真实边界。\n掌握三种持久化 chunking 策略及有界预览。\n追踪 202 索引任务从 pending 到 running、succeeded 或 failed 的过程。\n理解重建索引、失败重试、覆盖上传和删除时的向量清理。\n正确解释前端轮询、业务任务状态和文档 indexStatus。\n功能入口与完整调用链 前端入口是 /knowledge，由 apps/frontend/src/views/KnowledgeView.vue 展示知识库、上传区、文档列表和详情。useKnowledgeStore 调用 createKnowledgeClient：先向 POST /knowledge-bases/{knowledge_base_id}/documents 发送 multipart 文件和 chunking JSON；上传成功后请求 chunk preview，再调用文档的 /index-tasks 创建首次索引任务。\n📷 [图片 token=GTOVb31TzoLY30x4a8icYhTMnGb（未能下载，见飞书原文）]\n后端 upload_knowledge_document 先通过 _validate_upload 检查扩展名、MIME 和 10 MiB 大小限制，再由 extract_indexable_text 提取正文。Markdown 必须是 UTF-8 且非空；PDF 使用 pypdf.PdfReader 逐页提取可选择文本，扫描图片型 PDF 没有文本时会被拒绝。服务计算 SHA-256，按 owner、knowledge base、hash 查重，把正文和 chunking 配置保存到文档 metadata。\n📷 [图片 token=JjCJbl8ysogzknx3qaNc2cIsnNd（未能下载，见飞书原文）]\n索引 API 创建 DocumentIndexTaskRecord 后立即调度 durable job 并返回 HTTP 202。后台 handler 调用 DocumentIndexingService.run_task，读取同一 owner 下的文档，按持久化策略切分，批量请求 embedding，初始化 Milvus，删除该文档旧 chunks，插入新 chunks，最后同步更新任务和文档的索引状态。\n📷 [图片 token=JXGebYszUo3356x02ozcyAB8nGg（未能下载，见飞书原文）]\nKnowledgeView → knowledgeClient.uploadDocument → upload_knowledge_document → 文件校验、文本提取、SHA-256、SQLite 文档记录 → chunk-preview → create_document_index_task 返回 202 → DurableDocumentIndexTaskScheduler → DocumentIndexingService.run_task → chunk → embedding → Milvus initialize/delete/insert → task succeeded 或 failed，document indexed 或 failed 📷 [图片 token=Ntn7b9Wyso0GUxxNCkRci69RnRb（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/api/app.py upload_knowledge_document、get_document_chunk_preview、create_document_index_task、retry_document_index_task 受保护上传、预览、创建任务、读取状态和失败重试路由。 apps/backend/src/super_ai/documents/extraction.py extract_indexable_text 区分 Markdown UTF-8 解码与 PDF 文本提取。 apps/backend/src/super_ai/documents/policy.py DOCUMENT_MAX_SIZE_BYTES、允许扩展名与 MIME 常量 定义后端上传策略。 apps/backend/src/super_ai/documents/indexing.py DocumentIndexingService、chunk_document_text、DocumentChunk 确定性切分、embedding 和范围向量重建。 apps/backend/src/super_ai/memory/sqlite.py SQLiteKnowledgeDocumentRepository、文档索引任务 Repository 持久化文档元数据、状态、失败原因与重试来源。 apps/backend/src/super_ai/vector_store/milvus.py MilvusVectorStore.insert_chunks、delete_document_chunks 批量写入以及 tenant、知识库、文档三重范围删除。 packages/api-contracts/src/documents.ts KnowledgeDocument、DocumentChunkingConfiguration、DOCUMENT_UPLOAD_POLICY 共享文档、状态、策略和预览形态。 packages/api-contracts/src/indexing.ts DocumentIndexTask、DocumentIndexTaskStatus 共享任务状态、失败原因和 retryOfTaskId。 apps/frontend/src/stores/knowledge.ts useKnowledgeStore、trackIndexTask、refreshIndexTask 上传编排、2 秒轮询、重建、重试和页面错误反馈。 apps/backend/tests/test_document_indexing.py test_document_indexing_service_writes_scoped_chunks_and_marks_success 验证服务写入 owner-scoped chunks 并转换真实状态。 openspec/specs/document-indexing-jobs/spec.md Non-blocking indexing execution 规定非阻塞、可恢复、失败可重试的索引任务。 📷 [图片 token=MJImbl1RJofNR1xvL5gcKu9Tnbg（未能下载，见飞书原文）]\n📷 [图片 token=BZZdb7V32oXUXJx3lXucEqxsnGh（未能下载，见飞书原文）]\n📷 [图片 token=RWF6bOyp4oWvbax2UL2cICBpnbg（未能下载，见飞书原文）]\n📷 [图片 token=E6iRbrUmhoHXDDxU0XzcHFr4nBf（未能下载，见飞书原文）]\n代码调用流程图 上传、任务受理和向量写入是三个不同阶段。流程图把 201、202 与最终 indexed 状态对应到不同源码节点。\n📷 [图片 token=AsBnbE0HfoIMryxzg8mc9l39n3c（未能下载，见飞书原文）]\n📷 [图片 token=UnA4bkpaFo7R76xq8dZcFInynNf（未能下载，见飞书原文）]\n关键实现拆解 上传不是索引：先保存可重复处理的输入 **看什么：**看上传路由如何先验证和提取文本，再用 owner 与知识库范围查内容哈希；只有这些输入持久化后，后续索引任务才有可重复读取的来源。\ncontent = await file.read() try: # 1. 类型、大小与可索引文本在创建记录前验证。 _validate_upload(file, content) try: indexable_text = extract_indexable_text(file.filename or \u0026#34;document\u0026#34;, content) except ValueError as exc: raise ApiErrorException(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, str(exc)) from exc chunking_configuration = _parse_chunking_configuration(chunking) content_hash = f\u0026#34;sha256:{sha256(content).hexdigest()}\u0026#34; repositories = _memory_repositories(request) # 2. 重复判断显式包含 owner 与知识库范围。 duplicate = await repositories.documents.find_active_by_hash( owner_user_id=user.id, knowledge_base_id=knowledge_base_id, content_hash=content_hash, ) if duplicate is not None and not overwrite: raise ApiErrorException(\u0026#34;BUSINESS_CONFLICT\u0026#34;) # … 省略 overwrite 时删除旧向量并标记旧文档的代码 # 3. 新记录保存可索引文本和用户选择的切分配置。 document = await repositories.documents.create_document( owner_user_id=user.id, document_id=f\u0026#34;doc_{uuid4().hex}\u0026#34;, knowledge_base_id=knowledge_base_id, # … 省略文件名、大小、MIME、哈希等参数 metadata={ \u0026#34;upload\u0026#34;: \u0026#34;user-selected\u0026#34;, \u0026#34;indexableText\u0026#34;: indexable_text, \u0026#34;chunking\u0026#34;: chunking_configuration, }, ) 📷 [图片 token=UJXLb5V68o7cRrxppbGcpU2qncc（未能下载，见飞书原文）]\n📷 [图片 token=Vm6tbnTZDoy4ezx5kuscE4D2nTc（未能下载，见飞书原文）]\n📷 [图片 token=MTGwbuxZDoTCwTxNptYcWoYsnQg（未能下载，见飞书原文）]\n📷 [图片 token=I2UzbHHkooG0cox3vOfcwAPYnyf（未能下载，见飞书原文）]\n📷 [图片 token=MqaRbFOqjonkUQxqfesc2RxHnFd（未能下载，见飞书原文）]\n这段代码证明上传成功只创建文档输入，不代表向量已生成；新记录的默认索引状态仍是 pending。overwrite 会创建新 document ID，且旧向量删除与新记录创建不在一个跨系统事务里，失败时不能假定两边会自动回滚。\n📷 [图片 token=SAvMbdDcyoZnmfxb7yJc7UDtnGf（未能下载，见飞书原文）]\nSQLiteKnowledgeDocumentRepository.create_document 默认创建 status 为 ready、index_status 为 pending 的记录。除了文件名、大小、MIME、hash 和时间，它还保存 indexableText 与 chunking。因此重建索引不需要用户重新上传原文件；索引服务从 SQLite 文档记录恢复输入。\n📷 [图片 token=AM8DbGp9PoVKbQxJsMocEqLInOh（未能下载，见飞书原文）]\n允许格式只有 .md 和 .pdf。Markdown 可接受浏览器常见的 text/markdown、text/plain、application/octet-stream 或空 MIME；PDF 扩展名必须与允许 MIME 一致。重复判断不只看文件名，而是使用 sha256: 前缀的内容哈希，并限定在当前 owner 和知识库。未明确 overwrite 时返回业务冲突；overwrite 为真时，先删除旧文档向量并把旧记录标记为 deleted，再创建新文档 ID。\n📷 [图片 token=KtgQbb4FEo62ulxICyocH9VZnfK（未能下载，见飞书原文）]\n当前实现把可索引正文存放在 SQLite JSON metadata 中，而不是单独对象存储。它适合当前 10 MiB 上限和本地工作台，但不能据此推断已经存在原始二进制文件归档、OCR 或对象存储版本管理；这些能力在这条实现中没有出现。\n📷 [图片 token=UX7db7jkfoOkRcxtvuUcdf5LnQv（未能下载，见飞书原文）]\n切分策略与预览复用 **看什么：**看公共切分入口如何根据持久化 strategy 分派，并在进入具体 splitter 前统一处理空文本和 fixed-character 参数边界。\neffective_chunk_size = DEFAULT_CHUNK_SIZE if chunk_size is None else chunk_size effective_chunk_overlap = chunk_overlap if chunk_overlap is not None else DEFAULT_CHUNK_OVERLAP # 1. 所有调用者共享同一组基本参数检查。 if effective_chunk_size \u0026lt;= 0: raise ValueError(\u0026#34;chunk_size must be greater than zero\u0026#34;) if effective_chunk_overlap \u0026lt; 0: raise ValueError(\u0026#34;chunk_overlap must be zero or greater\u0026#34;) normalized = text.strip() if not normalized: return [] # 2. 预览与真正索引都从这个 strategy 分派入口进入。 if strategy == \u0026#34;markdown-heading\u0026#34;: return _heading_chunks(normalized, effective_chunk_size) if strategy == \u0026#34;paragraph\u0026#34;: return _paragraph_chunks(normalized, effective_chunk_size) if strategy == \u0026#34;legacy-word\u0026#34;: return _legacy_word_chunks(normalized, effective_chunk_size) if strategy != \u0026#34;fixed-character\u0026#34;: raise ValueError(\u0026#34;Unsupported chunking strategy\u0026#34;) if effective_chunk_overlap \u0026gt;= effective_chunk_size: raise ValueError(\u0026#34;chunk_overlap must be smaller than chunk_size\u0026#34;) return _fixed_chunks(normalized, effective_chunk_size, effective_chunk_overlap) 📷 [图片 token=KVpqb5Sb6oxLjUxKBnMcz7jonnh（未能下载，见飞书原文）]\n📷 [图片 token=PEb9bUlnFoABQ9xjejkcdUIsnSc（未能下载，见飞书原文）]\n📷 [图片 token=RlvObB6JjofxwwxWR8yc2Tgdnef（未能下载，见飞书原文）]\n📷 [图片 token=IveabTAyQotSRdxC8dVcfH4Jnfe（未能下载，见飞书原文）]\n同一函数被 chunk preview 和 DocumentIndexingService 调用，因此保存的 strategy 与参数决定两处结果。空文本返回空列表，索引服务随后把它转成明确失败；这里没有 PDF 页码映射，start/end 仍只是提取后文本的字符位置。\n📷 [图片 token=Dv7jbsrgkoaBgJxFRUCcSIhpncb（未能下载，见飞书原文）]\n共享契约支持 fixed-character、markdown-heading 和 paragraph。固定字符策略要求最大字符数在 100 到 5000 之间，overlap 非负且小于最大字符数；默认是 1200 和 200。它使用 RecursiveCharacterTextSplitter，依次考虑空行、换行、空格和字符边界。Markdown 标题策略使用 MarkdownHeaderTextSplitter 处理一级到六级标题，过大的标题段再无重叠地固定切分。段落策略按双换行分组，过长单元同样回退到固定切分。\n📷 [图片 token=EJ1HbAPowoeHjjxlXzycDGbEnSC（未能下载，见飞书原文）]\nDocumentChunk 记录从 0 开始的 index、content、start、end 和可选 heading_path。预览 API 直接调用 chunk_document_text，返回总 chunk 数、是否超过 12 条、前 12 条的字符数与最多 400 字符 excerpt。预览和索引共享实现，避免“预览一种切法、真正索引另一种切法”。\n📷 [图片 token=RnksbK6kwoEisaxx9OYc907Tnpe（未能下载，见飞书原文）]\nchunk ID 使用文档 ID 加四位序号，例如 {document.id}_chunk_0000，在相同文档和相同切分配置下稳定。向量 metadata 还包含 chunk 边界、knowledgeType、chunkingStrategy、chunkingParameters，以及 ownerUserId、tenantId、knowledgeBaseId、documentId、chunkId。\n📷 [图片 token=Or2jbGip8oKqPTxmvkBcPZbFnIQ（未能下载，见飞书原文）]\n索引执行与真实失败 **看什么：**先看 worker 进入业务服务后最早发生的两个状态写入；它们位于后续统一异常捕获之前。\n# 1. durable job 已 running，领域 task 仍要单独更新。 await self._repositories.document_index_tasks.mark_running( owner_user_id=owner_user_id, task_id=task.id, ) # 2. 文档状态供页面表达正在构建索引。 await self._repositories.documents.update_index_status( owner_user_id=owner_user_id, knowledge_base_id=task.knowledge_base_id, document_id=task.document_id, index_status=\u0026#34;indexing\u0026#34;, ) 📷 [图片 token=CUu1bQOJjodLqZxBKKecygy1n3F（未能下载，见飞书原文）]\n这两次 Repository 调用不属于 Milvus 写入事务，也不在本方法的 catch 内。任一步骤自身失败会交给上层 durable runtime 处理，不能保证业务 task 已被本服务改成 failed；页面也可能短暂看到通用 job 和文档状态不同步。\n📷 [图片 token=UXYxb2NkMob5ESxWCijci6wbnbG（未能下载，见飞书原文）]\n**看什么：**再看真正的重建顺序：先得到完整 chunks 和等量 vectors，之后才初始化、删除旧范围并准备插入。\n# 1. 按文档保存的策略切分，空结果直接失败。 chunks = chunk_document_text( _indexable_text(document), strategy=strategy, chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) if not chunks: raise DocumentIndexingError(\u0026#34;Document has no indexable text.\u0026#34;) # 2. embedding 返回数量必须和 chunks 一一对应。 vectors = await self._embedding_model.aembed_documents( [chunk.content for chunk in chunks] ) if len(vectors) != len(chunks): raise DocumentIndexingError( \u0026#34;Embedding provider returned an unexpected vector count.\u0026#34; ) # 3. 只有向量完整后才删除当前文档的旧范围。 self._vector_store.initialize() self._vector_store.delete_document_chunks( tenant_id=owner_user_id, knowledge_base_id=document.knowledge_base_id, document_id=document.id, ) self._vector_store.insert_chunks( [ _vector_chunk_record( document=document, tenant_id=owner_user_id, chunk=chunk, vector=vector, ) for chunk, vector in zip(chunks, vectors, strict=True) ] ) 📷 [图片 token=PVdGbeBZNoUmx9x0993cxq4InWh（未能下载，见飞书原文）]\n📷 [图片 token=LHCEbARoPobCHFxRjoFcx9oyn1g（未能下载，见飞书原文）]\n📷 [图片 token=TQarbYasOodNoPx3clccT1DdnDf（未能下载，见飞书原文）]\n📷 [图片 token=R1NGbE3QJoppkzx6gZdcnQ3Pnxf（未能下载，见飞书原文）]\n📷 [图片 token=BGjibFnNSolqqCxxds3cwJUOnwe（未能下载，见飞书原文）]\n📷 [图片 token=OZoubI2fZov7aVxGnHXcPYSenAf（未能下载，见飞书原文）]\nembedding 失败不会先删旧索引，但删除成功、insert 失败仍会留下空档；catch 会诚实地把文档和 task 标为 failed。删除条件带 tenant、知识库与文档三重范围，不过 SQLite 状态与 Milvus 变更没有跨系统原子保证。\n📷 [图片 token=Q3xabh9PgoWPNwxSaooc84S3ntd（未能下载，见飞书原文）]\nrun_task 首先按 owner 读取任务，标记任务 running、文档 indexing。随后读取文档和策略，生成非空 chunks，调用 aembed_documents。返回向量数量必须与 chunk 数一致。只有 embedding 完成后才初始化 collection 和索引，再按文档范围删除旧 chunks，最后一次批量插入全部新 records。\n📷 [图片 token=LFWNbzTQhoKeHAxmtC0cHOjxnTe（未能下载，见飞书原文）]\n成功路径把文档设为 indexed，把任务设为 succeeded 并写完成时间。读取文档以及后续切分、embedding 和 Milvus 操作位于统一 catch 内：异常时文档设为 failed，任务设为 failed，保存至多 500 字符的 failure_reason，并返回失败任务。durable job handler 看到业务结果不是 succeeded 时会再次抛错，从而触发通用 job 的自动重试。需要注意，最初的任务读取、mark_running 和文档 indexing 状态更新在 catch 之外；这些 Repository 操作失败时由上层 job 捕获，业务任务不保证被这段服务代码转换为 failed。\n📷 [图片 token=NNQnbFBCBoq7xSxXvGGcKX0CnEg（未能下载，见飞书原文）]\n这里有两个诚实边界。第一，_safe_failure_reason 当前主要做空值兜底和 500 字符截断，并非通用秘密脱敏，因此 provider 和 vector store 层应只抛安全消息。第二，重建先删除旧 chunks 再插入新 chunks，不是跨 SQLite 与 Milvus 的原子事务；如果删除后插入失败，文档会明确显示 failed，而不会伪装成仍可用的成功索引。\n📷 [图片 token=JC3TbGnjnonK1MxEN57cy2R8nZg（未能下载，见飞书原文）]\n从页面动作推演一次完整状态变化 **看什么：**这张序列图把 HTTP 202、前端轮询、durable job 和两个业务状态分开，避免把“已接收”误读为“已索引”。\n📷 [图片 token=VMlfbafqPopaRAx2G1LcIHTRncb（未能下载，见飞书原文）]\n图中 202 只确认业务 task 与调度已接受，不等待 worker。task 与 document 来自不同读取接口，更新也不是同一原子快照；轮询停止条件是 task 终态，文档列表仍需重新读取才能对账最终 indexStatus。\n📷 [图片 token=QdZFbfgIpoDHDLx0WbBcLgUAnrh（未能下载，见飞书原文）]\n用户选择文件并点击上传时，前端先验证选择，再把文件、overwrite 和 chunking 放入 FormData。上传请求成功后，页面拿到的是 ready 文档和 pending 索引状态。store 随即读取 preview，并创建 index task。创建接口返回的 task 仍可能是 pending，因为后台 worker 是否已经领取并不属于 HTTP 202 的完成条件。store 把它加入 indexTasks 后开始每 2 秒轮询。\n📷 [图片 token=EMAvbeFFMoLkrVxZsFGcJczRn5g（未能下载，见飞书原文）]\nworker 领取 durable job 后，DocumentIndexingService 将业务 task 设为 running，同时把文档 indexStatus 设为 indexing。页面轮询的对象是 task，所以可以观察 pending 到 running；文档列表中的对象来自另一个 API，二者更新时刻并不完全相同。任务终态后计时器停止，用户重新打开详情或刷新列表时取得文档的 indexed 或 failed。页面还允许对任意已有文档触发 rebuild，这会新建一条 pending task，而不是覆写历史任务。\n📷 [图片 token=JAX6b2dIeoxBDrxAV4zcwpoHn6g（未能下载，见飞书原文）]\n如果索引失败，task.failureReason 是最直接的恢复依据。只有 failed 的 task 可以调用领域 retry 路由；后端验证知识库、文档和 owner 全部匹配后，用新 task ID 建立记录，并通过 retryOfTaskId 指向旧任务。新任务重新进入 durable scheduler。用户看到的是一条新的执行记录，旧失败原因仍保留，便于区分首次失败与后来恢复。\n📷 [图片 token=XOOKbVhpwo0w7rxZPnXcX672nEe（未能下载，见飞书原文）]\n切分确定性与稳定标识的范围 **看什么：**看 Milvus 记录的 chunk ID 与范围 metadata 如何从 document 身份和有序 chunk 生成；稳定性绑定的是同一个 document ID。\n# 1. 四位序号来自确定性 chunk 顺序，但前缀绑定 document.id。 chunk_id = f\u0026#34;{document.id}_chunk_{chunk.index:04d}\u0026#34; metadata: dict[str, object] = { **chunk.metadata, \u0026#34;knowledgeType\u0026#34;: _knowledge_type(document), \u0026#34;chunkingStrategy\u0026#34;: _chunking_kwargs(document)[0], \u0026#34;chunkingParameters\u0026#34;: _chunking_parameters(document), # 2. 检索和删除需要的 owner/tenant/资源范围一起写入。 **build_vector_chunk_metadata( owner_user_id=document.owner_user_id, tenant_id=tenant_id, knowledge_base_id=document.knowledge_base_id, document_id=document.id, chunk_id=chunk_id, ), } return VectorChunkRecord( chunk_id=chunk_id, document_id=document.id, knowledge_base_id=document.knowledge_base_id, owner_user_id=document.owner_user_id, tenant_id=tenant_id, content=chunk.content, vector=vector, metadata=metadata, source=document.source or document.filename, created_at=datetime.now(timezone.utc), ) 📷 [图片 token=ZBm1bVIGGoFUNhxeueacPC3bnNg（未能下载，见飞书原文）]\n📷 [图片 token=Awewbb0YVooFA2xSXCHciekPnzc（未能下载，见飞书原文）]\n📷 [图片 token=FCAab7ftXowHNfxnqzpc3DOUnie（未能下载，见飞书原文）]\n📷 [图片 token=JsPIbl2uBoxDRexcIzscJbTAn0c（未能下载，见飞书原文）]\n普通 rebuild 保留 document ID，所以相同文本和配置会复用同一序号体系；overwrite 创建新 document ID，chunk IDs 必然整体变化。metadata 提供范围与切分解释字段，但不会补出 PDF 页码，也不能让旧 citation 在身份更换后自动指向新 chunk。\n📷 [图片 token=VombbyLWPoGu7Zxu3zGclVFNnqb（未能下载，见飞书原文）]\n所谓确定性，是指同一份已保存文本、同一文档记录和同一 chunking 配置会得到相同顺序、边界与 chunk 序号。固定字符切分会利用原文查找位置，并根据 overlap 推进 cursor；标题和段落切分也按源文本顺序组装。测试不仅检查前几个 chunks，还覆盖超过 10 个 chunks 的索引，避免实现暗含小文档限制。\n📷 [图片 token=KNiJbzmUyo99hpxunkJcnSXnnkf（未能下载，见飞书原文）]\nchunk ID 包含 document.id，所以 overwrite 上传即使字节完全相同，也会先删除旧文档并创建新 document ID，随后产生一组新的 chunk IDs。稳定性并不跨越文档身份更换。普通 rebuild 保留 document ID，因此在策略不变时 chunk ID 稳定；如果用户未来改变持久化配置并重建，序号之后的内容映射可能变化，引用消费者必须把 document ID、chunk ID 和 metadata 一起看待。\n📷 [图片 token=Obcab0EfBoFq0GxtIYtc5PxfnYg（未能下载，见飞书原文）]\nstart 和 end 是面向可索引文本的字符位置，不是 PDF 原始字节偏移，也不是页码。PDF 提取把有文本的页面用空行拼接，当前 metadata 没有持久化 page number。由此可以提供 chunk excerpt 和来源文件，却不能把现有实现描述成精确 PDF 页定位。Markdown heading 才有可选 headingPath，而且只有解析到标题元数据时出现。\n📷 [图片 token=TyXVbW9pCoEdiYxT14CcmDVGnAI（未能下载，见飞书原文）]\n外部资源调用的顺序为何重要 **看什么：**这张图只标出当前真实调用顺序，并突出唯一危险窗口：旧向量已经删除，而新批次尚未成功插入。\n把 embedding 和数量校验放在删除前，减少了上游失败破坏旧索引的机会；但 G 到 H 之间没有版本化集合或原子别名切换。此时失败会留下明确 failed 状态，而不是继续宣称旧索引可用。\n📷 [图片 token=HgFRbOWJXoBC2lxgIT3cYVhHn0g（未能下载，见飞书原文）]\n服务先切分，再一次性调用 embedding 获取全部 vectors；数量核对通过后才触碰 Milvus。这个顺序避免 embedding 半失败时先删除已有索引。随后 initialize 确保 collection、标量索引、向量索引与加载状态，再范围删除旧 chunks，最后批量 insert。维度不匹配会在 MilvusVectorStore._chunk_to_entity 构造实体时被拒绝。\n📷 [图片 token=KrlsbBIUwopD6Dxnq5BcnAEXn0b（未能下载，见飞书原文）]\n删除后插入仍可能失败，因此重建并非零停机切换。系统用 failed 状态诚实表达这种窗口，没有保留旧版本 collection 或双写影子集合。若业务要求“新索引完全就绪前旧索引持续服务”，需要引入版本化文档索引、临时分区或原子别名切换；这些都不是当前实现，教学时不能把合理演进方向说成已有机制。\n📷 [图片 token=CJlkbJayCoS9uzxnsWbctfwcnXg（未能下载，见飞书原文）]\n删除文档走相反方向：API 先确认 owner 下文档存在，调用范围向量删除，再把 SQLite 记录标记 deleted。若 Milvus 删除抛错，请求不会继续伪装成功。文档列表默认过滤 deleted，Repository 仍支持内部 include_deleted 查询，这为审计保留了元数据，但不代表恢复删除的公开 API 已经实现。\n📷 [图片 token=N3INbymJwoQ8cCxb9CMcd86enTc（未能下载，见飞书原文）]\n上传文本的安全与容量取舍 **看什么：**看服务端上传校验的真实边界：空内容、10 MiB、扩展名和 MIME 组合都会在提取与持久化前被拒绝。\nfilename = file.filename or \u0026#34;\u0026#34; suffix = PurePosixPath(filename).suffix.lower() mime_type = file.content_type or \u0026#34;\u0026#34; display_mime_type = mime_type or \u0026#34;empty\u0026#34; # 1. 空文件与超过 10 MiB 的输入先失败。 if not content: raise ApiErrorException( \u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;文件不能为空，请上传包含正文的 Markdown 或 PDF。\u0026#34;, ) if len(content) \u0026gt; DOCUMENT_MAX_SIZE_BYTES: raise ApiErrorException(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;文件大小不能超过 10 MB。\u0026#34;) # 2. 扩展名和浏览器 MIME 变体必须同时落入允许目录。 if suffix not in ALLOWED_DOCUMENT_EXTENSIONS: raise ApiErrorException( \u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;仅支持 Markdown(.md) 与 PDF(.pdf) 文件。\u0026#34;, ) if suffix == \u0026#34;.md\u0026#34; and mime_type not in MARKDOWN_DOCUMENT_MIME_TYPES: raise ApiErrorException( \u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, f\u0026#34;Markdown 文件的 MIME 类型不符合要求：{display_mime_type}。请上传 .md 文件。\u0026#34;, ) 📷 [图片 token=Qpu6bSPkYo5zVrxhNRCc67AsnZe（未能下载，见飞书原文）]\n📷 [图片 token=QujobH9WVouqyXx3MVVc9pminBh（未能下载，见飞书原文）]\n📷 [图片 token=TagebnRnyo8XKJxY0Wycj5RrnJ0（未能下载，见飞书原文）]\n校验限制的是上传字节，不是提取后的字符数、chunk 数或 token 数；大型合法文档仍可能让 embedding 或 durable timeout 失败。原始 filename 作为元数据保存且应按不可信文本展示，完整可索引正文保存在本地 SQLite，也没有文档级加密或自动脱敏。\n📷 [图片 token=Gv8zbcMLJogai6xhflCcBu6inxb（未能下载，见飞书原文）]\n后端从 UploadFile.filename 读取文件名，以 PurePosixPath(filename).suffix 校验扩展名，并把原始 filename 保存到文档记录；当前代码没有额外执行 basename 归一化。索引使用保存的 filename 作为默认 source。正文存入 JSON metadata，API 的普通文档 payload 不回传 indexableText，只返回对页面必要的元数据和 chunking 配置；preview 也只返回有限 excerpt。这样减少了列表接口泄露完整文档正文的风险，但文件名本身仍应按不可信展示文本处理。\n📷 [图片 token=WbVnbNmk1ocUAkxDeVtcqSZunId（未能下载，见飞书原文）]\n不过，SQLite 仍然保存完整可索引文本，拥有本地数据库访问权的人能够读取它。当前系统是本地优先而非客户端加密知识库，代码没有实现文档级加密、内容脱敏或保留期限。日志规范要求不记录文档正文，索引服务的结构化日志只记录 taskId、documentId、chunkCount、耗时和异常类别。\n📷 [图片 token=Ips0bzCuKoFjruxKbAtc2VFancg（未能下载，见飞书原文）]\n10 MiB 是上传字节上限，不等于提取文本或 token 的精确上限。大型 PDF 可能产生很多 chunks；索引服务把完整文本列表一次交给 aembed_documents，但默认 OpenAIEmbeddings 客户端配置 chunk_size=10，会在 provider 层按 10 条一批发送。当前没有单文档 chunk 数硬上限。durable timeout 和失败状态为超时提供可观察边界，但容量规划仍需依据具体 embedding provider 和本机资源验证。\n📷 [图片 token=CqxxbxIbnoQHDTxQdu2cuRqIn7e（未能下载，见飞书原文）]\n前端错误提示通过统一 ApiClientError 和 toUserFacingError 转成中文反馈。重复上传会保留 pendingOverwriteFile 与对应 chunking，用户确认后才以 overwrite 重新提交；取消确认不会修改服务端文档。页面销毁或登出时 store 会停止所有轮询 timer 并清空受保护状态，避免下一位用户看到前一位用户的任务缓存。\n📷 [图片 token=S0UGbbm5foI0y2xE21pckwxanqd（未能下载，见飞书原文）]\n数据、契约与状态 文档的 status 只有 ready 与 deleted；indexStatus 是 pending、indexing、indexed、failed。索引任务另有 pending、running、succeeded、failed、cancelled。上传响应为 201，索引创建与重试为 202，表示已持久化并接受后台处理，不表示已完成。\n📷 [图片 token=GXV8bf6H7o2f1kxMtT2c0DiPn8e（未能下载，见飞书原文）]\n任务包含 failureReason、retryOfTaskId、created、updated、started、completed 时间。失败重试不是修改原任务，而是 create_retry 新建一条任务并关联来源。页面 store 把 pending 和 running 视为 active，每 2 秒读取任务；终态后停止该 task 的计时器。页面同时保留文档列表和任务列表，不能把某个任务的 succeeded 直接等同于所有历史任务都成功。\n当前 refreshIndexTask 在轮询终态时更新任务并停止轮询，但没有在该函数中重新拉取文档列表；上传流程初始会显示索引过程，文档的最终 indexStatus 可在重新加载文档、切换知识库或其他显式刷新路径后取得。描述页面行为时应以代码为准，不应宣称每次任务终态都立即刷新文档对象。\n📷 [图片 token=UNwPbc4Qoo41tgxEgUgcELIXnFh（未能下载，见飞书原文）]\n权限、安全与失败边界 后端用 _ensure_knowledge_base_access 要求知识库 ID 精确等于当前 kb_{user.id}。文档 Repository 的 create、get、list、hash 查重、删除和 indexStatus 更新都携带 owner_user_id 与 knowledge_base_id；索引任务也按 owner 读取。跨 tenant 请求统一返回 AUTH_FORBIDDEN，并且在创建任务或触碰 Milvus 前结束。\n📷 [图片 token=SyOhbecvPotWvOxBysxcKzGwncb（未能下载，见飞书原文）]\n向量清理始终提供 tenant、knowledge base、document 三个非空范围。删除文档时先调用范围向量删除，再标记 SQLite 文档 deleted；覆盖上传遵循相同范围。索引生成的每个 chunk 同时在标量字段和 metadata 中保留 ownership。空文档、无文本 PDF、向量数量异常、维度不符或 Milvus 写入失败都必须显式失败，不生成占位向量或虚假索引成功。\n📷 [图片 token=WPRMbxSfioRng9xZYsDcwCTsnMe（未能下载，见飞书原文）]\n阅读顺序与小结 先从共享 documents.ts 与 indexing.ts 认识页面能观察到的状态。\n再读上传路由和 extraction.py，确认真正保存了什么输入。\n随后逐步跟进 chunk_document_text 和 DocumentIndexingService.run_task。\n最后对照 knowledge store、Milvus 范围操作和状态转换，确认索引成功与失败分别留下什么事实。\n完整知识链路的成功条件是：文件有效、正文可提取、配置可复现、任务被后台领取、chunks 可生成、embedding 数量正确、Milvus 重建成功、SQLite 状态提交完成。OncallAgent 把这些阶段显式化，正是为了让失败成为可诊断的数据，而不是页面上的一个模糊提示。每个状态都应能追溯到真实执行阶段。\n📷 [图片 token=PnKJbnayPo8xXTxJu1NcX0OGnzd（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/07.%20%E7%9F%A5%E8%AF%86%E6%96%87%E6%A1%A3%E4%B8%8A%E4%BC%A0%E3%80%81%E5%88%87%E5%88%86%E3%80%81%E7%B4%A2%E5%BC%95%E4%B8%8E%E9%A1%B5%E9%9D%A2%E7%8A%B6%E6%80%81/","summary":"在 OncallAgent 这个本地优先 AIOps Agent 工作台中，知识文档不是“上传完就可以检索”。一次成功上传只说明文件通过校验、可索引文本已提取、元数据已写入 SQLite；只有后续后台索引完成，chunk 向量才进入 Mil","title":"07. 知识文档上传、切分、索引与页面状态"},{"content":" [!NOTE] 这篇属于项目之外的读物，不学也不影响学习后续的 oncall agent项目的学习。 目的是帮助大家理解「Loop 工程」\n大家好，我是小林。\n不知道你有没有跟我一样的感觉：AI 圈造新词的速度，已经超过我学习的速度了。\n前年说会用 AI 的关键是 prompt engineering，赶紧学；去年又说 prompt 过时了，现在流行 context engineering，行，接着学；今年 3 月冒出来一个 harness engineering，我还没整明白 harness 到底该怎么翻译，这才 6 月，又来了。\n这次的新词叫 loop engineering。\n讲真，我第一反应是翻白眼：又来？是不是把 engineering 前面换个单词就能造一个赛道？\n但我把来龙去脉扒了一圈之后，我收回了白眼。\n📷 [图片 token=ZsJOb0nNFotVpFxB6Usca6g4nc4（未能下载，见飞书原文）]\n这个词不是哪个营销号造的。\n点火的是 Peter Steinberger，开源 agent 项目 OpenClaw 的作者，他 6 月 7 日发了条推文，说：「每月例行提醒：你不该再给 coding agent 打 prompt 了。你该去设计那个给 agent 打 prompt 的 loop。」\n📷 [图片 token=J6TKbFgK7oeWCdxflLqcINPXn5e（未能下载，见飞书原文）]\n给概念定名写长文的是 Addy Osmani，Google Cloud 的 AI 总监：\n📷 [图片 token=XVUabPiqAoQPj6xkJKBc4lOjn6f（未能下载，见飞书原文）]\n而几天前，Claude Code 创始人 Boris Cherny 在访谈里说了一句更猛的话，等于提前给这个概念背了书：\n「我已经不 prompt Claude 了。是 loop 在运行着 prompt Claude、决定做什么。我的工作是写 loop。」\n一个造工具的、一个定方法论的、一个每天泡在一线的产品创始人，三个人在同一周说了同一件事。\n这种程度的共振，值得认真看一看。\n📷 [图片 token=UChbbVm66oieWuxL2KGcZCfInKc（未能下载，见飞书原文）]\n这篇文章我整理成 6 个问题：\nQ1：四个 engineering，到底都在 engineering 什么？\nQ2：Loop Engineering 到底是什么？\nQ3：一个 loop 由什么组成？\nQ4：拼起来之后，一个真实的 loop 长什么样？\nQ5：工具已经追上来了，现在就能搭\nQ6：三盆冷水：loop 越好用，这三个问题越尖锐\n我们一个一个来说。\nQ1：四个 engineering，到底都在 engineering 什么？ 在讲 loop 之前，得先把欠的账还了：prompt、context、harness 这三个词，很多人到现在也只是「听过」，没真正分清。\n先问一个问题：为什么这些词会一个接一个地冒出来？是 AI 圈闲得慌吗？\n还真不是。每个新词的出现，背后都是同一件事：上一个瓶颈被解决了，新的瓶颈暴露出来了。把这条线从头捋一遍，四个词一下就清楚了。\n时间回到 2023 年。那时候模型只会一问一答，你问得好不好，直接决定答得好不好。\n于是大家研究话术：角色扮演、思维链、少样本示例。\n这就是提示词工程（Prompt Engineering），本质是跟一个聪明但一根筋的实习生说话的技巧，同一件事换个问法，效果天差地别。这个阶段的瓶颈，卡在「怎么说」。\n但话术的红利吃不了太久。到了 2025 年，模型变强了，也开始当 agent 干活了，光会说话不够用了。\n你让它改一个 bug，它写得再漂亮也没用，因为它没看过你的代码、不知道你的规范、不了解之前的讨论。话术解决不了「巧妇难为无米之炊」。\n于是瓶颈从「怎么问」移到了「喂什么」：把对的代码、文档、工具、历史记忆，在对的时机塞进上下文窗口。这就是上下文工程（Context Engineering），当时 Shopify 的 CEO 和 Karpathy 先后带火了这个说法。\n类比一下：你不再纠结怎么跟实习生说话，而是开始给他准备一桌整理好的资料。\n📷 [图片 token=OL52bZDmJoYAbhxLbiCcZE3unWd（未能下载，见飞书原文）]\n材料的问题刚解决，新的短板马上接棒。2026 年初，模型已经能连续干几个小时的活了，这时候卡脖子的不再是材料，而是它干活的「环境」跟不上。\n它需要工具去执行命令、需要权限边界防止误伤、需要沙箱安全地试错、需要派出子 agent 分头探索、需要上下文管理机制防止越干越糊涂。这一整套围绕模型搭建的运行装备，业内叫 harness（直译是马具，可以理解成 agent 的「驾驶舱」）。\n今年 3 月前后，Anthropic、OpenAI、LangChain 几家几乎同时发文章讨论这件事，还有人给出了一个很好记的公式：Agent = 模型 + harness。同一个模型，驾驶舱不一样，能力可以差出几倍。\n📷 [图片 token=IwPzbneJRomddQxByVScVHGkn2b（未能下载，见飞书原文）]\n话术、材料、驾驶舱，三道坎都迈过去了。那最后剩下的瓶颈是谁？\n你自己。\n模型在等你布置任务，harness 在等你启动，材料在等你投喂。整条流水线上，唯一还需要人肉驱动的环节，就是你坐在屏幕前敲下一条 prompt。你睡觉，它就停工。\n📷 [图片 token=APgVbUih2oUxQJxYQwHcGTDqnVf（未能下载，见飞书原文）]\nloop engineering 瞄准的就是这最后一环：设计一个系统，让「下一次回车」不再由你来按。\n📷 [图片 token=Cta4bTCT0oTBwQxORnRcGF1nnRg（未能下载，见飞书原文）]\n看出规律了吗？模型每变强一截，瓶颈就往外移一层：从你说的那句话，到你给的那堆材料，到它干活的环境，最后落到你本人身上。\n所以这些词不是营销轮换，是瓶颈迁移的路标。\n四个 engineering，本质是同一场瓶颈迁移：最后一个瓶颈，是坐在键盘前的你。\nQ2：Loop Engineering 到底是什么？ 铺垫完了，现在正面回答：loop engineering 是什么？\n开头 Steinberger（OpenClaw 的作者） 那条 800 多万浏览的推文，只负责把口号喊响：别再 prompt 了，去设计 loop。但口号当不了定义。\n紧接着，Osmani（Google Cloud 的 AI 总监） 的长文给出了正式定义：\n「Loop engineering 就是把『亲自给 agent 写 prompt 的那个你』替换掉。你转而去设计那个代替你做这件事的系统。」\n他还补了一句对 loop 本身的解释：loop 可以理解为一个递归式的目标，你定义一个目的，AI 持续迭代，直到完成。\n说人话就是：过去两年，你跟 coding agent 的协作方式是回合制的，你写一条 prompt，读它的输出，再写下一条。agent 是工具，你全程握着它，一回合都不能松手。\nloop engineering 说的是，松手吧。你把「发现任务、布置任务、检查结果、决定下一步」这套流程设计成一个能自己运转的循环，然后让循环去握着 agent。\n📷 [图片 token=ARpPbrJSKojircxEKFSc6VfLnh5（未能下载，见飞书原文）]\n打个比方。以前你是客服热线的接线员，每个电话都要你亲自接、亲自答；现在你升级成了设计工单系统的人：电话怎么分流、哪类问题转给谁、办结标准是什么、办不了的怎么升级到你，规则定好，系统自己转。\n你没有离开这家公司，但你的岗位变了。\n这也是 Claude Code 创始人说的那句「我的工作是写 loop」的真实含义。注意，他没说工作变轻松了。这句话真正的重点是：工作没有变容易，是杠杆的支点移动了。\n什么叫支点移动？以前你写一条好 prompt，收益是「这一次回答变好」；现在你设计一个好 loop，收益是「之后每一次循环都变好」。投入从消耗品变成了资产。\n但反过来，设计 loop 也比写 prompt 难得多：你要考虑触发、并行、验证、状态、止损，相当于从「说一句话」升级到「设计一套制度」。杠杆变长了，对握杠杆的人要求也变高了。\n📷 [图片 token=Wrn9b6zfpoE0sNxPKVrcrrhMnCh（未能下载，见飞书原文）]\nloop engineering 一句话：你不再是 prompt 的作者，你是 prompt 生产系统的设计师。\nQ3：一个 loop 由什么组成？ 概念清楚了，落地的问题马上来了：一个能自己运转的 loop，到底需要哪几样东西？\n把那些真正跑起来的 loop 拆开看，你会发现零件出奇地一致：五大件，外加一个记东西的地方。我们一件一件过，每一件都对应一个「不装它就会翻车」的具体场景。\n第一件：自动化，loop 的心跳 先想一个问题：你写了一个很完美的工作流脚本，但每次都要你手动启动，它算 loop 吗？\n不算。自动化才让 loop 成为真正的 loop，否则它只是一个你跑过一次的任务。\n所以第一件就是定时或事件触发：每天早上自动扫一遍 CI 失败、每次 PR 合并自动跑一轮检查。心跳有了，循环才算活着。\n📷 [图片 token=Jws6bIOuFo6yJ0xsLM9cMZqZnVb（未能下载，见飞书原文）]\n第二件：worktree，让并行不变成打架 loop 一旦跑起来，经常是几个 agent 同时干活。这时候你会撞上一个特别具体的麻烦：两个 agent 同时改同一个文件。\n就像两个工程师挤在同一台电脑上改同一行代码，还互相不打招呼。\n解法是 git 的 worktree 机制：给每个 agent 一个独立的工作目录和独立分支，共享同一份仓库历史，但物理上互不干扰。各干各的，最后各开各的 PR。\n📷 [图片 token=WKZzbOzRuozdCIxw9uUcJRm9ndc（未能下载，见飞书原文）]\n第三件：skill，治好 agent 的「金鱼记忆」 agent 有个天生缺陷：每个会话都是冷启动，你项目里的规范、约定、坑，它一概不知。\n于是你不得不像对金鱼一样，每个会话把项目重新解释一遍。更要命的是，你没解释到的地方，它不会空着，它会用一个自信的猜测填上。\nskill 就是把这些项目知识写成文件放在仓库里，让 agent 该用的时候自己读。这件事对 loop 的意义比对单次会话大得多：没有 skill，loop 每个周期都从零重新推导你的项目；有了 skill，知识是复利的。\n📷 [图片 token=COxgbPdGyoid32xcvXIcqc6Mnve（未能下载，见飞书原文）]\n第四件：connector，让 loop 摸到真实世界 一个只能看见文件系统的 loop，撑死了算半个 loop。\n真实的工作流不止于代码：要读 issue 工单、查监控、发消息、开 PR。connector（基于 MCP 协议的连接器）就是把这些外部系统接进来的桥。\n接上之后的差别有多大？一个 agent 只会告诉你「修复方案在这里」，而一个完整的 loop 会自己开好 PR、关联好工单，等 CI 变绿之后自己去频道里通知人。\n📷 [图片 token=Ldllb53WUoURoAx8eG5cOc5gnZc（未能下载，见飞书原文）]\n第五件：sub-agent，写的人和查的人必须分开 五大件里最有用的结构性设计，我认为遥遥领先的一条，是这个：把写代码的 agent 和检查代码的 agent 分开。\n为什么？理由只有一句话，但谁听谁服：写代码的那个模型，给自己的作业打分时，实在太手下留情了。\n让 A 出方案，让一个干净上下文的 B 来挑刺，B 没有「希望自己是对的」的包袱，挑出来的问题才是真问题。\n📷 [图片 token=Vu7ib0DOQotk2KxROircSeA6n9h（未能下载，见飞书原文）]\n第六件：记忆，loop 的命根子 最后这件听起来最不起眼，但它是整个 loop 的命根子。\n问题是这样的：模型在两次运行之间会忘掉一切。今天的循环干了什么、哪些做完了、哪些卡住了，明天的循环一概不知道。\n解法朴素到让人意外：把记忆放在磁盘上，而不是上下文里。一个 markdown 文件、一个任务看板，什么都行，只要它活在单次对话之外，记录着「做完了什么、下一步是什么」。\n这件事，Osmani 的博客里留了一句很妙的总结：\n「agent 会忘，但 repo 不会。」\n📷 [图片 token=BgnebgoUlorFkRxhPgsc1mP5nBd（未能下载，见飞书原文）]\n📷 [图片 token=Hp9wb6i0ronbYkxDFDocQ46UnhM（未能下载，见飞书原文）]\n五大件让 loop 转得起来，磁盘上的记忆让它第二天还接得上。\nQ4：拼起来之后，一个真实的 loop 长什么样？ 零件都认识了，该看整车了。\nOsmani 在文章里给了一个他自己在用的 loop，我把流程完整搬过来。这个 loop 的任务是：每天早上自动把项目里值得修的问题找出来、修好、提交审核。\n第一步，每天早晨，自动化准时触发，调用一个负责分诊的 skill。\n第二步，这个 skill 去读昨天的 CI 失败记录、还没关闭的 issue、最近的提交，把「哪些问题值得处理」写进一个状态文件。\n第三步，对每一个值得做的问题，开一个隔离的 worktree，派一个 sub-agent 进去起草修复。\n第四步，第二个 sub-agent 登场，对照项目的 skill 规范和现有测试，把那份草稿审一遍。\n第五步，审过了，connector 自动开 PR、更新对应的工单。\n第六步，loop 搞不定的问题，不硬来，丢进一个待办收件箱，等真人来看。\n第七步，所有经过都写回状态文件。明天早上的循环从今天停下的地方继续。\n📷 [图片 token=Nx7rbqJBwo4FJhxNFp6cI4MdnUY（未能下载，见飞书原文）]\n📷 [图片 token=DcsEbCd1xohgOcxSUvUcdwF5nPB（未能下载，见飞书原文）]\n流程走完，你品一品这里面最关键的一件事：整个过程里，你只设计了一次，中间的任何一步，你都没有写过 prompt。\n这就是 Steinberger 那句口号的落地版本。你的活从「每一步都出现」变成了「只在两个地方出现」：设计循环的时候，和收件箱里有东西的时候。\n📷 [图片 token=AneTb7SyKo8eoRxsLRQcV0sBnzh（未能下载，见飞书原文）]\n一个好 loop 的标志：你只在设计时出现一次，之后只在收件箱前出现。\nQ5：工具已经追上来了，现在就能搭 听到这里你可能会想：道理是好道理，但搭这么一套系统，工程量不小吧？\n这正是这次概念能火起来的底气所在。这里有一个很关键的时间差：一年前，搭一个 loop 意味着写一堆只有你自己看得懂、还得永远自己维护的 bash 脚本；而现在，五大件全部内置在主流产品里。\n我把两家头部产品的部件整理成一张对照表：\nFqZXaV 部件 在 loop 里的职责 Codex Claude Code 自动化 定时发现和分诊 Automations 面板 + 分诊收件箱 计划任务、/loop、hooks worktree 隔离并行任务 每个线程内置 worktree git worktree、隔离配置 skill 固化项目知识 Agent Skills Agent Skills connector 连接外部工具 基于 MCP 的 Connectors MCP servers sub-agent 分头干活、写查分离 配置文件定义子 agent 子 agent、agent teams 记忆 追踪进度 markdown 或接工单系统 markdown 或接工单系统 📷 [图片 token=SUJObNk7koqBPOxbgqSceSAOnmu（未能下载，见飞书原文）]\n这张表里还藏着一个值得单独拎出来的细节：两家都有一个 /goal 类的能力，它和普通定时循环的区别很微妙，但很重要。\n普通的循环是按节奏重复跑：每小时跑一次，跑完就完了，对不对另说。而 /goal 是跑到你写的条件为真才停，比如「目录下所有测试通过且 lint 干净」。更妙的是，每一轮结束后，由一个独立的模型来判断条件是否达成。\n发现没有？这就是上一节说的「写的人和查的人分开」，只不过这次用在了「什么时候算干完了」这个停止条件上。连「我做完了」这句话，都不让干活的那个 agent 自己说。\n📷 [图片 token=FyQzbQQ7JoGv65xNJaHcPXZ9nNh（未能下载，见飞书原文）]\n实操：30 秒搭出你的第一个 loop 光说不练假把式，我们拿一个所有人都烦过的场景，真刀真枪走一遍。\n这个场景是：你提了个 PR，然后开始等 CI。挂了，切回去看日志、改、推送，再等。一下午切了八次窗口，正经活没干多少。\n在 Claude Code 里，这件事用一条 /loop 命令就能交出去：\n/loop 10m 检查当前分支 PR 的 CI 状态：有失败的检查就读日志、修复、推送；\n全部变绿后停下来，给我一句话总结改了什么\n把这条命令拆开看，麻雀虽小，loop 的骨架是全的。\n10m 是心跳：每 10 分钟自动醒来跑一轮，不用你按回车。中间那段是任务：每一轮干什么。最后一句是停止条件加汇报：什么时候算完、完了怎么交差。\n敲下去之后你就可以去干别的了。CI 挂了它自己修，修完自己推，全绿了它叫你。刚才那个来回切窗口的下午，被压缩成了「最后看一眼总结」。\n📷 [图片 token=ZaTKbM2wZoziKPxl3U7ccuTBnIe（未能下载，见飞书原文）]\n还有个更省心的玩法：把间隔省掉，直接 /loop 加任务。这时候节奏由模型自己定，它会根据「CI 一般要跑多久」来决定多久看一次，不会傻乎乎地一分钟刷一次。\n两个使用边界也交代清楚，免得你回头骂我。\n第一，/loop 活在当前会话里，适合「今天盯着这件事」的轮询；你关掉电脑它就停了。想要那种睡觉时也在跑的 loop，要用计划任务或者云端的 routines，让它脱离你的机器运行。\n第二，回头对照 Q3 的五大件你会发现，这个最小 loop 只有心跳、任务和停止条件，没有 worktree、没有写查分离的 sub-agent。这不是缺陷，是起点：先让最小的循环转起来，哪天你觉得「它自己改的代码我不放心」，再把检查的 sub-agent 加上；觉得「想同时盯三个 PR」，再上 worktree。部件是一件一件长出来的，不是一天配齐的。\n📷 [图片 token=CkFEbvzr6o7P9Fx8k9nci7JQn7f（未能下载，见飞书原文）]\n而对照表还说明了一件更大的事：两家产品的部件几乎一一对应，loop 的设计正在变得工具无关。部件是同样的部件，差的只是商标。\n这意味着什么？意味着「选 Codex 还是选 Claude Code」这种争论的重要性在下降。loop 的设计图纸是你的资产，画好了，放在哪家的产品上都能转。值得积累的是图纸，不是对某家工具的肌肉记忆。\n📷 [图片 token=TyCWbmyuhoPilWxqvOmc608Hnbh（未能下载，见飞书原文）]\n门槛已经从「自己造零件」降到了「学会拼装」，剩下的问题只是你想让 loop 替你做什么。\nQ6：三盆冷水：loop 越好用，这三个问题越尖锐 文章到这里都挺振奋的，该泼冷水了。\n有意思的是，泼得最狠的不是哪个反对派，恰恰是给概念定名的 Osmani 本人，他在博客里直说：「现在还早，我是持怀疑态度的。」\n冷水的核心是一句话：loop 改变了工作，但没有把你从工作中删除。而且有三个问题，会随着 loop 越来越好用，变得越来越尖锐，而不是越来越轻松。\n第一盆：验证仍然归你 loop 无人值守地运行，听起来很美。但换个角度念这句话：一个无人值守运行的 loop，也是一个无人值守犯错的 loop。\n你睡觉时它在干活，也意味着你睡觉时它在犯错。\n就算你按规矩配了负责检查的 sub-agent，也别高兴太早：检查 agent 嘴里的「done」，只是一个声明，不是一个证明。它说没问题，和真的没问题，中间还隔着你的眼睛。\n📷 [图片 token=PwHMbyMJ1oltI5x9aUQcWWGwnhf（未能下载，见飞书原文）]\n第二盆：理解债，越顺滑涨得越快 第二个问题更隐蔽。loop 交付你没写过的代码越快，「仓库里实际存在的东西」和「你脑子里真正理解的东西」之间的鸿沟就越大。\n这有个专门的名字，叫理解债（Comprehension Debt）。\n它和技术债不一样：技术债是代码烂，理解债是代码可能不烂，但你不知道它为什么是对的。出问题的那天，你面对的是一片自己「拥有」但不「理解」的代码。一个顺滑的 loop 不会帮你还这笔债，只会让它涨得更快，除非你坚持去读 loop 产出的东西。\n📷 [图片 token=GFrrb5aChoe0dyxSjUfctmYEnNh（未能下载，见飞书原文）]\n第三盆：认知投降，最舒服的姿势最危险 第三个问题最扎心。loop 自己转起来之后，你会发现一个特别舒服的姿势：不再对产出有自己的观点，它给什么就收什么。\n这个状态有个专门的名字，叫认知投降（Cognitive Surrender）。Osmani 的博客里有一段关于它的原话，非常锋利：\n「带着判断力去设计 loop，它是解药；为了逃避思考去设计 loop，它是助燃剂。同一个动作，相反的结果。」\n📷 [图片 token=GImQbyMz3ogbYhxS46QcO1Yanfb（未能下载，见飞书原文）]\n最后还有一笔很现实的账：token 成本。loop 是按循环烧 token 的，sub-agent 每多一个，就多一份模型和工具的开销。比较务实的花法，是把 sub-agent 用在「值得买第二意见」的地方，而不是处处双保险。token 富裕和精打细算这两种人，会设计出两种完全不同的 loop。\n这三盆冷水有个共同点：它们都不是 loop 的 bug，而是 loop 的代价，并且由你来付。\n最后 把整篇浓缩成 3 句话送你：\n第一，从 prompt 到 context 到 harness 再到 loop，四个词是一场瓶颈迁移：模型越强，瓶颈越往外移，最后移到了「亲自按回车的你」身上。\n第二，一个 loop 等于五大件加一份磁盘记忆：自动化是心跳，worktree 防打架，skill 治金鱼记忆，connector 摸到真实世界，sub-agent 写查分离，状态文件让明天接得上今天。\n第三，loop 把你的杠杆变长了，但验证、理解债、认知投降这三笔账也同时变大了，工具分不出你是在加速还是在逃避，你自己分得出。\nOsmani 文章的结尾有一段话，我觉得是整场讨论里最值得带走的：\n「两个人可以搭一模一样的 loop，得到完全相反的结果。一个用它在自己深刻理解的工作上加速，另一个用它彻底逃避理解工作。loop 分不出区别，你分得出。」\n参考资料 Peter Steinberger 的原帖：https://x.com/steipete/status/2063697162748260627\nAddy Osmani《Loop Engineering》：https://addyosmani.com/blog/loop-engineering\nBoris Cherny 访谈《Claude Code \u0026amp; the Future of Engineering》\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AFLoop%20Engineering%EF%BC%9F/","summary":"!NOTE  这篇属于项目之外的读物，不学也不影响学习后续的 oncall agent项目的学习。  目的是帮助大家理解「Loop 工程」 大家好，我是小林。 不知道你有没有跟我一样的感觉：AI 圈造新词的速度，已经超过我学习的速度了。 前","title":"什么是Loop Engineering？"},{"content":"对话Agent的核心需求是 知识检索+工具调用，比如：先问时间，然后问告警，再问错误码原因：ReAct会先思考需要调用什么工具 ，再行动调用对应的工具，最终生成答案。这种思考-\u0026gt;行动的模式就是ReAct，ReAct是对话Agent的核心技术支柱。\n📷 [图片 token=VovJbMlekoaAr9x11oOcZLFYnFc（未能下载，见飞书原文）]\nReAct是什么？ 先给定义：ReAct 是 AI 的解题方法论\nReAct = Reasoning（推理）+ Acting（行动），核心是让 AI 像人一样 ** 边想边做、边做边调整 **，通过 ** 思考-\u0026gt;行动-\u0026gt;观察-\u0026gt;再思考 **来解决问题。\n📷 [图片 token=JiOwbY24OoqxraxcVi2cLDUpn8e（未能下载，见飞书原文）]\n**大白话解释：就像你做饭时查菜谱 ** 想象你要做一道没做过的菜（比如 红烧肉 ），你会怎么一步步来？\n思考： 我需要知道红烧肉的步骤、调料、火候…… （分析问题，明确要做什么）\n行动：打开手机查抖音红烧肉教程（调用 抖音工具 获取信息）\n观察：看到教程说 需要炒糖色，用冰糖 （获取工具返回的结果）\n再思考： 家里只有白糖，能用吗？再查一下‘白糖代替冰糖炒糖色’ （根据观察结果调整思路，继续行动）\nReAct 就是让 AI 模仿这个过程：遇到问题不直接 瞎猜答案 ，而是先想 该查什么 ，再调用工具（比如计算器、数据库、搜索引擎），拿到结果后判断 够不够 ，不够就继续查，直到能给出最终答案。\n技术场景的例子： 地球和火星的质量加起来是多少？ （AI 不知道具体数值，需要调用工具） 第1次循环：思考-\u0026gt;行动-\u0026gt;观察\n思考：我需要地球和火星的质量，得先查地球质量。 行动：调用 查星球质量 工具，输入 地球 观察：工具返回 地球质量≈5.97×10²⁴ kg 第2次循环：再思考-\u0026gt;行动-\u0026gt;观察\n思考：有了地球质量，还缺火星的，继续查火星。 行动：调用 查星球质量 工具，输入 火星 观察：工具返回 火星质量≈6.42×10²³ kg 第3次循环：再思考-\u0026gt;行动-\u0026gt;观察\n思考：现在有两个数了，需要相加，得用计算器。 行动：调用 计算器 工具，输入 5.97e24 + 6.42e23 观察：工具返回 ≈6.61×10²⁴ kg 结束循环： 结果够了，可以回答用户了，地球和火星的质量加起来是6.61×10²⁴ kg。\n知道了ReAct的原理，接下来我们来看看到底怎么实现ReAct模式\n古法 ReAct（手搓字符串解析） 通过严格的 prompt 规范 AI 的输出格式，强制其按 ** Thought-\u0026gt;Action-\u0026gt;Pause-\u0026gt;Observation ** 流程执行\nsystem prompt 我们定义下面这段 system prompt。那么大模型将会严格依据prompt里面的要求返回。\n# 中文 system_prompt = \u0026#34;\u0026#34;\u0026#34; 你是一个反应（推理和行动）代理，遵循思考、行动、暂停和观察的循环来解决问题。 Workflow: 1. Thought: 推理下一步做什么 2. Action: 调用工具（格式：Action:工具名:输入） 3. PAUSE: 等待工具返回 4. Observation: 工具结果分析 可用工具： - calculation: 数学计算（如 \u0026#34;5*7/4\u0026#34;） - planet_mass: 查询行星质量（如 \u0026#34;Mars\u0026#34;） \u0026#34;\u0026#34;\u0026#34; system_prompt = \u0026#34;\u0026#34;\u0026#34; You are a React(Reasoning and Acting) agent that follows a loop of Thought, Action, PAUSE, and Observation to solve problems. Workflow: 1. Thought: Describe your reasoning or plan for solving the problem. 2. Action: Execute an appropriate action based on your reasoning. The available actions are listed below. 3. PAUSE: Indicate that you are pushing to observe the result of the action. Stop output anything while pause. 4. Observation: Analyze the result of the action and incorporate it into your reasoning. At the end of the loop, provide a final Answer based on the information gathered. Your available actions are: calculation: e.g. calculation: 5*7/4 run a calculation and return the number using python so be sure to use floating point syntax if needed. planet_mass: e.g. planet_mass: Mars returns the mass of the planet in the solar system \u0026#34;\u0026#34;\u0026#34; 代码执行流程 我们根据大模型的消息，对消息进行字符串解析：\n# 伪代码 def query(question): while 循环（最多 N 轮）: 1. 发送 prompt + 问题 + 历史记录 给 AI print(\u0026#34;----------------------\u0026#34;) print(f\u0026#34;step: {i}\u0026#34;) print(result) 2. 解析 AI 返回：用正则匹配 Action（如 `Action:calculation:5*7`） 3. 调用工具函数，获取结果 -\u0026gt; 生成 Observation print(f\u0026#34;\\nObservation: {observation}\u0026#34;) 4. 将 Observation 加入新 prompt，进入下一轮循环 5. 若 AI 无 Action，返回最终答案 大模型执行日志 Question: What is the mass of Earth plus Mars? ---这是我们的提问 ---------------------- step:1 --- 第一次循环 Thought: I need to find the masses of Earth and Mars, then add them. --- 大模型第一次思考 Action: planet_mass: Earth ---大模型第一次行动 PAUSE # ---这是我们代码里解析出planet_mass函数与输入Earth，执行函数后返回的内容 Observation: Earth has a mass of 5.972 × 10^24 kg ---------------------- step:2 --- 第二次循环 Thought: Now I have Earth\u0026#39;s mass. I need Mars\u0026#39;s mass. ---大模型第二次思考 Action: planet_mass: Mars ---大模型第二次行动 PAUSE # ---这是我们代码里解析出planet_mass函数与输入Mars，执行函数后返回的内容 Observation: Mars has a mass of 6.4171 × 10^23 kg ---------------------- step:3 --- 第三次循环 Thought: Now I can calculate the sum. ---大模型第三次思考 Action: calculate: 5.972e24 + 6.4171e23 ---大模型第三次行动 PAUSE # ---这是我们代码里解析出calculate函数与输入5.972e24 , 6.4171e23，执行函数后返回的内容 Observation: 5.972e24 + 6.4171e23 = 6.614e24 ---------------------- step:4 --- 第四次循环 Answer: The combined mass is approximately 6.614 × 10^24 kg. --- 返回最终答案 总结 所谓的行动，其实是我们根据大模型的输出，解析出应该调用哪个工具（函数），然后代码执行。并将结果告诉大模型。至此ReAct不再神秘，说白了就是字符串的处理。\n这种写法是最原始的，举个例子，如果函数的输入输出的map，是map套map，那字符串生成/解析的难度指数级上升。大模型处理这种场景就比较困难，很容易出错。\n现代 ReAct（Function Call） 标准化工具交互 大模型厂商推出 Function Call，用 JSON 统一工具定义和调用格式：\n工具描述：用 JSON 定义工具名、参数、功能（如 {\u0026quot;name\u0026quot;:\u0026quot;calculation\u0026quot;, \u0026quot;parameters\u0026quot;:{\u0026quot;type\u0026quot;:\u0026quot;number\u0026quot;}}）\n调用格式：AI 直接返回 JSON（如 {\u0026quot;action\u0026quot;:\u0026quot;calculation\u0026quot;, \u0026quot;input\u0026quot;:5}），避免字符串解析混乱。\n优点：工具描述和回复格式统一，便于针对性训练AI模型，若AI回复错误，服务器端可检测并自动重试，降低用户端开发难度和token开销。\n现代ReAct执行流程 将工具信息按照function call要求的格式和用户的输入，一起发送给大模型（这里就不需要像原始的ReAct要求写Action之类的prompt了）\n大模型有了上面的tool信息，会自行判断是否需要使用工具，如果要使用，则tool call（模型会按照function call要求的格式生成内容，指定函数名和参数）。\n将tool的响应添加到输入中，开始新的一轮循环，直到大模型不再tool call，那么就结束了\n不论是古法ReAct还是现代ReAct，核心思想都是一样的，根据工具返回来决定下一步要做什么\n这里可能有一点抽象，没关系。知道有这个循环流程就行，下一节就有ReAct代码实战。\n📷 [图片 token=KMRJbPiDUoVkftxTBdDcSzPLnid（未能下载，见飞书原文）]\n古法与现代的对比 核心差异在于从人工字符串约定升级为机器可理解的结构化协议 ，大幅降低了格式依赖和实现成本。\n对比维度 古法 ReAct 现代 ReAct 工具调用格式 自然语言字符串（如 Action: planet_mass: Mars） 标准化 JSON 结构（通过 Function Call 规范） 工具描述方式 依赖用户自定义 Prompt 中的自然语言说明 工具信息标准化（如 JSON 对象定义工具名、参数、功能） 解析方式 正则表达式解析字符串（易出错，依赖格式严格匹配） 结构化 JSON 解析（大模型/框架原生支持，可靠性高） 复杂数据处理 困难（如嵌套结构的输入输出，字符串解析易混乱） 可靠（JSON 天然支持复杂参数类型，如嵌套对象） 错误处理 需手动实现（如未知 Action 抛出异常） 框架自动支持（如工具调用格式错误时，大模型/框架自动重试） 依赖技术 纯 Prompt 工程 + 字符串处理 大模型 Function Call 功能（厂商官方支持） 格式规范来源 用户自定义 Prompt 中的格式约束 大模型厂商定义的标准化协议 实现复杂度 高（需手动处理字符串生成、解析、异常捕获） 低（框架封装了格式处理、工具调用逻辑） 总结：ReAct 的本质 ReAct = Reasoning（推理）+ Acting（行动），先想后做：拆解问题-\u0026gt;制定计划。边做边说：调用工具（搜索引擎/数据库）-\u0026gt;观察结果-\u0026gt;反复验证，核心是** 思考 -\u0026gt; 行动 -\u0026gt; 观察 -\u0026gt; 再思考 **的闭环：\n思考：分析问题、执行计划，决定下一步该做什么\n行动：根据思考，去调用外部工具来获取信息\n观察：查询看行动（工具/函数）的返回结果\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E6%97%A7%E6%96%87%E6%A1%A3%E5%A4%87%E4%BB%BD/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AReAct%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/","summary":"对话Agent的核心需求是 知识检索+工具调用，比如：先问时间，然后问告警，再问错误码原因：ReAct会 先思考 需要调用什么工具 ，再 行动 调用对应的工具，最终生成答案。这种思考-\\ 行动的模式就是ReAct，ReAct是对话Agent","title":"架构设计：ReAct设计模式核心原理"},{"content":" 📷 [图片 token=H9UUbjpbyoOKQUx6KOIcsBzGnVf（未能下载，见飞书原文）]\n📷 [图片 token=Vmz8bkS8Po57qvxcfF5cezr6nRh（未能下载，见飞书原文）]\n前言 上一节我们实现了知识库Agent的上半部分，这一节我们来实现知识库的召回功能。\n核心代码分布在：app/services/vector_search_service.py — 使用原生 PyMilvus 执行向量检索\n📷 [图片 token=E8TWbe4qpoDMMXxKZdNcVvlJnRd（未能下载，见飞书原文）]\n召回 我们之前已经将文档向量化存储到了 Milvus，所以召回时也从这个数据库去查询。流程分两步：\n将查询文本向量化\n相似度查询\nsearch_similar_documents 是底层召回的完整实现：\ndef search_similar_documents(self, query: str, top_k: int = 3) -\u0026gt; List[SearchResult]: \u0026#34;\u0026#34;\u0026#34; 搜索相似文档 Args: query: 查询文本 top_k: 返回最相似的K个结果 Returns: List[SearchResult]: 搜索结果列表 \u0026#34;\u0026#34;\u0026#34; logger.info(f\u0026#34;开始搜索相似文档, 查询: {query}, topK: {top_k}\u0026#34;) # 1. 将查询文本向量化 query_vector = vector_embedding_service.embed_query(query) logger.debug(f\u0026#34;查询向量生成成功, 维度: {len(query_vector)}\u0026#34;) # 2. 获取 collection collection: Collection = milvus_manager.get_collection() # 3. 构建搜索参数 search_params = { \u0026#34;metric_type\u0026#34;: \u0026#34;L2\u0026#34;, # 欧氏距离，与入库时的索引类型保持一致 \u0026#34;params\u0026#34;: {\u0026#34;nprobe\u0026#34;: 10}, } # 4. 执行搜索 results = collection.search( data=[query_vector], anns_field=\u0026#34;vector\u0026#34;, param=search_params, limit=top_k, output_fields=[\u0026#34;id\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;metadata\u0026#34;], ) # 5. 解析搜索结果 search_results = [] for hits in results: for hit in hits: result = SearchResult( id=hit.entity.get(\u0026#34;id\u0026#34;), content=hit.entity.get(\u0026#34;content\u0026#34;), score=hit.distance, # L2 距离，越小越相似 metadata=hit.entity.get(\u0026#34;metadata\u0026#34;, {}), ) search_results.append(result) logger.info(f\u0026#34;搜索完成, 找到 {len(search_results)} 个相似文档\u0026#34;) return search_results 查询文本向量化 首先对用户问题进行向量化，调用 DashScopeEmbeddings.embed_query，通过 DashScope OpenAI 兼容接口获取 1024 维向量：\n# 1. 将查询文本向量化 query_vector = vector_embedding_service.embed_query(query) embed_query 的实现（复用入库时相同的 API，无额外开销）：\ndef embed_query(self, text: str) -\u0026gt; List[float]: \u0026#34;\u0026#34;\u0026#34;嵌入单个查询文本，返回单条向量\u0026#34;\u0026#34;\u0026#34; response = self.client.embeddings.create( model=self.model, # text-embedding-v4 input=text, dimensions=self.dimensions, # 1024 encoding_format=\u0026#34;float\u0026#34; ) return response.data[0].embedding 构建搜索参数并执行向量检索 然后使用 PyMilvus 的 collection.search 进行相似度查询，获取距离最近的向量数据：\n# 构建搜索参数 search_params = { \u0026#34;metric_type\u0026#34;: \u0026#34;L2\u0026#34;, # 欧氏距离 \u0026#34;params\u0026#34;: {\u0026#34;nprobe\u0026#34;: 10}, # 搜索时探测的 cluster 数量，越大越精确但越慢 } # 执行搜索 results = collection.search( data=[query_vector], # 查询向量（批量，这里只有一条） anns_field=\u0026#34;vector\u0026#34;, # 向量字段名，与入库时一致 param=search_params, limit=top_k, # 返回最相似的前 K 条 output_fields=[\u0026#34;id\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;metadata\u0026#34;], # 需要返回的字段 ) 搜索结果封装到 SearchResult 对象中，score 为 L2 欧氏距离，越小表示越相似：\nclass SearchResult: def __init__(self, id: str, content: str, score: float, metadata: Dict[str, Any]): self.id = id self.content = content self.score = score # L2 距离，越小越相似 self.metadata = metadata 总结 至此，RAG 的分片、索引、召回功能都已实现完毕。后续会介绍 RAG Agent 是怎么使用知识库、怎么结合召回来与大模型进行交互的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9ARAG%E5%8F%AC%E5%9B%9E%E5%AE%9E%E6%88%982%28Python%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;H9UUbjpbyoOKQUx6KOIcsBzGnVf\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：RAG召回实战2(Python)"},{"content":" 📷 [图片 token=KqB8bTZAqoQntrxQzr3cQFK4nFh（未能下载，见飞书原文）]\n📷 [图片 token=R4MBb04Iho5RC2x7rEkcjPCunTh（未能下载，见飞书原文）]\n前言 这部分代码在：app/api/chat.py 和 app/services/rag_agent_service.py\n📷 [图片 token=EMw8bVgCfo43cUxs8x1cTAmUnJh（未能下载，见飞书原文）]\n流程梳理 对话 Agent 的核心目标是结合外部知识（RAG 召回）与工具调用能力（ReAct 模式），解决复杂问题。\n整体流程可概括为：\n用户输入 → embedding → 向量数据库召回\n将召回内容作为上下文注入 prompt\nLangGraph ReAct 模式多轮工具调用\n流式输出最终答案\n实战 消息召回 召回通过 retrieve_knowledge 工具实现，Agent 在推理时会自动判断是否需要调用该工具检索知识库。工具内部通过 VectorStoreManager.similarity_search 完成向量检索，详见 RAG 召回章节。\n# retrieve_knowledge 工具挂载到 Agent 上 self.tools = [retrieve_knowledge, get_current_time] 构建 prompt 系统提示词在 _build_system_prompt 中构建，描述 Agent 的角色定位和行为准则。与工具列表无关——LangChain 框架会自动将工具信息传递给大模型，prompt 中无需手动列举：\ndef _build_system_prompt(self) -\u0026gt; str: from textwrap import dedent return dedent(\u0026#34;\u0026#34;\u0026#34; 你是一个专业的AI助手，能够使用多种工具来帮助用户解决问题。 工作原则: 1. 理解用户需求，选择合适的工具来完成任务 2. 当需要获取实时信息或专业知识时，主动使用相关工具 3. 基于工具返回的结果提供准确、专业的回答 4. 如果工具无法提供足够信息，请诚实地告知用户 回答要求: - 保持友好、专业的语气 - 回答简洁明了，重点突出 - 基于事实，不编造信息 - 如有不确定的地方，明确说明 请根据用户的问题，灵活使用可用工具，提供高质量的帮助。 \u0026#34;\u0026#34;\u0026#34;).strip() 会话历史由 LangGraph 的 MemorySaver checkpointer 自动管理，每次调用时传入相同的 thread_id（即 session_id）即可自动携带上下文，无需手动拼接历史消息到 prompt。\n创建 ReAct Agent 使用 LangChain 的 create_agent 创建 Agent，绑定 ChatQwen 模型、工具列表和 MemorySaver 检查点。MCP 工具（腾讯云 CLS 日志、监控告警等）在首次请求时异步加载，与本地工具合并后一起绑定：\nclass RagAgentService: def __init__(self, streaming: bool = True): self.model = ChatQwen( model=config.rag_model, # 默认 qwen-max api_key=config.dashscope_api_key, temperature=0.7, streaming=streaming, ) # 本地工具：RAG 知识检索 + 时间查询 self.tools = [retrieve_knowledge, get_current_time] # 会话持久化（基于内存的 checkpointer） self.checkpointer = MemorySaver() self.agent = None # 延迟初始化（等待 MCP 工具加载完成） async def _initialize_agent(self): \u0026#34;\u0026#34;\u0026#34;异步初始化 Agent（包括 MCP 工具）\u0026#34;\u0026#34;\u0026#34; if self._agent_initialized: return # 加载 MCP 工具（CLS 日志服务 + 监控告警） mcp_client = await get_mcp_client_with_retry() mcp_tools = await mcp_client.get_tools() # 合并所有工具 all_tools = self.tools + mcp_tools self.agent = create_agent( self.model, tools=all_tools, checkpointer=self.checkpointer, ) self._agent_initialized = True 执行 ReAct Agent 非流式调用 调用 agent.ainvoke，等待 Agent 完成全部推理和工具调用后一次性返回结果：\nasync def query(self, question: str, session_id: str) -\u0026gt; str: await self._initialize_agent() messages = [ SystemMessage(content=self.system_prompt), HumanMessage(content=question) ] result = await self.agent.ainvoke( input={\u0026#34;messages\u0026#34;: messages}, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, ) # 取最后一条消息作为最终答案 last_message = result[\u0026#34;messages\u0026#34;][-1] return last_message.content 流式调用 调用 agent.astream，使用 stream_mode=\u0026quot;messages\u0026quot; 逐 token 输出，配合 FastAPI 的 SSE 接口实时推送给前端：\nasync def query_stream(self, question: str, session_id: str) -\u0026gt; AsyncGenerator: await self._initialize_agent() messages = [ SystemMessage(content=self.system_prompt), HumanMessage(content=question) ] async for token, metadata in self.agent.astream( input={\u0026#34;messages\u0026#34;: messages}, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, stream_mode=\u0026#34;messages\u0026#34;, ): if type(token).__name__ in (\u0026#34;AIMessage\u0026#34;, \u0026#34;AIMessageChunk\u0026#34;): content_blocks = getattr(token, \u0026#39;content_blocks\u0026#39;, None) if content_blocks: for block in content_blocks: if isinstance(block, dict) and block.get(\u0026#39;type\u0026#39;) == \u0026#39;text\u0026#39;: text = block.get(\u0026#39;text\u0026#39;, \u0026#39;\u0026#39;) if text: yield {\u0026#34;type\u0026#34;: \u0026#34;content\u0026#34;, \u0026#34;data\u0026#34;: text} yield {\u0026#34;type\u0026#34;: \u0026#34;complete\u0026#34;} SSE 接口层 chat_stream 接口将 query_stream 产生的事件包装成 SSE 格式推送给客户端，不同类型的事件对应不同的前端展示逻辑：\n@router.post(\u0026#34;/chat_stream\u0026#34;) async def chat_stream(request: ChatRequest): async def event_generator(): async for chunk in rag_agent_service.query_stream(request.question, session_id=request.id): chunk_type = chunk.get(\u0026#34;type\u0026#34;) if chunk_type == \u0026#34;content\u0026#34;: # 逐 token 文本内容 yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({\u0026#34;type\u0026#34;: \u0026#34;content\u0026#34;, \u0026#34;data\u0026#34;: chunk[\u0026#34;data\u0026#34;]}, ensure_ascii=False) } elif chunk_type == \u0026#34;tool_call\u0026#34;: # 工具调用状态（前端可展示\u0026#34;正在检索知识库...\u0026#34;等提示） yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({\u0026#34;type\u0026#34;: \u0026#34;tool_call\u0026#34;, \u0026#34;data\u0026#34;: chunk[\u0026#34;data\u0026#34;]}, ensure_ascii=False) } elif chunk_type == \u0026#34;complete\u0026#34;: # 完成信号 yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({\u0026#34;type\u0026#34;: \u0026#34;done\u0026#34;, \u0026#34;data\u0026#34;: chunk.get(\u0026#34;data\u0026#34;)}, ensure_ascii=False) } elif chunk_type == \u0026#34;error\u0026#34;: yield { \u0026#34;event\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;data\u0026#34;: json.dumps({\u0026#34;type\u0026#34;: \u0026#34;error\u0026#34;, \u0026#34;data\u0026#34;: str(chunk[\u0026#34;data\u0026#34;])}, ensure_ascii=False) } return EventSourceResponse(event_generator()) curl 调用示例：\n# 非流式对话 curl -X POST http://localhost:8000/api/chat \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;question\u0026#34;: \u0026#34;CPU 使用率过高怎么排查？\u0026#34;}\u0026#39; # 流式对话（SSE） curl -X POST http://localhost:8000/api/chat_stream \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;id\u0026#34;: \u0026#34;session-001\u0026#34;, \u0026#34;question\u0026#34;: \u0026#34;CPU 使用率过高怎么排查？\u0026#34;}\u0026#39; 总结 至此，对话 Agent 的核心流程——RAG 召回与 ReAct 模式的代码就讲完了。框架帮我们做了很多事情：LangChain 负责工具绑定与调用，LangGraph 负责多轮推理的状态流转，MemorySaver 负责会话历史管理。核心是要搞懂设计原理：RAG 补充外部知识，ReAct 让模型具备多步骤工具调用能力。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%9B%9B%E7%AB%A0%EF%BD%9C%E5%AF%B9%E8%AF%9DAgent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E5%AF%B9%E8%AF%9DAgent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Python%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;KqB8bTZAqoQntrxQzr3cQFK4nFh\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2070\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"源码分析：对话Agent代码实现(Python)"},{"content":" 📷 [图片 token=Wy2CbrxSOoMSeLxMPYGc9co4nSd（未能下载，见飞书原文）]\n前言 关键代码：app/agent/aiops/ 目录下的 state.py、planner.py、executor.py、replanner.py，以及 app/services/aiops_service.py。\n📷 [图片 token=BxHebByEioqLYQxFy82chBk2nnh（未能下载，见飞书原文）]\n流程梳理 运维 Agent 的核心目标是 规划 → 执行 → 评估 → 调整。整体流程就是三个节点：\nPlanner：拆解排查步骤，生成执行计划\nExecutor：从计划中取出第一个步骤，调用工具执行\nReplanner：评估执行结果，决定继续、调整计划还是生成最终报告\n三个节点通过 LangGraph StateGraph 串联，共享同一份 PlanExecuteState 状态对象在整个流程中传递。\n📷 [图片 token=S0bFbHxhmoplyuxMjrJcPpXhn0d（未能下载，见飞书原文）]\n实战 状态定义 整个 Plan-Execute-Replan 流程的数据流通过 PlanExecuteState 承载，字段设计非常简洁：\nclass PlanExecuteState(TypedDict): input: str # 用户输入的任务描述 plan: List[str] # 待执行的步骤列表 past_steps: Annotated[List[tuple], operator.add] # 已执行的步骤历史（追加式更新） response: str # 最终报告/响应 past_steps 使用 Annotated[List[tuple], operator.add] 声明，LangGraph 会将每次节点返回的 past_steps 自动追加到列表中，而不是覆盖，无需手动维护历史。\nPlanner 节点 Planner 负责制定执行计划，输出一个结构化的步骤列表。核心流程：\n先调用 retrieve_knowledge 查询知识库，寻找历史经验文档\n获取所有可用工具（本地工具 + MCP 工具），格式化为文字描述\n将工具列表和经验文档注入 prompt，调用 LLM 生成结构化计划\nasync def planner(state: PlanExecuteState) -\u0026gt; Dict[str, Any]: input_text = state.get(\u0026#34;input\u0026#34;, \u0026#34;\u0026#34;) # 1. 查询内部知识库，寻找相关经验 context_str = await retrieve_knowledge.ainvoke({\u0026#34;query\u0026#34;: input_text}) experience_docs = context_str if context_str and context_str.strip() else \u0026#34;\u0026#34; # 2. 获取所有可用工具（本地 + MCP） local_tools = [get_current_time, retrieve_knowledge] mcp_tools = await (await get_mcp_client_with_retry()).get_tools() all_tools = local_tools + mcp_tools # 3. 调用 LLM 生成结构化计划 llm = ChatQwen(model=config.rag_model, api_key=config.dashscope_api_key, temperature=0) planner_chain = planner_prompt | llm.with_structured_output(Plan) plan_result = await planner_chain.ainvoke({ \u0026#34;messages\u0026#34;: [(\u0026#34;user\u0026#34;, input_text)], \u0026#34;tools_description\u0026#34;: format_tools_description(all_tools), \u0026#34;experience_context\u0026#34;: experience_docs }) return {\u0026#34;plan\u0026#34;: plan_result.steps} 计划的输出格式用 Pydantic Plan 模型约束，通过 llm.with_structured_output(Plan) 保证 LLM 的输出可以直接解析为步骤列表：\nclass Plan(BaseModel): steps: List[str] = Field( description=\u0026#34;完成任务所需的不同步骤，按顺序执行，每一步建立在前一步的基础上。\u0026#34; ) Planner Prompt Planner 的系统提示词要求模型将任务分解为逻辑独立的步骤，每步指明使用哪个工具及所需参数。如果查到了经验文档，也会作为参考注入：\nplanner_prompt = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;\u0026#34;\u0026#34; 作为一个专家级别的规划者，你需要将复杂的任务分解为可执行的步骤。 可用工具列表（用于制定计划时参考）： {tools_description} 注意：你的职责是制定计划，实际的工具调用由 Executor 负责执行。 {experience_context} 对于给定的任务，请创建一个简单的、逐步的计划： - 将任务分解为逻辑上独立的步骤 - 每个步骤明确使用哪些工具（如果需要），最好能同时提供工具所需参数 - 步骤之间应有清晰的依赖关系 - 如果有相关经验文档，请参考其中的方法和步骤制定计划 \u0026#34;\u0026#34;\u0026#34;), (\u0026#34;placeholder\u0026#34;, \u0026#34;{messages}\u0026#34;), ]) Executor 节点 Executor 每次只执行计划中的第一个步骤，使用 LangGraph 的 ToolNode 自动处理工具调用，执行完后将该步骤从 plan 中移除，并将执行结果追加到 past_steps：\nasync def executor(state: PlanExecuteState) -\u0026gt; Dict[str, Any]: plan = state.get(\u0026#34;plan\u0026#34;, []) task = plan[0] # 只取第一个步骤 # 绑定工具的 LLM all_tools = local_tools + mcp_tools llm_with_tools = llm.bind_tools(all_tools) tool_node = ToolNode(all_tools) messages = [ SystemMessage(content=\u0026#34;你是一个能力强大的助手，负责执行具体的任务步骤。...\u0026#34;), HumanMessage(content=f\u0026#34;请执行以下任务: {task}\u0026#34;) ] # 第一步：LLM 决定是否需要工具调用 llm_response = await llm_with_tools.ainvoke(messages) # 第二步：如果有工具调用，使用 ToolNode 自动执行 if hasattr(llm_response, \u0026#34;tool_calls\u0026#34;) and llm_response.tool_calls: messages.append(llm_response) tool_messages = await tool_node.ainvoke({\u0026#34;messages\u0026#34;: messages}) messages.extend(tool_messages[\u0026#34;messages\u0026#34;]) # 第三步：将工具结果返回给 LLM 生成最终答案 final_response = await llm_with_tools.ainvoke(messages) result = final_response.content else: result = llm_response.content return { \u0026#34;plan\u0026#34;: plan[1:], # 移除已执行的第一个步骤 \u0026#34;past_steps\u0026#34;: [(task, result)], # 追加执行历史 } Replanner 节点 Replanner 根据原始任务、已执行步骤和剩余计划做出三选一的决策：\n决策 含义 触发条件 respond 信息充足，立即生成最终报告 最高优先级，已执行 ≥ 3 步且有关键信息 continue 当前计划合理，继续执行 剩余步骤确实必要 replan 调整计划，替换剩余步骤 最低优先级，计划有重大偏差时才使用 决策同样用 Pydantic 模型约束输出：\nclass Act(BaseModel): action: str = Field(description=\u0026#34;下一步行动: \u0026#39;continue\u0026#39; | \u0026#39;replan\u0026#39; | \u0026#39;respond\u0026#39;\u0026#34;) new_steps: List[str] = Field(default_factory=list, description=\u0026#34;replan 时的新步骤列表\u0026#34;) Replanner 核心逻辑（含安全限制）：\nasync def replanner(state: PlanExecuteState) -\u0026gt; Dict[str, Any]: past_steps = state.get(\u0026#34;past_steps\u0026#34;, []) plan = state.get(\u0026#34;plan\u0026#34;, []) # 安全限制：已执行步骤超过 8 步，强制生成响应，防止无限循环 if len(past_steps) \u0026gt;= 8: return await _generate_response(state, llm) if plan: # 还有剩余步骤，让 LLM 做决策 act = await replanner_chain.ainvoke({ \u0026#34;messages\u0026#34;: [ (\u0026#34;user\u0026#34;, f\u0026#34;原始任务: {input_text}\u0026#34;), (\u0026#34;user\u0026#34;, f\u0026#34;已执行的步骤:\\n{steps_summary}\u0026#34;), (\u0026#34;user\u0026#34;, f\u0026#34;剩余计划: {\u0026#39;, \u0026#39;.join(plan)}\u0026#34;), ], \u0026#34;tools_description\u0026#34;: tools_description }) if act.action == \u0026#34;respond\u0026#34;: return await _generate_response(state, llm) elif act.action == \u0026#34;replan\u0026#34;: # 安全限制：新步骤数不能超过当前剩余步骤数 new_steps = act.new_steps[:len(plan)] return {\u0026#34;plan\u0026#34;: new_steps} else: # continue return {} # 不修改状态，继续执行下一步 else: # 计划已执行完毕，直接生成最终响应 return await _generate_response(state, llm) 当决定 respond 时，_generate_response 会整理所有执行历史，生成结构化的 Markdown 报告：\nasync def _generate_response(state, llm) -\u0026gt; Dict[str, Any]: execution_history = \u0026#34;\\n\\n\u0026#34;.join([ f\u0026#34;### 步骤: {step}\\n**结果:**\\n{result}\u0026#34; for step, result in past_steps ]) response_gen = response_prompt | llm.with_structured_output(Response) response_obj = await response_gen.ainvoke({ \u0026#34;messages\u0026#34;: [ (\u0026#34;user\u0026#34;, f\u0026#34;原始任务: {input_text}\u0026#34;), (\u0026#34;user\u0026#34;, f\u0026#34;执行历史:\\n{execution_history}\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;请基于以上信息生成全面的最终响应\u0026#34;), ] }) return {\u0026#34;response\u0026#34;: response_obj.response} 构建 LangGraph 工作流 三个节点通过 StateGraph 连接，replanner 之后根据状态中是否存在 response 进行条件路由：\ndef _build_graph(self): workflow = StateGraph(PlanExecuteState) # 添加三个节点 workflow.add_node(\u0026#34;planner\u0026#34;, planner) workflow.add_node(\u0026#34;executor\u0026#34;, executor) workflow.add_node(\u0026#34;replanner\u0026#34;, replanner) # 固定边：planner -\u0026gt; executor -\u0026gt; replanner workflow.set_entry_point(\u0026#34;planner\u0026#34;) workflow.add_edge(\u0026#34;planner\u0026#34;, \u0026#34;executor\u0026#34;) workflow.add_edge(\u0026#34;executor\u0026#34;, \u0026#34;replanner\u0026#34;) # 条件边：replanner 根据状态决定结束还是继续执行 def should_continue(state: PlanExecuteState) -\u0026gt; str: if state.get(\u0026#34;response\u0026#34;): return END # 已生成最终响应，结束流程 if state.get(\u0026#34;plan\u0026#34;): return \u0026#34;executor\u0026#34; # 还有步骤，继续执行 return END workflow.add_conditional_edges(\u0026#34;replanner\u0026#34;, should_continue, { \u0026#34;executor\u0026#34;: \u0026#34;executor\u0026#34;, END: END }) return workflow.compile(checkpointer=MemorySaver()) 工作流结构如下：\n[用户任务] → planner → executor → replanner ↑ | | continue/replan └───────────┘ | respond/空计划 ↓ [END] 执行工作流（流式输出） AIOpsService.execute 使用 graph.astream(stream_mode=\u0026quot;updates\u0026quot;) 流式执行，每个节点完成后立即产生事件，可以实时推送给前端展示执行进度：\nasync def execute(self, user_input: str, session_id: str) -\u0026gt; AsyncGenerator: initial_state: PlanExecuteState = { \u0026#34;input\u0026#34;: user_input, \u0026#34;plan\u0026#34;: [], \u0026#34;past_steps\u0026#34;: [], \u0026#34;response\u0026#34;: \u0026#34;\u0026#34; } async for event in self.graph.astream( input=initial_state, config={\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}, stream_mode=\u0026#34;updates\u0026#34; # 每个节点更新后立即推送 ): for node_name, node_output in event.items(): if node_name == \u0026#34;planner\u0026#34;: yield self._format_planner_event(node_output) # type: plan elif node_name == \u0026#34;executor\u0026#34;: yield self._format_executor_event(node_output) # type: step_complete elif node_name == \u0026#34;replanner\u0026#34;: yield self._format_replanner_event(node_output) # type: report / status # 所有节点完成，推送最终响应 final_state = self.graph.get_state({\u0026#34;configurable\u0026#34;: {\u0026#34;thread_id\u0026#34;: session_id}}) final_response = final_state.values.get(\u0026#34;response\u0026#34;, \u0026#34;\u0026#34;) if final_state else \u0026#34;\u0026#34; yield { \u0026#34;type\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;stage\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;任务执行完成\u0026#34;, \u0026#34;response\u0026#34;: final_response } 流式事件类型说明：\ntype stage 含义 plan plan_created Planner 生成了执行计划 step_complete step_executed Executor 执行完一个步骤 report final_report Replanner 生成了最终报告 status 各节点名 节点运行中的状态通知 complete complete 整个工作流结束 error error 执行出错 总结 通过上面的分析，我们已经了解了 Planner、Executor、Replanner 的作用和相关 prompt。代码的核心是 LangGraph 的 StateGraph 管理节点间的状态流转，PlanExecuteState 在整个流程中传递，三个节点各司其职：\nPlanner 查询知识库获取经验，生成结构化步骤列表\nExecutor 取出第一步，通过 ToolNode 自动执行工具调用，返回结果并移除该步\nReplanner 评估已执行结果，决定是继续、调整还是收敛生成最终报告\n框架帮我们处理了节点间的数据传递、条件路由和流式输出，核心还是要搞懂设计原理：Plan-Execute-Replan 本质上是一个带反馈的闭环调度器，Replanner 的收敛策略决定了整个流程的质量与效率。\n📷 [图片 token=GIrDbzR7WouUWMxHlD3cQGyYnpf（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%BA%94%E7%AB%A0%EF%BD%9C%E8%BF%90%E7%BB%B4Agent%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E8%BF%90%E7%BB%B4Agent%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%28Python%29/","summary":"\u0026lt;image token=\u0026ldquo;Wy2CbrxSOoMSeLxMPYGc9co4nSd\u0026rdquo; width=\u0026ldquo;2618\u0026rdquo; height=\u0026ldquo;2074\u0026rdquo; align=\u0026ldquo;center\u0026rdquo;/  前言 关键代码： app/agent/aiops/  目录下的","title":"源码分析：运维Agent代码实现(Python)"},{"content":"配置修改 配置文件路径：super_biz_agent_py/.env\nPython版本只需要修改 DASHSCOPE_API_KEY 即可跑起来\n📷 [图片 token=FfgkbdgE4oMwzLxAlmNc8pidnJr（未能下载，见飞书原文）]\n一键式启动 在启动项目之前，请完整的看完README.md，里面写的非常详细，一键启动脚本也帮你写好了 📷 [图片 token=OeSMbVLIhoKnALxSVDtc0Si9n1e（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE%E6%95%99%E7%A8%8B%28Python%29/","summary":"配置修改 配置文件路径： super_biz_agent_py/.env Python版本只需要修改  DASHSCOPE_API_KEY  即可跑起来 \u0026lt;image token=\u0026ldquo;FfgkbdgE4oMwzLxAlmNc8pidnJr\u0026rdquo;","title":"运行项目教程(Python)"},{"content":"OncallAgent 是本地优先 AIOps Agent 工作台，知识检索面对的内容既有自然语言，也有错误码、API 路径、服务名和中英文混排。纯向量检索擅长语义近似，却可能错过精确标识符；纯关键词检索能抓住错误码，却不理解同义表达。当前实现因此采用两路并行召回、RRF 融合和真实 rerank 的流水线。\n📷 [图片 token=KBcZb7vwqoZKVFxaE6jcpp6WnJd（未能下载，见飞书原文）]\n这条流水线不是聊天的固定前置步骤。知识能力被包装为 knowledge_retrieval LangChain Tool，只有 Agent 判断需要文档上下文时才调用。工具被创建时已经绑定当前用户和可访问知识库，模型可以选择 query、topK 和有限过滤器，却不能扩大权限范围。\n📷 [图片 token=GlpPbmgjYoCsp3xsAJtcfkgSn3B（未能下载，见飞书原文）]\n“混合检索”也不意味着某一路失败时尽量返回另一边。当前规格和代码选择严格一致性：embedding、Milvus 搜索、chunk 枚举、BM25 或 rerank 任一失败，整个工具返回安全系统错误；只有确实无匹配时才返回空 results。这个边界避免把粗排分数冒充最终相关性。\n📷 [图片 token=EHGybSVD2ofLL7xZuAJcudmFnSe（未能下载，见飞书原文）]\n学习目标 理解 Milvus 向量召回和进程内 BM25L 关键词召回如何并行。\n掌握中文、运维标识符的确定性分词与 BM25 候选筛选。\n手算 RRF(k=60) 的排名贡献并理解去重。\n区分 vector、BM25、RRF、rerank 四类分数和三类一基排名。\n识别 tenant 过滤、空知识库短路和失败不降级的安全边界。\n功能入口与完整调用链 LangChainChatAgentRunner.stream 为每次聊天请求调用 create_langchain_knowledge_retrieval_tool。这个工厂闭包绑定 owner_user_id 与 accessible_knowledge_base_ids，生成结构化工具。模型调用后，KnowledgeRetrievalTool.run 先校验非空 query、把 topK 限制在最多 5，并验证请求知识库是否是可访问集合的子集。\n📷 [图片 token=MFNqbaW71omhk8xddVzcVmEOnDd（未能下载，见飞书原文）]\n只要至少存在一个授权知识库，run 就用 asyncio.gather 同时启动两条分支。向量分支把 query 送给 embedding，再调用 MilvusVectorStore.search_chunks，最多取 20 条；关键词分支通过 list_chunks 按 tenant 与知识库枚举标量 chunk，把内容载入当前进程，使用 BM25L 排出最多 20 条。\n📷 [图片 token=B9VIbAnd5ohf03xVotKc02bznVf（未能下载，见飞书原文）]\n两路结果再次执行 owner、tenant、知识库、document 和 metadata 过滤，然后以 chunk ID 做 RRF。最多 20 个融合候选被送到 QwenVlRerankModel.arerank，最终按 relevance_score 返回最多 5 条命中和一一对应引用。LangChain 事件适配器从 citations 生成 reference.source SSE，前端由 rerankScore 优先排序展示。\n📷 [图片 token=VOOqbDi4loAxi4xZS7Ncau6nnVg（未能下载，见飞书原文）]\nAgent 选择调用 knowledge_retrieval → 校验 query、topK、filters、可访问知识库 → 并行： query embedding → Milvus COSINE 向量召回，最多 20 scoped list_chunks → tokenize → BM25L，最多 20 → 二次 owner / tenant / filter 校验 → RRF(k=60) 按 chunkId 融合，最多 20 → qwen3-vl-rerank → 最多 5 条 results 与 citations 📷 [图片 token=X1C8bqnBEoVbuvx81X8cSenwnue（未能下载，见飞书原文）]\n核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/retrieval/tool.py KnowledgeRetrievalTool、create_langchain_knowledge_retrieval_tool 校验、双路并发、过滤、融合、精排及结构化输出。 apps/backend/src/super_ai/retrieval/hybrid.py tokenize_hybrid_text、rank_bm25_documents、reciprocal_rank_fusion 中英文运维文本分词、BM25L 与 RRF 原语。 apps/backend/src/super_ai/vector_store/milvus.py MilvusVectorStore.search_chunks、list_chunks 受范围约束的向量搜索与关键词语料枚举。 apps/backend/src/super_ai/vector_store/schema.py build_chunk_collection_schema、build_index_definitions 定义标量字段、JSON metadata、FLOAT_VECTOR 和 HNSW 等索引。 apps/backend/src/super_ai/memory/vector_scope.py build_milvus_tenant_filter 构造 tenant 与允许知识库 ID 的 Milvus 布尔表达式。 apps/backend/src/super_ai/llm/rerank.py QwenVlRerankModel、RerankResult 调用 qwen3-vl-rerank，有限重试并严格校验返回结构。 packages/api-contracts/src/retrieval.ts KnowledgeRetrievalHit、KnowledgeRetrievalCitationSource 共享分阶段排名、分数和引用字段。 packages/api-contracts/src/sse.ts ReferenceSourceSseEvent 让检索解释信息通过聊天或诊断流传到前端。 apps/backend/tests/test_hybrid_retrieval.py test_rrf_combines_shared_candidates_with_k_60_and_deterministic_order 验证分词、非负 BM25 与确定性 RRF。 apps/backend/tests/test_knowledge_retrieval_tool.py test_vector_and_bm25_recall_execute_concurrently 验证并发、过滤、上限、失败边界和 LangChain 绑定。 openspec/specs/knowledge-retrieval-tool/spec.md Two-stage reranked retrieval 规定 20 条粗召回、RRF、真实 rerank 与最多 5 条输出。 📷 [图片 token=KkdzbV0omoYRrvxApXgcXh6jnlb（未能下载，见飞书原文）]\n代码调用流程图 混合检索的关键不是把两个分数相加，而是先并行获得不同类型的候选，再用 RRF 统一排名，最后交给 rerank 模型精排。\n📷 [图片 token=BAOJbBzq1oKjXGxapkPchREAnFg（未能下载，见飞书原文）]\n关键实现拆解 Milvus 向量召回与范围过滤 **看什么：**看 Milvus 搜索在连接前处理空知识库范围，并把 tenant 与允许知识库集合编译成服务端 filter，而不是先做无范围 ANN 再丢弃结果。\n# 1. 空授权集合直接短路，不连接 Milvus。 if not knowledge_base_ids: return [] client = self._connection_manager.connect() search_result = client.search( collection_name=self._settings.collection_name, data=[list(query_vector)], anns_field=VECTOR_FIELD, # 2. tenant 与知识库范围进入 Milvus 查询表达式。 filter=build_milvus_tenant_filter( tenant_id=tenant_id, knowledge_base_ids=knowledge_base_ids, ), limit=limit, search_params={ \u0026#34;metric_type\u0026#34;: self._settings.metric_type, \u0026#34;params\u0026#34;: dict(self._settings.search_params), }, output_fields=list(OUTPUT_FIELDS), timeout=self._settings.timeout_seconds, ) return [_search_hit_to_result(hit) for result_set in search_result for hit in result_set] 📷 [图片 token=AeCSbNHxgorQ5mx3ByXc2uU3ncg（未能下载，见飞书原文）]\n范围过滤发生在 ANN 搜索本身，随后工具层还会检查 ownerUserId、tenantId 与知识库 ID，形成存储与应用双层防御。配置决定向量维度、metric 和 search params；调用失败会上升为安全系统错误，不会退化成不带向量证据的伪精排结果。\n📷 [图片 token=ETmlbChXhoHVwNxyyygcgYYan8c（未能下载，见飞书原文）]\n文档索引写入的 collection 包含 chunkId、documentId、knowledgeBaseId、ownerUserId、tenantId、content、source、createdAt、metadata 和 vector。默认设置由项目配置加载，主规格要求默认 1024 维、HNSW、COSINE；实际 schema 使用配置维度，vector 索引和搜索参数也来自配置，而不是在检索代码里写死。\n📷 [图片 token=S9vMbTokronTL4xRR3uc7nNpnub（未能下载，见飞书原文）]\nsearch_chunks 在知识库列表为空时直接返回空，不连接 Milvus，也不发无范围查询。非空时，filter 形如 tenantId 等于当前用户且 knowledgeBaseId 位于允许集合，搜索只返回 OUTPUT_FIELDS 标量和 score。工具层随后还通过 _filter_chunks 检查 ownerUserId 与 tenantId 都等于当前用户，并应用 documentIds 和精确 metadata 键值过滤。这是存储过滤与应用防御的双层边界。\n📷 [图片 token=N74bbbYsAoYb6Exl5EKcs9bYnmf（未能下载，见飞书原文）]\n向量分支先调用 embedding 的 aembed_documents，且必须只得到一个 query vector。Milvus 搜索由 asyncio.to_thread 承载同步客户端调用，避免直接阻塞事件循环。向量候选上限是 RERANK_CANDIDATE_LIMIT，当前为 20。\n📷 [图片 token=Xb6qbq7gbowFljxfguqcJXZ5nMc（未能下载，见飞书原文）]\nBM25L：从 scoped chunks 建立临时词法语料 **看什么：**看 BM25L 前的 token 交集门槛和确定性排序；这两个细节防止 delta 常量把完全不含查询词的文档带入候选。\n# 1. 中英文混合查询先进入同一确定性 tokenizer。 query_tokens = tokenize_hybrid_text(query) if not query_tokens or not documents or limit \u0026lt; 1: return [] corpus_tokens = [tokenize_hybrid_text(document) for document in documents] scorer = _create_bm25_scorer(corpus_tokens) scores = scorer.get_scores(query_tokens) query_token_set = set(query_tokens) # 2. 至少有一个真实 token 交集才允许成为候选。 ranks = [ Bm25Rank(index=index, score=float(scores[index])) for index, tokens in enumerate(corpus_tokens) if query_token_set.intersection(tokens) ] # 3. 同分时回到原始语料顺序，保证结果可重复。 ranks.sort(key=lambda item: (-item.score, item.index)) return ranks[:limit] 📷 [图片 token=EkjJbiBcVot0h2xhVCYcN8i9nHg（未能下载，见飞书原文）]\n关键词分支的 documents 已经由 owner、tenant、知识库、documentIds 和 metadata 过滤，因此其他租户文本不会参与 IDF 统计。语料每次从 Milvus 标量字段枚举并临时建 scorer，没有持久 BM25 索引或缓存；枚举失败会让整个混合检索失败。\n📷 [图片 token=Hta2bfarCoOZwzxfIuXcEIQ4nDe（未能下载，见飞书原文）]\nBM25 语料不是另一份长期索引。list_chunks 使用 Milvus query iterator，每批 1000 条，枚举当前 tenant 和知识库的标量字段，不读取 vector。返回的 StoredVectorChunk 内容在当前工具调用中进入进程内列表，rank_bm25_documents 动态构建 rank_bm25.BM25L scorer。因此“内存 BM25L”指临时计算路径，不代表 SQLite 中维护了一套持久倒排索引。\n📷 [图片 token=OaUIb5D7oo4j6XxOPZHcTCwDnXc（未能下载，见飞书原文）]\ntokenize_hybrid_text 对 ASCII 运维标识符保留字母数字及点、下划线、冒号、斜杠、连字符组合并转小写，例如错误码和 v1/chat 不会被简单打散。连续中文同时产生单字和相邻双字 token，增强“超时错误”等短语匹配。排序前还要求 query token 与文档 token 至少有交集，因此未命中内容不会仅靠 BM25L 的 delta 常量混入候选。\n📷 [图片 token=OZJzbpT57oRFHGx8U8JcFJ3Dnle（未能下载，见飞书原文）]\nBM25 候选按分数降序、原始序号升序确定性排序并截断为 20。使用 BM25L 的一个具体目标是保持小语料精确命中的正分，并避免高频查询词产生负贡献；相关性质由 test_bm25_small_corpus_uses_positive_idf_for_exact_identifier 和 test_bm25_high_frequency_terms_never_create_negative_scores 固化。\n📷 [图片 token=JTj1bIYUSoJANdxILSTcPUGqngg（未能下载，见飞书原文）]\nRRF 融合与真实 rerank **看什么：**看粗召回候选如何先融合、空候选如何短路，再把文本交给真实 rerank；最终顺序不由 COSINE、BM25 或 RRF 任一原始分数冒充。\n# 1. RRF 只使用阶段排名，不直接比较两种原始分数量纲。 candidates = _fuse_candidates( vector_hits=filtered_vector_hits[:RERANK_CANDIDATE_LIMIT], keyword_chunks=filtered_keyword_chunks, bm25_ranks=bm25_ranks, ) if not candidates: return KnowledgeRetrievalToolResult( query=query, top_k=top_k, results=[], citations=[] ) try: # 2. rerank 决定最终顺序，top_n 不超过候选数量。 rankings = await self._rerank_model.arerank( query=query, documents=[candidate.chunk.content for candidate in candidates], top_n=min(top_k, len(candidates)), ) except Exception as exc: raise KnowledgeRetrievalError( code=\u0026#34;SYSTEM_UNAVAILABLE\u0026#34;, message=\u0026#34;Knowledge reranking is temporarily unavailable.\u0026#34;, ) from exc 📷 [图片 token=HZvLbqAgzogadtxL7ctc2tuTnZu（未能下载，见飞书原文）]\n没有候选时不调用 rerank，也不生成回退正文；rerank 异常统一变成安全的不可用错误。有效返回中的 relevance_score 才成为最终 score，且 provider 的 index 必须映射回当前候选，不能用粗排分数补造精排成功。\n📷 [图片 token=KUxabJuSRo0xiKxkgk1cvYSanlc（未能下载，见飞书原文）]\nRRF 不直接比较 COSINE score 和 BM25 score，因为两者量纲不同。reciprocal_rank_fusion 对每路列表先去重，再按一基 rank 累加 1 / (60 + rank)。例如一个 chunk 在向量第 2、BM25 第 1，其融合分是 1 / 62 + 1 / 61。共享候选通常因获得两路贡献而上升，单路候选仍可进入融合。\n📷 [图片 token=VWQ5biRz5okCAgxlx7nc8oRqnp1（未能下载，见飞书原文）]\n同分时，代码先比较两路中更靠前的 rank，再按 chunk ID，保证结果可重复。融合记录保留 vector_rank、bm25_rank 和 rrf_score，同时从原始两路字典带出 vector_score 与 bm25_score。没有在某一路命中的字段保持空值，不会伪造 0 分或虚假排名。\n📷 [图片 token=Xv2SbEFt9oWghzxqnQLcSZ1xn2B（未能下载，见飞书原文）]\n融合顺序仍只是粗排。QwenVlRerankModel 向配置 endpoint 发送 query 和候选文本，要求 top_n 合法；对 429、服务端错误和传输异常执行有限指数等待。返回必须包含不重复、在输入范围内的 index，以及 0 到 1 的有限 relevance_score。结果按相关性降序，最终 score 等于 rerank_score，rerank_rank 表示输出位置，不是 provider 的输入 index。\n📷 [图片 token=NqEkb3pENomRkkxYHQMct1bTnAb（未能下载，见飞书原文）]\n用一个运维查询手算候选流转 **看什么：**用这张局部候选图追踪同一个 chunk 在两路名次、RRF 和 rerank 中的位置变化；数字只是文中示例，公式与阶段顺序对应真实实现。\n📷 [图片 token=R90FbMmrroa8p5xBSRNcaZC6nUd（未能下载，见飞书原文）]\nRRF 奖励两路一致但不锁定最终顺序；只在单路命中的候选也进入并集。rerank 可以把精确错误码复盘提到第 1，最终 rerankRank 必须表示输出位置，未命中阶段的 rank 和 score 保持空值。\n📷 [图片 token=KzHDblkcpo2dy6xVeWrco8hSnFg（未能下载，见飞书原文）]\n假设用户问“api-gateway 出现 E_CONN_RESET 如何恢复”。向量分支可能把语义相近的“网关连接重置处置手册”排第 1，把包含精确错误码的复盘排第 3；BM25L 分支则可能把错误码复盘排第 1，把处置手册排第 4。复盘的 RRF 分为 1 / 63 + 1 / 61，手册为 1 / 61 + 1 / 64。RRF 用名次而非原始分数，让两路不同量纲都能贡献。\n📷 [图片 token=OMP9bEylDoiM6AxPJlxcJdqenCb（未能下载，见飞书原文）]\n如果另一个 chunk 只在 BM25L 第 2 出现，它仍获得 1 / 62 并进入候选；未命中的 vectorRank 和 vectorScore 保持空。反之，语义命中但不含查询 token 的 chunk 也可仅靠向量分支进入。这正是混合召回比简单交集更有价值的地方：并集保证覆盖，RRF 奖励多路一致。\n📷 [图片 token=SmxobaybaoBWv1xqhLGczOGkndb（未能下载，见飞书原文）]\nrerank 接收的是 RRF 排序后的文本数组，但 provider 可以完全改变顺序。例如只在 BM25L 命中的精确错误码文档，可能最终被判为最相关并得到 rerankRank 1。工具按 provider 返回 index 重新引用原候选，随后显式枚举 rerankRank。最终展示必须以 rerank 排名为准，同时保留粗召回排名解释“它为何进入候选”。\n📷 [图片 token=VbS6bqtVjoyFDvxrekacxnCBnHt（未能下载，见飞书原文）]\n过滤器在哪个阶段生效 **看什么：**看应用层过滤器同时核对存储范围字段、可选 documentIds 和浅层 metadata 精确等值，而不是把模型提供的任意表达式交给 Milvus。\nallowed_knowledge_base_ids = set(knowledge_base_ids) requested_document_ids = set(filters.document_ids) # 1. ownerUserId 与 tenantId 必须同时匹配当前用户。 return [ hit for hit in hits if hit.owner_user_id == owner_user_id and hit.tenant_id == owner_user_id and hit.knowledge_base_id in allowed_knowledge_base_ids and (not requested_document_ids or hit.document_id in requested_document_ids) and _metadata_matches(hit.metadata, filters.metadata) ] def _metadata_matches( hit_metadata: Mapping[str, object], required_metadata: Mapping[str, str | int | float | bool], ) -\u0026gt; bool: # 2. metadata 仅支持浅层键值精确匹配。 return all(hit_metadata.get(key) == value for key, value in required_metadata.items()) 📷 [图片 token=FLItbI93iocboox7HyRcb2Zvnvc（未能下载，见飞书原文）]\n这段过滤会同时用于向量 hits 和 BM25 语料，保证两路候选语义一致，并避免越权文档改变词法统计。它不支持范围、数组包含、嵌套路径或自定义查询语法；越权 knowledgeBaseIds 更早就在外部资源访问前被整体拒绝。\n📷 [图片 token=QgfdbhWqKoyozVx2RuzcZ86Nnwd（未能下载，见飞书原文）]\nknowledgeBaseIds 先与服务端 accessible set 比较，越权时整个调用在外部资源访问前结束。合法范围传给 Milvus 的 search 与 list，因此两路初始语料都已受 tenant 和知识库限制。documentIds 与 metadata 不被拼进当前 Milvus filter，而是在工具层对向量 hits 和枚举 chunks 做精确过滤，再进行 RRF。这保证两路过滤语义一致。\n📷 [图片 token=HDLjbWS5towM4MxVlGicLXCunib（未能下载，见飞书原文）]\nmetadata 匹配采用 hit_metadata.get(key) == value，是浅层精确等值，不支持范围、数组包含、模糊匹配或嵌套路径。布尔值、数字和字符串来自共享契约；调用者不能通过 metadata 请求任意表达式。这个限制降低了模型生成危险查询语法的风险，也意味着复杂检索条件需要未来明确扩展类型和测试。\n📷 [图片 token=EPX5bfKoIoiPbQx7OSwcNe5snzc（未能下载，见飞书原文）]\n向量结果即使来自带 Milvus filter 的查询，仍会被应用层检查 ownerUserId 与 tenantId。这能防御脏数据或错误写入。关键词分支在 BM25 前过滤，避免无权限文本参与词频统计；否则即使最终结果被过滤，其他租户文档也可能通过 IDF 改变排名。当前顺序把权限隔离同时落实到数据可见性和排序统计。\n📷 [图片 token=CxyqbQF3GocmpuxUC3qcFZ2knDe（未能下载，见飞书原文）]\n候选解释字段如何进入聊天引用 **看什么：**看最终 hit 与 citation 如何共享同一批阶段字段；citation 是结构转换，不会重新搜索或再次排序。\nresult = candidate.chunk # 1. 兼容 score 明确等于最终 rerank_score。 return KnowledgeRetrievalHit( chunk_id=result.chunk_id, # … 省略与本节无关的身份、正文和 metadata 字段 score=rerank_score, vector_rank=candidate.vector_rank, bm25_rank=candidate.bm25_rank, rerank_rank=rerank_rank, vector_score=candidate.vector_score, bm25_score=candidate.bm25_score, rrf_score=candidate.rrf_score, rerank_score=rerank_score, ) def _citation_from_hit(hit: KnowledgeRetrievalHit) -\u0026gt; KnowledgeRetrievalCitationSource: # 2. citation 直接复制同一 hit 的阶段解释字段。 return KnowledgeRetrievalCitationSource( id=hit.chunk_id, title=hit.source or hit.document_id, source_type=\u0026#34;knowledge-base\u0026#34;, # … 省略与本节无关的展示字段 vector_rank=hit.vector_rank, bm25_rank=hit.bm25_rank, rerank_rank=hit.rerank_rank, vector_score=hit.vector_score, bm25_score=hit.bm25_score, rrf_score=hit.rrf_score, rerank_score=hit.rerank_score, ) 📷 [图片 token=SIpnbDp1ro7N4IxnKxFcQKN9nbd（未能下载，见飞书原文）]\n结果与引用共用 chunk 身份和分阶段排名，因此聊天适配器可以把 citation 转成 reference.source 而不丢失召回路径。excerpt 只为展示截断，Agent 使用的 hit.content 仍是完整 chunk；高分解释相关性，不等于证明运维根因。\n📷 [图片 token=AzYhbKZKNoGV9Mxll3OcuwHAned（未能下载，见飞书原文）]\n_hit_from_fused_candidate 把 rerank score 同时写入 score 与 rerank_score，并保留三类 rank。_citation_from_hit 不是重新计算，而是从同一个 hit 派生 title、sourceType、excerpt 和阶段字段，因此 results 与 citations 不会因为二次排序而错位。citation ID 基于 chunk，能与工具结果和前端反馈关联。\n📷 [图片 token=E72IbqU81oGrt3xk7Q0c5K1TnfG（未能下载，见飞书原文）]\n当 LangChain 工具完成时，聊天适配器读取 output.citations，把字段转换为 reference.source。前端引用排序优先 rerankScore，并最多展示 5 条。这条链路让用户能看到“最终相关性”和“召回路径”，但分数仍是模型与检索算法输出，不是因果证明。AIOps 结论还需要结合真实工具结果和持久证据，不能仅凭高 rerankScore 宣称根因成立。\n📷 [图片 token=EPagbY7dhoWVuMx8Vl1cMAAunzh（未能下载，见飞书原文）]\nexcerpt 截断只服务展示，Agent 工具结果中的 hit.content 仍包含完整 chunk。metadata 可能带 headingPath、chunkingStrategy、knowledgeType 等索引信息。前端应优先使用结构化字段呈现来源，不应把 metadata 原样序列化成大段 JSON；现有聊天组件测试正是围绕可读引用详情而不是原始对象展开。\n📷 [图片 token=RFsNbvz9rojgydxnqmrcNL5Onfa（未能下载，见飞书原文）]\n性能与一致性的当前取舍 **看什么：**把最初集中展示的并行召回代码放到性能主题中看：两路共享相同范围，但 gather 只降低串行等待，不减少任一路工作量。\ntry: # 1. 两路召回共享 query、owner 与授权知识库范围。 vector_hits, keyword_recall = await asyncio.gather( self._vector_recall( query=query, owner_user_id=owner_user_id, knowledge_base_ids=knowledge_base_ids, ), self._keyword_recall( query=query, owner_user_id=owner_user_id, knowledge_base_ids=knowledge_base_ids, filters=filters, ), ) except KnowledgeRetrievalError: raise except Exception as exc: # 2. 任一路失败都会使整次混合检索安全失败。 raise KnowledgeRetrievalError( code=\u0026#34;SYSTEM_UNAVAILABLE\u0026#34;, message=\u0026#34;Knowledge retrieval is temporarily unavailable.\u0026#34;, ) from exc 📷 [图片 token=Qly9b2sryoDvcWxFod9caIXRnT0（未能下载，见飞书原文）]\n当前规范要求粗召回不可用时不得静默退化为单路，所以 gather 中任一基础设施异常都会终止本次调用。同步 Milvus 和 BM25 工作被送入线程以免阻塞事件循环，但线程中的调用不会因请求取消而变成可回滚事务。\n📷 [图片 token=OO9NbOzV3oGVoSxcilDclQN3n4e（未能下载，见飞书原文）]\n双路并发缩短了串行等待时间，但并不减少工作量。向量分支需要一次 query embedding 和一次 ANN 搜索，关键词分支需要枚举授权范围内全部标量 chunks 并在进程内建 BM25L。小型本地知识库下实现简单、结果新鲜；语料增长后，枚举和 scorer 构建可能成为主要成本。\n📷 [图片 token=Zs8xbeOJWo0A14xxHwKc1rhGnCf（未能下载，见飞书原文）]\n当前没有 BM25 cache。每次工具调用都从 Milvus 重新读取，所以刚完成索引或删除后的词法语料能及时反映存储状态，也避免维护第二套一致性协议。代价是重复查询。未来若加缓存，缓存键至少需要 tenant、知识库集合和索引版本，失效还要覆盖文档覆盖、删除与重建；只按 query 缓存会破坏权限和新鲜度。\n📷 [图片 token=FNyTbcY4fo9vdnxvBv0ckV6dnzh（未能下载，见飞书原文）]\n两路 asyncio.gather 中，向量 embedding 是异步 provider 调用，Milvus 与 BM25 同步工作通过 thread 执行。这个设计让事件循环保持响应，但线程并不把底层调用变成可取消事务；请求取消时，已经进入的同步 Milvus 查询可能仍运行到客户端超时。超时和重试由各 provider 设置控制，而不是 retrieval tool 自己提供统一总时限。\n📷 [图片 token=YZD9bAKxAo1hbpx9Vc9ciWtNnke（未能下载，见飞书原文）]\n空结果、错误与“没有证据” **看什么：**这张状态图把“成功但无命中”和“执行失败”分成不同终点；只有前者能被解释为当前授权范围内没有检索结果。\n📷 [图片 token=LZwrbNYU6oED0bxFXQicoAlgnDb（未能下载，见飞书原文）]\nEmpty 是正常成功结果，不调用 rerank，也不生成文档内容；Forbidden 和 Unavailable 是错误，不能被 Agent 改写成“知识库没有证据”。未完成索引的文档本来就不在 Milvus 中，所以空结果也不能证明某个已上传但 failed 的文档不包含答案。\n📷 [图片 token=X7YwbcS8touzOCxIvVAcUzM9nwe（未能下载，见飞书原文）]\n空结果有两种正常来源：授权后没有任何知识库，或者两路召回都没有通过过滤的候选。第一种在 embedding 和 Milvus 之前短路，第二种在 RRF 前后得到空集合；二者都返回原 query、规范化 topK、空 results 和空 citations，不调用 rerank。Agent 应据此说明未找到知识依据，而不是由工具补写一段看似合理的文档内容。\n📷 [图片 token=OPFwbUbqyoqAUlxnUWoc9vVKnFd（未能下载，见飞书原文）]\n无效 query、topK 或越权过滤属于调用错误，分别使用验证或授权代码。基础设施异常属于系统不可用。代码有意不把 provider 原始错误拼进公开 message，测试会注入带秘密标记的异常并断言未泄露。区分空结果与系统错误非常重要：前者是成功执行后的“无命中”，后者表示当前检索结论不可信，不能被解释成知识库确实没有相关内容。\n📷 [图片 token=EUD9b00S4oeCJexVLiIcznlxngg（未能下载，见飞书原文）]\nrerank 返回空列表在结构上可以形成空最终结果，但 provider 返回格式错误、重复 index、越界 index、非有限分数或超出 0 到 1 的分数会被 LlmRerankError 拒绝。工具不自行修补这些数据。严格校验保护 citation 与输入候选的映射，防止某个错误 index 把另一个 tenant 范围内不可见的问题外推为引用错配。\n📷 [图片 token=KGKKb9dCUomcZzxhkoIc7LZpnrb（未能下载，见飞书原文）]\n检索工具描述是“搜索当前用户已索引的知识库文档”。未完成索引的 SQLite 文档不会出现在 Milvus，failed 文档也没有可靠的新 chunks；工具不会读取上传 metadata 来生成临时候选。换言之，文档管理页面的 indexed 状态是检索可用性的前提之一，混合算法不能补偿上游索引失败。\n📷 [图片 token=R2zJbJJB6o20ckxrb0lcnJnwnIc（未能下载，见飞书原文）]\n数据、契约与状态 一个最终 hit 同时携带 chunk、document、knowledge base、owner、tenant、content、source、metadata 和解释字段。vectorRank、bm25Rank、rerankRank 是一基排名；vectorScore 是 Milvus 向量得分，bm25Score 是 BM25L 分数，rrfScore 是倒数排名贡献之和，rerankScore 是最终模型相关性。不能跨阶段比较这些数值大小。\n📷 [图片 token=Oc5WbnlriocZAtxyn0Dcf7fNnkA（未能下载，见飞书原文）]\ntopK 默认 5，输入小于 1 是验证错误，超过 5 会被硬限制到 5。RRF 候选最多 20，最终没有额外最低分阈值。results 和 citations 一一对应；citation 的 excerpt 最多 480 个字符，并保留同样的阶段分数与排名，以便 UI 展示可追溯依据。\n📷 [图片 token=D3Jxbo043oLdvuxZfJWc0S0wn9g（未能下载，见飞书原文）]\n权限、安全与失败边界 _resolve_knowledge_base_ids 先对可访问列表去重。模型不传过滤器时使用全部可访问知识库；传入时必须是子集，任何越权 ID 都触发 AUTH_FORBIDDEN，且不会调用 embedding、Milvus search 或 list。授权后集合为空则直接返回空 results 和 citations。\n📷 [图片 token=AaP7bXIWforT23xpUA2cQL3EnJf（未能下载，见飞书原文）]\n双路通过 asyncio.gather 并发，但这也意味着任一分支失败都会取消当前完整结果。工具把未知粗召回异常转换为 SYSTEM_UNAVAILABLE 和固定安全消息。rerank 失败单独映射为知识精排暂不可用，绝不回退成 vector-only、BM25-only 或 RRF-only。空候选则不调用 rerank，诚实返回空集合。\n📷 [图片 token=ZmVMbJPlHoeajrxCCpPcavrdnVh（未能下载，见飞书原文）]\n当前 BM25L 每次枚举授权范围内全部 chunks，复杂度会随单个用户语料增长；它不是无界全租户读取，因为 Milvus filter 始终存在，但也尚未实现持久倒排索引或增量词法索引。评估大规模数据时必须把这个现实成本纳入设计。\n📷 [图片 token=IuDpbVgkgofdl3x91soc6EZXnhh（未能下载，见飞书原文）]\n阅读顺序与小结 先读 retrieval.ts，明确每个排名和分数字段的含义。\n再读 vector_scope.py 与 Milvus 的 search、list，确认语料范围。\n随后结合 apps/backend/src/super_ai/retrieval/hybrid.py 手算 tokenize、BM25 和 RRF。\n最后沿 KnowledgeRetrievalTool.run 跟到 rerank、citation 和 LangChain Tool。\n这条混合检索链路的价值不只是更高召回率，而是把每一阶段的作用和证据分开：Milvus 找语义，BM25L 找词项，RRF 融合排名，rerank 决定最终顺序。严格的 owner scope、空结果和失败不降级规则，让这些分数可以被解释，而不会变成看似精确的伪证据。\n📷 [图片 token=Tz0ebm8XYoHmPpxzux1c1U3DnUg（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/08.%20Milvus%E3%80%81BM25L%E3%80%81RRF%20%E4%B8%8E%20rerank%20%E6%B7%B7%E5%90%88%E6%A3%80%E7%B4%A2/","summary":"OncallAgent 是本地优先 AIOps Agent 工作台，知识检索面对的内容既有自然语言，也有错误码、API 路径、服务名和中英文混排。纯向量检索擅长语义近似，却可能错过精确标识符；纯关键词检索能抓住错误码，却不理解同义表达。当前","title":"08. Milvus、BM25L、RRF 与 rerank 混合检索"},{"content":"前言 AI时代，代码是最不值钱的东西。与其“求代码”，不如你花10分钟，下载一个ai coding 的ide上手，自己去提出第一个问题。\n当别人把思路发出来之后，我们只需要 把需求 转述 给AI对话框，代码就出来了\u0026hellip;\n2026年了，拥抱AI吧。学会从写代码，到给AI提需求。\n📷 [图片 token=Q7e1bnFt8omTAcxleutcYlgqnsb（未能下载，见飞书原文）]\n常见的AI IDE **你现在当务之急是随便下载一个IDE打开对话框随便问几个问题，提几个需求，**随便下载哪个都行。\n字节trae：https://www.trae.cn/\n腾讯codebuddy ：https://www.codebuddy.ai/home\n阿里qoder：https://qoder.com/zh\ncursor：https://cursor.com/\n优化 对于 重排、上下文压缩、知识库支持pdf，这三类是问的最多的。\n这三点真的很难吗？或许因为没动手自己写过会觉得困难。别怕，AI会就行！\n你还可以要求AI写完代码之后，教你代码是怎么写的。\n在AI时代，这三个功能，10分钟即可。请动手下载一个AI IDE自己试一试吧～（选个聪明的模型）\n重排 我要增加一个功能：在召回文档后，对文档内容进行重排： 1. 指定一个计划，跟我对齐后开始工作 2. 改动的内容记录到文档上，详细告诉我改了什么，把我教明白 1. 使用阿里云百炼平台的重排模型 2. 不降级 3. 文档写到更新日志—{功能}.md里 简历描述：在 RAG 流程中实现向量相似度召回 +文本重排两阶段检索：先由 Milvus 召回 Top-N 候选，再调用 Rerank 按查询相关性重排序，提升检索结果与问题的匹配度。\n🎬 视频「重排.mp4」（飞书视频，无法在博客播放）\n上下文压缩 我要增加一个功能，对话增加上下文自动压缩： 1. 现在的对话功能比较简单，只是把前面的对话轮数存储到了内存里面 2. 现在我想让大模型的上下文窗口在70%的时候，就自动对前面的内容进行大模型压缩 3. 先看看langchain框架是否有这个自动压缩总结的功能，如果没有则自己实现 4. 首先定制计划，跟我对齐后开始工作。最后把修改的代码和内容写入到一个文档里，把我当作一个什么都不懂的学生教会我 简历描述：基于 LangChain SummarizationMiddleware，在对话 token 接近模型上下文上限（可配置比例，默认 70%）时自动触发 LLM 摘要，保留最近多轮原文，避免长会话与 RAG 多轮检索撑爆上下文窗口。\n🎬 视频「上下文自动压缩.mp4」（飞书视频，无法在博客播放）\n知识库支持多类型文件 在文档上传知识库自动向量化的环节里，以前只支持了md还是txt？我现在要求支持多文件类型： 1. 根据上传文件类型不同，自动匹配到不同的文件类型处理器里面去进行分片操作 2. 把文件类型处理器支持的所有类型，都写到文档里面，告诉我里面具体是怎么分片的 3. 现阶段要求支持md,txt,pdf,word这四种类型。尝试使用开源的最佳实践方案或者sdk 4. 跟我对齐方案后进行工作，最后把改动的代码内容都写入到一个文档里，教会我怎么写的怎么改的 简历描述：实现知识库多类型文档（TXT/Markdown/PDF/Word）自动解析与向量化索引，采用提取层与分片层解耦及开源组件（pypdf、python-docx），提升知识入库覆盖率与可扩展性。\n🎬 视频「支持多类型.mp4」（飞书视频，无法在博客播放）\n其他优化思路 记忆的持久化 现状：现在的记忆都是存储在内存之后，服务重启即丢失\n方向：将记忆持久化到db里面，用户请求到来之时，先去检索一遍数据库，获取历史对话信息\nRAG 评测 现状：无测评\n方向：熟悉RAGAS框架，熟练背诵RAGAS八股文/面试题\n价值：支撑RAG 召回评测和真实文档的完整故事\n业务 Skill落地 **现状：**在最初的设计之中，各种处理文档，SOP都是存在知识库之中的。\n**方向：**在26年之后skill爆火。其实这部分内容是可以写到skill里面的，而不再需要知识库了。\n价值：体现真实技术迭代而非纯包装。\n可观测性与稳定性 指标：请求延迟、Token 用量、检索条数等常见业务指标\n**方向：**适用于有经验的社招，实习可不用关注\n**价值：**作为一个成熟的项目，除了项目本身，更应该思考整体的稳定性，可观测等\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/vibe%20coding---%E4%BC%98%E5%8C%96%E9%A1%B9%E7%9B%AE/","summary":"前言 AI时代，代码是最不值钱的东西。与其“求代码”，不如你花10分钟，下载一个ai coding 的ide上手，自己去提出第一个问题。 当别人把思路发出来之后，我们只需要 把需求 转述 给AI对话框，代码就出来了\u0026hellip; 2026年了，拥抱","title":"vibe coding---优化项目"},{"content":"上一章讲完了 Function Calling，大模型通过标准化的 JSON 格式，告诉 Agent 要调哪个工具、传什么参数。到这里，你已经理解了工具是什么、工具怎么被调用起来。\n但实际开发 Agent 的时候，你会很快遇到下一个麻烦：**工具集成本身，是个无底洞。 **\n没有 MCP 之前：重复造轮子的噩梦 假设你正在开发一个 Agent，需要它能读取 GitHub 代码仓库、查询公司数据库、操作本地文件系统、还能发 Slack 消息。\n每一个工具，你都得自己来：\n自己研究这个工具的 API 文档\n自己把 API 封装成函数\n自己给每个函数写 Function Calling 定义（name、description、parameters）\n自己处理认证、错误处理、数据格式转换\n光这四件事，就够你忙好几天。更麻烦的是，这些集成代码只能用在你这个项目里，团队里其他同学做类似的 Agent，还得重新来一遍。\n如果你的 Agent 还需要同时支持多个大模型（今天接 Claude、明天接 GPT-4、后天接 Qwen），问题就更大了：\n10 个工具 × 5 个大模型 = 50 套集成代码\n不同模型的 Function Calling 格式还可能有细微差别，你要针对每个模型写适配层。哪天 GitHub API 升级了，50 套代码你一个个去改，这还是基础设施代码，不是真正做产品该花时间的地方。\n整个生态里，无数开发者都在重复做着同一件事：把 GitHub、Slack、数据库、文件系统……这些常见服务接入自己的 AI 应用。每个人写一套，互相之间完全无法复用。这就是没有统一标准时的现实，大家都在重复造同一个轮子。\nMCP 的出现：给 AI 工具世界定一个标准 这就是 Anthropic 在 2024 年 11 月推出 MCP（Model Context Protocol，模型上下文协议） 的背景。\nMCP 要解决的核心问题，可以用一句话概括：把工具的「写好」和「用起来」彻底拆开。\n📷 [图片 token=TtdBbDfHyoTuHKxmhDqcE8b5nug（未能下载，见飞书原文）]\n举个生活中的例子：在 USB-C 统一之前，各家手机厂商各有各的充电口，苹果用 Lightning、安卓早期用 Micro-USB，各品牌之间完全不兼容，出门得带三四根不同的充电线。USB-C 出来之后，只要设备支持 USB-C，任何 USB-C 的线都能用，充电器、数据线全部通用。\nMCP 对于 AI 工具世界的意义，就和 USB-C 对于充电口的意义一模一样。\n📷 [图片 token=WfKjbkljPo64buxNpJUcL3VEnoQ（未能下载，见飞书原文）]\n工具开发者只需要实现一套 MCP Server，把工具能力按 MCP 协议暴露出来，之后所有支持 MCP 的 AI 应用，都能直接接入，不需要任何额外适配。\nAI 开发者只需要接入 MCP Client，就能调用整个 MCP 生态里所有已有的工具，不用自己写任何工具集成代码。\n原来的 N×M 问题，变成了 N+M：N 个工具各写一次 MCP Server，M 个 AI 应用各写一次 MCP Client，然后任意组合，全部互通。\nMCP 的三大角色 MCP 架构里有三个核心角色，弄清楚它们是谁、各自做什么，MCP 就说明白一大半了。\n📷 [图片 token=V88KbpAIeoj5mpxflSOc5Ybenlh（未能下载，见飞书原文）]\nHost（宿主） Host 就是你最终使用的那个 AI 应用，可以是 Claude Desktop、带 AI 功能的 VS Code、或者你自己开发的 Agent 程序。Host 是整个交互的起点，用户在 Host 里提问，Host 决定要调用哪些工具来完成任务。\nClient（客户端） Client 是 Host 内部的一个组件，专门负责管理和 MCP Server 的连接。你可以把它理解为一个「连接器」，Host 说「我要调用 GitHub 工具」，Client 就负责找到对应的 MCP Server、建立连接、发送请求、拿回结果。\n每个 Host 通常会内置一个 MCP Client，你不需要自己开发，直接配置就能用。\nServer（服务端） Server 是对外暴露工具能力的轻量级服务程序。一个 MCP Server 通常负责一类工具，比如专门处理 GitHub 的 MCP Server、专门操作本地文件的 MCP Server、专门查询数据库的 MCP Server。\nServer 和 Host 可以运行在同一台机器上（本地 Server），也可以部署在远程服务器上（远程 Server）。对于 Host 和 Client 来说，这些细节完全透明，调用方式完全一致。\n三者的关系用一张图来看：\n📷 [图片 token=KW4Vbz2dvoxoldxNHSLcgGTin1e（未能下载，见飞书原文）]\n用户提问给 Host，Host 借助 Client 调用一个或多个 MCP Server，Server 执行完毕把结果回传，Host 拿到结果后给用户生成最终答案。\nMCP 和 Function Calling 是什么关系？ 学到这里，很多同学脑子里会冒出一个问题：上一章学了 Function Calling，说大模型通过 Function Calling 告诉 Agent 调哪个工具；这章又学了 MCP，说 Agent 通过 MCP 来调用工具。这两个东西，到底有什么区别？MCP 是不是把 Function Calling 给替代了？\n完全不是。Function Calling 和 MCP 解决的是不同层面的问题，它们是配合关系，不是替代关系。 我们来一步步理清楚。\n第一步：确认 Function Calling 工作在哪个层面\nFunction Calling 解决的问题是：大模型做出「要调这个工具」的决策之后，怎么把这个决策用标准化 JSON 格式传给 Agent。这是大模型和 Agent 之间的通信协议，负责「大模型怎么开口下指令」这一段。\n第二步：确认 MCP 工作在哪个层面\nMCP 解决的问题是：工具怎么被统一注册、统一发现、统一调用。这是Agent 和工具服务之间的连接协议，负责「Agent 怎么找到并执行工具」这一段。\n第三步：看两者怎么配合\n把两者放进整个调用链条，位置就一目了然：\n📷 [图片 token=ZVUlbMjWXopkyuxCIp3cv3HknPd（未能下载，见飞书原文）]\nFunction Calling 负责上半段：大模型用 Function Calling 格式告诉 Agent「调哪个工具、传什么参数」。\nMCP 负责下半段：Agent 通过 MCP 协议，找到对应的 MCP Server，把工具真正执行起来。\n用一个具体例子把两者串联起来\n还是查天气的场景，用户问「上海明天天气怎样」：\n大模型通过 Function Calling 返回调用指令，「调 check_weather，city=上海」。这是 Function Calling 层，大模型在开口下指令。\nAgent 里的 MCP Client 收到这条 Function Calling 指令，通过 MCP 协议找到天气 MCP Server，把请求路由过去。这是 MCP 层，Agent 在找到并执行工具。\n天气 MCP Server 调用真实的天气 API，拿到结果，按 MCP 格式回传。\n大模型收到结果，整理成自然语言告诉用户。\n所以 Function Calling 是「说什么」的规范，MCP 是「怎么找到并执行」的规范。少了 Function Calling，大模型不知道怎么开口下指令；少了 MCP，Agent 不知道去哪里找工具来执行。两者分别在调用链的不同位置发挥作用，缺一不可。\nMCP Server 的三种能力 一个 MCP Server 可以暴露三种类型的能力，分别对应 AI 在不同场景下的不同需求。\nTools（工具） 这是 MCP Server 最核心的能力，也是和上一章讲的工具概念最直接对应的部分。Tools 就是 AI 可以主动调用执行的函数，发邮件、查数据库、提交代码、搜索网页，都属于 Tool。\nAI 调用 Tool 的机制，底层就是 Function Calling，MCP 在这之上做了一层标准化封装，让你不需要手写每个 Tool 的 Function Calling 定义，MCP Server 会自动按标准格式对外声明工具清单。\nResources（资源） Resources 是 AI 可以读取访问的数据，文件内容、数据库记录、代码仓库、网页内容等。\nTools 和 Resources 的区别在于：Tools 是「做一件事」，有副作用，会改变外部状态；Resources 是「读一份数据」，只读，不会改变任何东西。这个区分很重要，因为 AI 系统通常对「会产生副作用的操作」需要更谨慎的权限控制，读数据和写数据，应该分开授权。\nPrompts（提示模板） Prompts 是预定义的可复用提示词模板。当你有一些常用的、固定结构的提示词（比如「代码 Review 模板」「会议纪要生成模板」），可以把它们封装成 MCP Prompts，在不同 Agent 项目里直接复用，不用每次重新写。\n一次完整的 MCP 调用流程 用「帮我查一下 GitHub 上 React 仓库最近的 commit」这个任务，走一遍完整的 MCP 调用流程：\n📷 [图片 token=M2iubOk4IoqbmuxL8a2c2zwwnLg（未能下载，见飞书原文）]\n用户提问：在 Host（比如 Claude Desktop）里输入「帮我查一下 React 仓库最近的 commit」\nHost 分析任务：大模型判断需要调用 GitHub 工具，生成 Function Call 格式的调用指令\nClient 接收指令：Host 把调用指令交给内置的 MCP Client\nClient 路由到对应 Server：Client 根据工具名，找到负责 GitHub 能力的 MCP Server，把请求发过去\nServer 执行：GitHub MCP Server 调用 GitHub API，拿到最近的 commit 列表\n结果回传：Server 把结果按 MCP 协议格式回传给 Client，Client 转交给 Host\nHost 生成回复：大模型拿到结果，整理成自然语言回复给用户\n整个过程中，Host 和背后的大模型完全不需要知道 GitHub API 的任何细节，它只管说「我要调 GitHub 工具」，剩下的事情 MCP Server 全权负责。这就是「解耦」的价值：工具的实现细节，和 AI 的调用决策，完全分离。\n一张表看懂：没有 MCP vs 有 MCP vJKoy7 对比维度 没有 MCP（自己写集成） 有 MCP 工具集成方式 每个工具自己写 Function Calling 定义、自己对接 API、自己处理格式 直接接入已有 MCP Server，工具现成可用 多模型支持 N 个工具 × M 个模型 = N×M 套集成代码 N 个工具 + M 个模型，各写一遍标准接口，任意组合 跨项目复用 几乎不可能，每个项目重新写一遍 MCP Server 一次实现，所有项目直接接入 工具 API 升级 所有集成代码都要跟着改 只需更新对应的 MCP Server，所有 Host 自动受益 生态 各自为战，没有共享生态 全球开发者贡献 MCP Server，接入即可使用海量现成工具 开发成本 极高，大量时间花在工具集成而非业务逻辑上 极低，专注业务逻辑，工具接入开箱即用 总结 整理一下这一章的核心认知：\nMCP 是什么：Anthropic 推出的开放标准协议，定义了 AI 应用和工具服务之间如何标准化通信，是 AI 工具世界的「USB-C」。\n为什么需要它：没有统一标准时，每个工具都要自己写集成，多模型场景下是 N×M 的重复工作量；MCP 把这个问题变成 N+M，工具写一次，全平台可用。\n三大角色：Host（AI 应用）通过内置的 Client（连接器）调用 MCP Server（工具服务），职责清晰，完全解耦。\n三种能力：Tools（可执行操作）、Resources（可读取数据）、Prompts（可复用模板），覆盖 AI 在工具调用场景下的所有需求。\n后续章节呼应：\nRAG：解决「大模型看不到你私有数据」的问题，把知识库检索封装成工具，让 Agent 能随时访问私有知识，是 Agent 开发中最常见的能力之一 ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AFMCP%EF%BC%9F/","summary":"上一章讲完了 Function Calling，大模型通过标准化的 JSON 格式，告诉 Agent 要调哪个工具、传什么参数。到这里，你已经理解了工具是什么、工具怎么被调用起来。 但实际开发 Agent 的时候，你会很快遇到下一个麻烦：","title":"什么是MCP？"},{"content":" 📷 [图片 token=UL28bV4v4of7LxxA60OcWLVbnvb（未能下载，见飞书原文）]\n📷 [图片 token=UrGebdiIVoWjsGxpGhicMk4knVb（未能下载，见飞书原文）]\n注意，运行程序之前请先看：\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\n前言 本节我们来实现知识库Agent的上半部分，即将文件向量化后存储到数据库中。\n这部分代码在：SuperBizAgent/internal/ai/agent/knowledge_index_pipeline\n运行的代码在：SuperBizAgent/internal/ai/cmd/knowledge_cmd/main.go\n📷 [图片 token=UKl8byYCPo3gNIx4ux0cDCIbnhd（未能下载，见飞书原文）]\n流程梳理 我们的目标是将文件向量化后存储到数据库中，这里面具体步骤：\n读取文件\n切分文件\n索引（Embedding和存储）\n既然有3个步骤，我们可以使用eino的可视化编排插件，来进行流程的编排：\n首先在Goland里面安装eino-dev的插件。\n打开插件，点击右上角的add node，按照下图进行编排。(或者使用右下角的导入功能，直接导入SuperBizAgent/internal/ai/cmd/knowledge_cmd/workflow.json)\n最后点击生成代码，插件会自动生成代码到你输入目标目录。\n📷 [图片 token=H3pWbLRi9ovVZVxcF8zcjVQ9nzf（未能下载，见飞书原文）]\n生成完后，会在目标目录看到生成出来的这些组件，下面我们来逐个介绍，注意看代码注释！\n📷 [图片 token=Y2g1bpH4gobL6Dxyr8gc2BfvnKf（未能下载，见飞书原文）]\n这部分代码在：SuperBizAgent/internal/ai/agent/knowledge_index_pipeline\n实战 注意，在运行代码之前，务必先看 [运行项目教程(Go)](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\n在运行之前，请在这个目录（SuperBizAgent/internal/ai/cmd/knowledge_cmd/docs）下，随便放几个markdown文件。\n向量数据库前端地址：http://localhost:8000/#/databases/agent/biz/data\nRunnable执行器 运行前与运行后 在运行之前，我们先进入向量数据库页面观察一下，可以看到现在是没有数据的。\n📷 [图片 token=LUWEbPfZuowdh7xDkZlcam3KnZb（未能下载，见飞书原文）]\n运行代码，将你docs目录下的md文件向量化到数据库中。\n# 运行代码 cd SuperBizAgent/internal/ai/cmd/knowledge_cmd go run main.go # 我的docs目录下有一个告警处理手册.md (base) ➜ knowledge_cmd git:(main) ✗ tree . ├── config │ └── config.yaml ├── docs │ └── 告警处理手册.md ├── main.go └── workflow.json 运行后，看到[done]就说明我们执行成功了。通过最后一行日志可以看到我们将文档切分成了5个分片。\n(base) ➜ knowledge_cmd git:(main) ✗ go run main.go [start] indexing file: docs/告警处理手册.md [view start]:[Graph::KnowledgeIndexing] {\u0026#34;URI\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;} [view start]:[Loader:FileLoader:] {\u0026#34;Source\u0026#34;:{\u0026#34;URI\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;},\u0026#34;Extra\u0026#34;:null} [view end]:[Loader:FileLoader:] [view start]:[DocumentTransformer:MarkdownHeaderSplitter:] [{\u0026#34;id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;# XXXX\u0026#34;,\u0026#34;meta_data\u0026#34;:{\u0026#34;_extension\u0026#34;:\u0026#34;.md\u0026#34;,\u0026#34;_file_name\u0026#34;:\u0026#34;告警处理手册.md\u0026#34;,\u0026#34;_source\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;}}] [view end]:[DocumentTransformer:MarkdownHeaderSplitter:] [view start]:[Indexer:Milvus:] {\u0026#34;Docs\u0026#34;:[{\u0026#34;id\u0026#34;:\u0026#34;1129d47f-7f1d-47f3-96f2-2f82444811ab\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;# 服务下线\u0026#34;,\u0026#34;meta_data\u0026#34;:\u0026#34;,\u0026#34;_source\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;服务下线\u0026#34;}},{\u0026#34;id\u0026#34;:\u0026#34;d5c0c309-4e7e-4728-ac65-9345fb5f0836\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;# 接口失败率过高\\n告警解释：接口失败率过高可能是由于服务调用异常或者下游服务不可用导致的\\n解决方案：\\n1. 根据接口名和\\\u0026#34;response\\\u0026#34;关键词进行sion\u0026#34;:\u0026#34;.md\u0026#34;,\u0026#34;_file_name\u0026#34;:\u0026#34;告警处理手册.md\u0026#34;,\u0026#34;_source\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;接口失败率过高\u0026#34;}},{\u0026#34;id\u0026#34;:\u0026#34;b243b1fa-de01-4f7c-8070-9bb19c5b45d4\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;# 与下游对账发现差异\\n告警解释：与下游对账发现差异可能是由于数据同步异常或者计算错误导致的\\据关键字\\\u0026#34;error\\\u0026#34;和\\\u0026#34;reconciliation\\\u0026#34;进行最近1小时的日志搜索\\n2. 根据查询到的日志内容分析是什么原因导致的对账差异\u0026#34;,\u0026#34;meta_data\u0026#34;:{\u0026#34;_extension\u0026#34;:\u0026#34;.md\u0026#34;,\u0026#34;_file_name\u0026#34;:\u0026#34;告警处理手册.md\u0026#34;,\u0026#34;_source\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;与下游对账发现差异\u0026#34;}},{\u0026#34;id\u0026#34;:\u0026#34;e不匹配\\n问题解释：在计费数据处理中，我们发现部分业务由于使用了错误的MQ队列把资源事件错投递到其他地域，造成资源所在地域与计费的服务地域不匹配。\\n解决方案：\\n1. 根据关键字\\\u0026#34;region mismatch\\\u0026#34;进行最近1小时的日志搜索\\n2. 根据查询到的日志内容，汇总出调用方与资源地域不匹配\u0026#34;}},{\u0026#34;id\u0026#34;:\u0026#34;fb9784cf-efeb-4af9-ab03-ce70af4a1958\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;# 服务错误码与常见原因\\n- 12000000001：调用接口参数错误（如:类型不匹配）\\n- 12000000002：数据库更新失败（数据库的问题，建议排查日志）\\n- 12000000003：下游接口错误（下游接口报\u0026#34;_extension\u0026#34;:\u0026#34;.md\u0026#34;,\u0026#34;_file_name\u0026#34;:\u0026#34;告警处理手册.md\u0026#34;,\u0026#34;_source\u0026#34;:\u0026#34;docs/告警处理手册.md\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;服务错误码与常见原因\u0026#34;}}],\u0026#34;Extra\u0026#34;:null} [view start]:[Embedding:Ark:ArkEmbedding] {\u0026#34;Texts\u0026#34;:[\u0026#34;# 服务下线\\nXXXXXX\u0026#34;],\u0026#34;Config\u0026#34;:{\u0026#34;Model\u0026#34;:\u0026#34;doubao-embedding-text-240715\u0026#34;,\u0026#34;EncodingFormat\u0026#34;:\u0026#34;float\u0026#34;},\u0026#34;Extra\u0026#34;:null} [view end]:[Embedding:Ark:ArkEmbedding] [view end]:[Indexer:Milvus:] [view end]:[Graph::KnowledgeIndexing] [done] indexing file: docs/告警处理手册.md, len of parts: 5 数据库前端页面也可以看到，我们存储了5行记录。\n📷 [图片 token=SpCUbZa0DowU88xhHnAcHmdznXe（未能下载，见飞书原文）]\n执行代码研究 好，执行完成后。我们来看看具体的代码实现是怎么样的。我们来重点看下面高亮的代码行：\n首先调用 knowledge_index_pipeline.BuildKnowledgeIndexing创建了一个runner执行器。\n调用runner的Invoke方法，入参是文件的地址。\nfunc main() { ctx := context.Background() r, err := knowledge_index_pipeline.BuildKnowledgeIndexing(ctx) if err != nil { return } err = filepath.WalkDir(\u0026#34;./docs\u0026#34;, func(path string, d fs.DirEntry, err error) error { /// fmt.Printf(\u0026#34;[start] indexing file: %s\\n\u0026#34;, path) // 重新构建 ids, err := r.Invoke(ctx, document.Source{URI: path}, compose.WithCallbacks(log_call_back.LogCallback(nil))) if err != nil { return fmt.Errorf(\u0026#34;invoke index graph failed: %w\u0026#34;, err) } fmt.Printf(\u0026#34;[done] indexing file: %s, len of parts: %d\\n\u0026#34;, path, len(ids)) return nil }) } BuildKnowledgeIndexing研究 我们重点来看一下第一个返回值，r compose.Runnable[document.Source, []string]，这个返回值代表返回一个可以执行的执行器。\n其中[document.Source, []string] 对应着下面的Runnable接口的[I, O any]。\n这是一个泛型，I代表Input，输入；O代表output输出。\n所以这里我们输入了 document.Source，其内部就是一个URI字段，存放路径。\n通过注释我们可以知道，Invoke方法就是直接输出的意思，Stream方法是流式输出的意思。\n总结一下BuildKnowledgeIndexing：返回一个执行器，这个执行器的入参是文件地址，出参是一个string切片。\nfunc BuildKnowledgeIndexing(ctx context.Context) (r compose.Runnable[document.Source, []string], err error) { /// } // Runnable is the interface for an executable object. Graph, Chain can be compiled into Runnable. // runnable is the core conception of eino, we do downgrade compatibility for four data flow patterns, // and can automatically connect components that only implement one or more methods. // eg, if a component only implements Stream() method, you can still call Invoke() to convert stream output to invoke output. type Runnable[I, O any] interface { Invoke(ctx context.Context, input I, opts ...Option) (output O, err error) Stream(ctx context.Context, input I, opts ...Option) (output *schema.StreamReader[O], err error) } type Source struct { URI string } 我们继续看看编排代码BuildKnowledgeIndexing的其他流程。\n里面有很多AddEdge，这里面的点、边连接顺序，其实就是上面我们用eino-dev插件编排的顺序。\n执行顺序：START -\u0026gt; FileLoader -\u0026gt; MarkdownSplitter -\u0026gt; MilvusIndexer -\u0026gt; END\n也就是说：BuildKnowledgeIndexing返回的执行器，在调用后会按照这个顺序执行。\n下面我们继续来看看每个节点(FileLoader,MarkdownSplitter,MilvusIndexer)具体做了什么？\nfunc BuildKnowledgeIndexing(ctx context.Context) (r compose.Runnable[document.Source, []string], err error) { // _ = g.AddEdge(compose.START, FileLoader) _ = g.AddEdge(FileLoader, MarkdownSplitter) _ = g.AddEdge(MarkdownSplitter, MilvusIndexer) _ = g.AddEdge(MilvusIndexer, compose.END) r, err = g.Compile(ctx, compose.WithGraphName(\u0026#34;KnowledgeIndexing\u0026#34;), compose.WithNodeTriggerMode(compose.AnyPredecessor)) if err != nil { return nil, err } return r, err } 文件加载-Loader组件 我们先来观察下默认代码和返回值，可以看到返回值是一个 document.Loader接口，这个接口需要实现Load方法。也就是说，我们需要在newLoader函数里面，去返回一个实现了Load方法的类。\n至于什么时候调用Load方法，框架编排后会自动帮我们调用。所以我们不需要考虑调用顺序的事情（因为你在编排graph的时候就包含了顺序），只需要考虑具体的功能实现\n// newLoader component initialization function of node \u0026#39;FileLoader\u0026#39; in graph \u0026#39;KnowledgeIndexing\u0026#39; func newLoader(ctx context.Context) (ldr document.Loader, err error) { // TODO Modify component configuration here. config := \u0026amp;file.FileLoaderConfig{} ldr, err = file.NewFileLoader(ctx, config) if err != nil { return nil, err } return ldr, nil } // Loader is a document loader. type Loader interface { Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error) } 这里我们使用官方默认实现的file loader（file.NewFileLoader）\n我们来看看 file loader 的Load方法怎么写的：\n打开文件：openFile\n记录文件元数据信息\n将文件内容读到内存中\n构造返回值，返回\nfunc (f *FileLoader) Load(ctx context.Context, src document.Source, opts ...document.LoaderOption) (docs []*schema.Document, err error) { // 1. 打开文件 file, err := openFile(src.URI) // 2. 记录文件元数据 name := filepath.Base(src.URI) ext := filepath.Ext(src.URI) meta := map[string]any{ MetaKeyExtension: ext, MetaKeyFileName: name, MetaKeySource: src.URI, } // 3. f.Parser.Parse对文件做了什么？实际上就是把文件从磁盘读到内存里，并构造返回的结构体 docs, err = f.Parser.Parse(ctx, file, append([]parser.Option{parser.WithURI(src.URI), parser.WithExtraMeta(meta)}, o.ParserOptions...)...) // 4. 返回 return docs, nil } // Parse reads the text from a reader and returns a single document. func (dp TextParser) Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) { // 3.1 首先从文件读出所有内容到内存中 data, err := io.ReadAll(reader) if err != nil { return nil, err } // 3.2 构建元数据 meta := make(map[string]any) meta[MetaKeySource] = opt.URI for k, v := range opt.ExtraMeta { meta[k] = v } // 3.3 构造返回值 doc := \u0026amp;schema.Document{ Content: string(data), MetaData: meta, } return []*schema.Document{doc}, nil } 文件分块-DocumentTransformer组件 我们依旧来观察一下默认的代码和返回值。返回值是document.Transformer接口，这个接口需要实现Transform方法。\n// newDocumentTransformer component initialization function of node \u0026#39;MarkdownSplitter\u0026#39; in graph \u0026#39;KnowledgeIndexing\u0026#39; func newDocumentTransformer(ctx context.Context) (tfr document.Transformer, err error) { config := \u0026amp;markdown.HeaderConfig{ Headers: map[string]string{ \u0026#34;#\u0026#34;: \u0026#34;title\u0026#34;, }, IDGenerator: func(ctx context.Context, originalID string, splitIndex int) string { return uuid.New().String() }, } tfr, err = markdown.NewHeaderSplitter(ctx, config) return tfr, nil } // Transformer is to convert documents, such as split or filter. type Transformer interface { Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error) } 我们进一步看看这个 markdown.NewHeaderSplitter的Transform方法是怎么写的\n将文件内容，按照标题#进行切分\n每个切分出来的分片，都赋予一个唯一的uuid\n对每个分片都增加元数据信息\n构造返回值，返回\nfunc (h *headerSplitter) Transform(ctx context.Context, docs []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) { var ret []*schema.Document for _, doc := range docs { // 1. 将文件按照标题#进行切分 result := h.splitText(ctx, doc.Content) for i := range result { // 2. 对每个分片，赋予唯一的uuid nDoc := \u0026amp;schema.Document{ ID: h.idGenerator(ctx, doc.ID, i), Content: result[i].chunk, MetaData: deepCopyAnyMap(doc.MetaData), } for k, v := range result[i].meta { nDoc.MetaData[k] = v } // 3. append ret = append(ret, nDoc) } } // 4. 返回所有分片 return ret, nil } func (h *headerSplitter) splitText(ctx context.Context, text string) []splitResult { /* 举个例子，假设有文本： # Title1 Content1 # Title2 Content2 分割后，第一个块包含\u0026#34;# Title1\u0026#34;和\u0026#34;Content1\u0026#34;。第二个块包含\u0026#34;# Title2\u0026#34;和\u0026#34;Content2\u0026#34; */ } 代码流程总结：将文档按照#来分块\n文件索引(向量化和存储到数据库)-Indexer组件 文档分块之后，就要对每个块进行embedding了。我们还是先来看一下代码，主要实现Index接口的Store方法\nfunc NewMilvusIndexer(ctx context.Context) (idr indexer.Indexer, err error) { config := \u0026amp;milvus.IndexerConfig{ Client: client.NewMilvusClient(ctx), Collection: embedder2.DoubaoEmbedding(ctx), Fields: fields, Embedding: eb, } indexer, err := milvus.NewIndexer(ctx, config) if err != nil { return nil, err } return indexer, nil } type Indexer interface { // Store stores the documents. Store(ctx context.Context, docs []*schema.Document, opts ...Option) (ids []string, err error) // invoke } 进一步查看 milvus.NewIndexer实现的Store方法：\n首先对所有分片进行向量化，获取向量数组\n构造符合milvus表记录的结构体。id、content、vector、metadata\n构造完记录后，插入到数据库中。最后返回所有的id\n// Store stores the documents into the indexer. func (i *Indexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) (ids []string, err error) { // 1. 调用embedding模型的能力，对所有分块进行向量化。获得向量数组vectors // embedding vectors, err := emb.EmbedStrings(makeEmbeddingCtx(ctx, emb), texts) // 2. 主要就是构成符合insert的结构体 // load documents content rows, err := i.config.DocumentConverter(ctx, docs, vectors) // 3. 将记录插入到数据库中 // store documents into milvus results, err := i.config.Client.InsertRows(ctx, i.config.Collection, io.Partition, rows) ids = make([]string, results.Len()) for idx := 0; idx \u0026lt; results.Len(); idx++ { ids[idx], err = results.GetAsString(idx) if err != nil { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] failed to get id: %w\u0026#34;, err) } } // 4. 返回主键id return ids, nil } func (i *IndexerConfig) getDefaultDocumentConvert() func(ctx context.Context, docs []*schema.Document, vectors [][]float64) ([]interface{}, error) { return func(ctx context.Context, docs []*schema.Document, vectors [][]float64) ([]interface{}, error) { for _, doc := range docs { em = append(em, defaultSchema{ // 列id：分块的id ID: doc.ID, // 列content：分块的正文 Content: doc.Content, Vector: nil, // 列Metadata：分块的元数据 Metadata: doc.metadata, }) texts = append(texts, doc.Content) } // build embedding documents for storing for idx, vec := range vectors { // 列vector：embedding出来的向量 em[idx].Vector = vector2Bytes(vec) rows = append(rows, \u0026amp;em[idx]) } return rows, nil } } 总结 到这里，提问前数据准备的三个流程就讲完了。其实代码实现并不难，核心是要搞懂这3个步骤里面都做了什么事情，以及代码是怎么讲流程串联起来的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%EF%BC%9ARAG%E4%BB%A3%E7%A0%81%E5%AE%9E%E6%88%981%28Go%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;UL28bV4v4of7LxxA60OcWLVbnvb\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"实战演练：RAG代码实战1(Go)"},{"content":"OncallAgent 的普通聊天是一条由 LangChain Agent 驱动的流式链路。后端不会先无条件执行 RAG，而是把知识检索、当前时间、渐进式 Skill 和当前用户已发现的 MCP 工具交给模型，由模型决定是否调用。最终答案、模型提供的推理元数据、工具生命周期和引用通过统一 SSE 合同发送给 Vue 前端。\n📷 [图片 token=Ih9AbC9jtoZLN2x6QZtcbNy9nrg（未能下载，见飞书原文）]\nSSE 的意义不只是“打字机效果”。它把一次长模型执行拆成可判别事件：客户端能知道当前收到的是答案字符、推理片段、工具启动、工具完成、引用、终态还是结构化错误。服务端同时把用户消息、完整助手消息、引用、推理和工具调用审计写入 SQLite，使流式临时状态最终回归持久事实。\n这条链路也有清晰边界：聊天流不是 durable job event log，断线后不能用 Last-Event-ID 从中间续传；前端在收到 complete 后会重新读取会话与审计，完成最终对账。流中失败可能已经把部分字符显示给用户，但不会把部分助手消息保存到历史。\n📷 [图片 token=VADtbfHfZoSTPBxBK3ycwJZxnTe（未能下载，见飞书原文）]\n学习目标 理解 create_agent、astream_events 到共享 SSE 的适配过程。\n掌握 SSE 的 type 判别字段和每类 payload。\n追踪 tool.call 的 stable ID、started、completed、failed 与 SQLite 审计。\n理解逐字符 content、推理、引用、complete 和 error 的顺序与持久化语义。\n识别浏览器解析、前端草稿、终态对账和断线恢复的实际边界。\n功能入口与完整调用链 前端 createChatClient.streamMessage 向 POST /chat/sessions/{session_id}/messages:stream 发送 JSON，并通过 createSseClient 加上 bearer token 和 Accept: text/event-stream。后端路由 stream_chat_message 先按当前 user.id 查询会话；会话不属于当前用户时，直接返回 HTTP 403，Agent 不会运行。\nChatStreamingService.stream_message 校验内容、读取历史、装配系统提示词和 Skills，并在上下文上限检查通过后先持久化用户消息。它构造 ChatAgentRequest，把 owner、session、模型上下文、授权知识库和配置交给 LangChainChatAgentRunner。\nRunner 创建当前请求专属工具集合，调用 langchain.agents.create_agent，再遍历 agent.astream_events(version=\u0026quot;v2\u0026quot;)。_agent_event_from_langchain_event 把 LangChain 原始事件转成内部 dataclass；服务进一步生成共享 SSE payload，路由用 encode_sse 编码为 event/data 帧。前端 store 按 event.type 更新草稿、推理、实时工具和引用，收到 complete 后重新读取会话与审计。\n📷 [图片 token=ESBfbjSeroBRQ3xkO7gcBX54nJh（未能下载，见飞书原文）]\nChatView → useChatStore.send → ChatClient.streamMessage → POST messages:stream → owner-scoped session check → ChatStreamingService 持久化 user message → LangChainChatAgentRunner → create_agent + astream_events v2 → 内部 Agent event → shared SSE payload → encode_sse → sseClient.parseSseFrames → Pinia 草稿状态 → complete 后重新加载 SQLite 会话与工具审计 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/chat/streaming.py ChatStreamingService、LangChainChatAgentRunner、encode_sse Agent 运行、事件适配、消息持久化和 SSE 编码。 apps/backend/src/super_ai/api/app.py stream_chat_message 先做 owner 会话鉴权，再返回 StreamingResponse。 packages/api-contracts/src/sse.ts SseEvent、SSE_EVENT_TYPES、ToolCallSseEvent 定义事件判别联合、channel 和各 payload。 packages/api-contracts/src/chat.ts StreamChatMessageRequest、ChatStreamCompleteResult、ToolCallAudit 定义请求、最终结果和持久审计 DTO。 apps/frontend/src/api/sseClient.ts createSseClient、parseSseFrames 认证 fetch、流式解码、frame 分割和基础事件校验。 apps/frontend/src/chat/chatClient.ts createChatClient 组合普通 HTTP 与聊天 SSE API。 apps/frontend/src/stores/chat.ts useChatStore.send、updateLiveToolCall、uniqueReferences 逐字渲染、实时工具、引用聚合和 complete 对账。 apps/frontend/src/views/ChatView.vue activeTitle、openCitationDocument、saveChatConfiguration 呈现会话、消息、工具、引用与输入状态。 apps/backend/tests/test_stream_rag_chat_api.py test_streaming_chat_emits_sse_events_and_persists_messages 端到端验证顺序、逐字符、持久化、审计与安全日志。 apps/frontend/tests/chatStore.test.ts reconciles streamed content, references, and tool calls with persisted history 验证临时流状态最终以服务端历史为准。 openspec/specs/stream-rag-chat/spec.md Chat stream event sequence 规定 Agent 决策工具、事件顺序、持久化与失败语义。 代码调用流程图 聊天链路包含两个事实面：SSE 负责即时过程，SQLite 保存完成后的消息和审计。complete 到达后，前端重新加载持久数据完成对账。\n关键实现拆解 从 LangChain 原始事件到稳定领域事件 **看什么：**看 Runner 如何把 LangChain v2 原始 event 收敛成项目内部 dataclass；未知框架事件不会直接穿透到共享 SSE。\n# 1. 每次请求独立消费 LangChain v2 事件协议。 async for raw_event in agent.astream_events( cast(Any, {\u0026#34;messages\u0026#34;: messages}), version=\u0026#34;v2\u0026#34;, ): # 2. 适配层把外部事件收敛为稳定的项目领域事件。 parsed = _agent_event_from_langchain_event(cast(Mapping[str, object], raw_event)) if parsed is None: continue if isinstance(parsed, list): for item in parsed: yield item else: yield parsed 适配器只识别工具开始、完成、失败以及模型内容/推理流；未知事件被忽略，不会迫使前端跟随 LangChain 内部命名变化。run_id 通常成为稳定 tool call ID，缺失时才生成新值；适配层没有生成 tool delta 状态。\n📷 [图片 token=CytCbRt34oYmhQxdpamcszU3nlk（未能下载，见飞书原文）]\nLangChainChatAgentRunner 每次请求创建知识检索工具和当前时间工具；选中 Skills 时加入 load_skill；配置了用户 MCP connection service 时，只加载该用户已启用且发现成功的工具。Runner 把 SQLite 消息转成 user/assistant 消息列表，并把最终 system prompt 传给 create_agent。普通聊天没有自定义 LangGraph 状态图。\n📷 [图片 token=KCAab5Frioh06WxNelUcjX7wnBb（未能下载，见飞书原文）]\n_agent_event_from_langchain_event 只处理明确事件：on_tool_start 生成 started，on_tool_end 生成 completed 并从工具输出抽取 citations，on_tool_error 生成 failed，模型或 model chain 的 stream 事件抽取 content 和 reasoning。工具调用 ID 来自 LangChain run_id；缺失时才生成新的 ID，因此开始和终态通常能用同一 ID 对账。\n📷 [图片 token=AMsSbpkD9oycZUxOxSJcqfk3nyd（未能下载，见飞书原文）]\n共享契约允许 tool status 为 started、delta、completed、failed，但当前 LangChain 适配器实际生成 started、completed 和 failed，没有生成 delta 的分支。契约预留能力不能被描述成已经存在的运行时行为。类似地，推理只从模型 chunk 的 reasoning_content 或 reasoning 读取；模型不提供时，服务不会合成“思考过程”。\nSSE 判别字段与事件粒度 **看什么：**先看模型内容 delta 在领域服务中如何被拆成单字符事件；sequence 同时被内容和 reasoning 共享，但工具与引用不带 sequence。\nasync for event in self._agent_runner.stream(request): if isinstance(event, ChatAgentContentDelta): if event.delta == \u0026#34;\u0026#34;: continue answer_parts.append(event.delta) # 1. 模型 delta 可能多字符，服务主动逐字符拆分。 for character in event.delta: sequence += 1 yield _sse_event( \u0026#34;content.delta\u0026#34;, { \u0026#34;delta\u0026#34;: character, \u0026#34;sequence\u0026#34;: sequence, }, ) 持久化答案使用未拆分的 answer_parts 拼接，因此字符动画不会改变最终正文。sequence 只在当前请求内递增，前端目前也不按 sequence 重排或补洞；它不是跨连接可重放的全局游标。\n**看什么：**再看每个领域事件如何补齐四个基础判别字段，并编码成标准 event/data/空行帧。\ndef encode_sse(event: Mapping[str, object]) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Encode a shared SSE event payload as one text/event-stream frame.\u0026#34;\u0026#34;\u0026#34; event_type = str(event[\u0026#34;type\u0026#34;]) # 1. event 行与 data 内 type 来自同一个值。 return f\u0026#34;event: {event_type}\\ndata: {json.dumps(event, separators=(\u0026#39;,\u0026#39;, \u0026#39;:\u0026#39;))}\\n\\n\u0026#34; def _sse_event(event_type: str, payload: Mapping[str, object]) -\u0026gt; dict[str, object]: # 2. 所有聊天事件共享身份、类型、channel 与时间戳。 return { \u0026#34;id\u0026#34;: f\u0026#34;evt_{uuid4().hex}\u0026#34;, \u0026#34;type\u0026#34;: event_type, \u0026#34;channel\u0026#34;: \u0026#34;chat\u0026#34;, \u0026#34;timestamp\u0026#34;: _now_iso(), **payload, } 前端 parser 实际忽略 event 行并信任 data JSON 的 type，所以后端必须保持二者一致。基础字段构成运行时最低守卫，逐类 payload 主要依靠共享 TypeScript 判别联合和后端契约测试，当前没有完整 runtime schema 验证。\n每个事件都有 id、type、channel、timestamp。聊天 channel 固定为 chat；type 是判别联合的核心。content.delta 和 reasoning.delta 带 delta 与 sequence；tool.call 带 toolCall；reference.source 带 reference；complete 带可选 result；error 复用统一 ApiErrorMessage。\n📷 [图片 token=OQiSbX7yAobokHx2btlcS7G5nLP（未能下载，见飞书原文）]\nAgent runner 可能一次给出多字符内容，但 ChatStreamingService 会遍历每个字符，逐一发出 content.delta。sequence 是服务内的递增计数，reasoning 事件也会推进它；工具和引用事件不带 sequence。答案字符被加入 answer_parts，推理加入 reasoning_parts，引用转成详细 payload，工具 ID 去重累积。\n📷 [图片 token=SBhNbrd3UorsDvxFJNYciBnAnXb（未能下载，见飞书原文）]\nencode_sse 同时写 event: {type} 和一行 JSON data。前端 parser 以空行切帧，拼接多条 data 行并 JSON.parse。运行时守卫目前只核对 id、type、channel、timestamp 是字符串，并没有逐类验证整个 payload；静态 TypeScript 联合提供编译期约束，后端测试负责契约形状。若将来面对不可信 SSE 源，应增加完整 runtime schema 校验。\n工具审计、引用和完成对账 **看什么：**看 tool event 在发 SSE 前先尝试写审计，以及审计失败为何被刻意隔离，不阻塞主答案。\ntry: if event.status == \u0026#34;started\u0026#34;: # 1. started 先创建 owner-scoped 审计，再允许上层发 SSE。 await repository.create_for_chat_session( owner_user_id=owner_user_id, audit_id=event.id, chat_session_id=session_id, tool_name=event.name, arguments=_json_dict_or_empty(event.input), ) return if event.status == \u0026#34;completed\u0026#34;: audit = await repository.finalize( owner_user_id=owner_user_id, audit_id=event.id, status=\u0026#34;completed\u0026#34;, result_summary=_audit_summary(event.output), ) if audit is None: # 2. 缺少 started 记录时补建并完成同一 ID。 await self._create_and_finalize_missing_audit( owner_user_id=owner_user_id, session_id=session_id, event=event, result_summary=_audit_summary(event.output), ) return # … 省略 failed 终态的脱敏摘要分支 except Exception: # 3. 审计是旁路可观察性，失败不抑制聊天输出。 return 审计和 SSE 不是同一事务，因此极端情况下 UI 看到了 tool.call 而历史审计缺项。相反，assistant message 是 complete 的主事实：只有答案、引用和工具 ID 成功写入 SQLite 后，服务才发 complete；引用从同一工具 output.citations 结构转换，不重新执行检索。\n服务在发出每个 ChatAgentToolCall 前先调用 _persist_tool_call_audit。started 创建 owner-scoped 审计，保存 session、tool name 和 arguments；completed 或 failed 以相同 ID finalize，保存有限结果摘要或脱敏错误摘要。若只收到终态而没有已有记录，服务会补建再完成。审计持久化异常被吞掉，以免抑制聊天输出；因此 SSE 生命周期和审计属于尽力对齐，而不是同一数据库事务。\n📷 [图片 token=VHjHbkyuMo2ZaBxH2q2cWz4pnkg（未能下载，见飞书原文）]\n知识检索工具完成时，适配器先发 completed tool event，再从 output.citations 转换为一个或多个 ChatAgentReference。reference 可携带 chunk、document、knowledge base、来源、metadata、excerpt、knowledgeType，以及 vector、BM25、RRF、rerank 分数与排名。前端 uniqueReferences 按 ID 去重，优先 rerankScore、其次 score 排序，最多保留 5 条。\n📷 [图片 token=MUX1bqxXLoegCgx9FTsc6Xc5n3l（未能下载，见飞书原文）]\n只有 Agent 迭代正常结束且助手消息成功写入 SQLite 后，服务才发 complete。complete.result 包含刷新后的 session 和记忆状态，以及真正持久化的 assistant message。前端把流中内容视为草稿，收到 complete 只标记 finished；循环结束后并行 loadSession 与 reloadSessions，用后端历史和工具审计替换临时状态。\n📷 [图片 token=JqoAbebGToyVQ0xTgxGcxRcknfb（未能下载，见飞书原文）]\n按时间线观察一次工具型回答 **看什么：**这张序列图展示一次可能的工具型回答，而不是规定所有请求都必须出现每一种事件。\n工具开始和终态通常用同一 run_id 对账，引用紧随包含 citations 的工具完成事件。纯聊天可跳过工具和引用，模型不提供 reasoning 就没有 reasoning.delta；前端必须按 type 分支，不能依赖固定位置。\n一次典型知识问答可能先产生 reasoning.delta，表示模型实际提供了准备检索的推理元数据；随后 on_tool_start 产生 tool.call started，审计记录在对应 SSE 发出前创建。知识工具完成时先出现 tool.call completed，接着是若干 reference.source。最后模型依据工具结果生成答案，服务把一个模型 chunk 拆成逐字符 content.delta，并在助手落库后发 complete。\n事件顺序由 Agent 实际运行决定，并非所有回答都必须包含上述类型。纯聊天可以只有 content.delta 和 complete；工具失败可能出现 started、failed，随后 Agent 选择继续回答，也可能让整个流进入 error；模型不返回 reasoning 时不会出现 reasoning.delta。前端因此必须按 type 独立处理，不能依赖固定数组位置或假设第一帧一定是内容。\n📷 [图片 token=FUVabNUoVoc4Izxo2dcc5IK2nag（未能下载，见飞书原文）]\nsequence 主要帮助保持内容与推理增量的顺序，但 SSE 基于单一 HTTP 流，本身已经按字节顺序到达。当前前端并没有检查 sequence 连续性或重排事件；测试确保服务生成递增值。若未来引入并行模型通道或断点续传，sequence 的范围和去重语义需要进一步定义，不能直接把当前请求内计数当作全局事件编号。\n前端草稿为何还要终态刷新 **看什么：**看 Pinia store 如何把流式事件写入临时草稿，却把 complete 仅当作完成标志；流结束后仍以服务端 session 和消息列表为准。\nlet finished = false; for await (const event of client.streamMessage(targetSessionId, { content })) { if (event.type === \u0026#34;content.delta\u0026#34;) { // 1. 后端虽已逐字符，前端仍对 delta 做兼容遍历。 for (const character of event.delta) { updateAssistantDraft(targetSessionId, draftId, character, messages); await waitForTypewriterTick(); } } // … 省略 reasoning.delta 分支 if (event.type === \u0026#34;reference.source\u0026#34;) { references.value = uniqueReferences([...references.value, event.reference]); } if (event.type === \u0026#34;tool.call\u0026#34;) { updateLiveToolCall(event.toolCall, liveToolCalls); } if (event.type === \u0026#34;error\u0026#34;) { throw new ApiClientError(event.error); } if (event.type === \u0026#34;complete\u0026#34;) { // 2. complete 只标记流完整，随后仍重新读取后端事实。 finished = true; } } if (!finished) { throw new Error(\u0026#34;回答流在完成前意外中断。\u0026#34;); } await Promise.all([loadSession(targetSessionId), reloadSessions()]); 原逻辑表明草稿 ID、owner 和字符节奏都不是持久事实。若流中断或 error，draft 会被删除；重新读取后，引用和工具审计也从持久消息与审计 API 对账。\n📷 [图片 token=YPJcbbuCiot1qNxbGtuceyJfnRf（未能下载，见飞书原文）]\nPinia store 在发送前创建 optimistic user message，ownerUserId 临时写为 current，ID 带 optimistic 前缀。助手草稿也使用本地 draft ID。它们只用于即时界面，真正的 message ID、owner、createdAt、完整 metadata 都由服务端 SQLite 记录决定。complete 到达后重新加载，能消除本地 ID、字符节奏和服务端最终持久化之间的差异。\nreferences 在流中独立维护，assistant draft 的 metadata 初始 citations 为空。最终 reload 后，setReferencesFromMessages 从最近一条持久化 assistant message 重新构造引用。工具调用同样分为 liveToolCalls 与 toolAudits：前者服务进行中状态，流结束后清空；后者来自审计 API，可在重新打开会话时恢复。\n📷 [图片 token=LxUQb4w2LoOHakxUnt8cXwGqnab（未能下载，见飞书原文）]\n逐字符效果有两层。后端已经保证每个 content.delta 只有一个字符；前端仍对 event.delta 再遍历字符，并在每个字符后等待 28 毫秒。这使测试注入多字符事件时仍能平滑显示，也为契约演进提供容错。代价是长回答在网络已结束后仍可能等待 UI 动画；store 只有消费完全部字符后才处理后续事件，所以视觉 complete 可能晚于服务端完成时间。\n错误发生在不同阶段时会留下什么 **看什么：**这张状态图把错误发生点与 SQLite 中可留下的消息分开，重点观察 user message 持久化前后的边界。\n鉴权失败是普通 HTTP 错误；准备阶段失败是 SSE error 且不写 user message。进入 Agent 后失败会保留 user message但不保留部分 assistant，前端见过的草稿会被删除；只有 assistant 写入成功的路径能够发 complete。\n鉴权失败发生在 StreamingResponse 建立前，客户端收到普通 HTTP 403 和统一错误 envelope，不会进入 SSE parser。空消息或 95% 上下文硬上限则由 stream service 生成 error SSE；此时没有用户消息落库。用户消息成功保存后，Agent 或工具再失败，错误作为 SSE 发出，历史保留用户问题。\n如果已经输出部分答案后发生异常，浏览器先看到若干 content.delta，再看到 error。服务不会创建 assistant message，前端 catch 会删除 draft。重新加载会话时只剩 user message。这种行为避免把不完整输出长期当成正式答案，但用户短暂看到过的内容无法从屏幕体验中“撤销已阅读”，所以 UI 必须明确标出失败，而不能只悄悄消失。\n更晚的失败是 assistant persistence 自身出错。即使模型已经完整返回，append_message 失败仍会进入 error，绝不发 complete。相反，如果工具审计写入失败，服务刻意继续 SSE 和答案持久化，因为审计是旁路可观察性；最终审计列表可能缺项。这两个选择体现了主事实优先级：正式 assistant message 是 complete 的前提，审计不是。\n📷 [图片 token=JE3KbzwSnolv7ixMHB0c1xp9nrc（未能下载，见飞书原文）]\n传输协议的细节与限制 **看什么：**看浏览器端如何用 streaming TextDecoder 保留半个 UTF-8 字符或半帧，并只在空行边界解析完整 SSE frame。\nconst reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = \u0026#34;\u0026#34;; for (;;) { const { done, value } = await reader.read(); if (done) { break; } // 1. streaming decode 避免网络 chunk 截断 UTF-8 字符。 buffer += decoder.decode(value, { stream: true }); const parsed = parseSseFrames(buffer); buffer = parsed.remainder; yield* parsed.events; } buffer += decoder.decode(); const parsed = parseSseFrames(buffer); yield* parsed.events; } }; } function parseSseFrames(buffer: string): { readonly events: readonly SseEvent[]; readonly remainder: string; } { const frames = buffer.split(/\\r?\\n\\r?\\n/); // 2. 最后一段不完整 frame 留给下一次读取。 const remainder = frames.pop() ?? \u0026#34;\u0026#34;; 客户端用 fetch 而非 EventSource，以便发送 POST JSON 和 Authorization header；当前没有自动重连、Last-Event-ID、心跳或主动 AbortController，可靠恢复依赖 complete 后重新读取持久会话，而不是 SSE 帧重放。\n后端设置 media_type=\u0026quot;text/event-stream\u0026quot; 和 Cache-Control: no-cache。每帧包含 event 行、data 行和空行。data JSON 使用紧凑分隔符，非 ASCII 默认会被 JSON 转义，但前端 JSON.parse 后恢复原字符。事件 id 是随机 UUID 前缀，不与 message ID、tool ID 或 sequence 相同。\n前端 decoder 使用 streaming 模式处理 UTF-8 字节被网络 chunk 截断的情况，buffer 保留不完整 frame。HTTP 非成功状态先走普通错误解析；成功但 response.body 为空映射为系统不可用。frame JSON 无效或缺少四个基础字段时，统一转成系统内部错误。parser 忽略 event 行，仅信任 data 内的 type，因此后端必须确保二者一致。\n📷 [图片 token=W51GbZCAMoCxPAxNWMQck6Mqn4d（未能下载，见飞书原文）]\n浏览器端使用 fetch 读取 ReadableStream，而不是原生 EventSource，因为请求需要 POST JSON 和 Authorization header。当前客户端没有主动取消控制器、心跳帧、自动重连或空闲超时逻辑；这些能力依赖浏览器和上游连接行为。把 SSE 作为协议选择不自动获得可靠重放，持久化 complete 后的会话读取才是现有恢复路径。\nAgent 工具集合与事件可见性 **看什么：**看请求级 Runner 如何装配固定工具、选中 Skill 和 owner-scoped MCP 工具；前端不能通过请求体直接指定任意工具名。\nlangchain_tool = create_langchain_knowledge_retrieval_tool( self._retrieval_tool, owner_user_id=request.owner_user_id, accessible_knowledge_base_ids=request.accessible_knowledge_base_ids, ) # 1. 知识检索与当前时间始终进入普通聊天工具集合。 tools = [langchain_tool, create_current_time_tool()] if request.skills: tools.append(create_load_skill_tool(request.skills)) mcp_client = self._mcp_client if self._mcp_client_provider is not None: # 2. MCP client 由当前 owner 的连接服务提供。 mcp_client = await self._mcp_client_provider.client_for_user( owner_user_id=request.owner_user_id ) if mcp_client is not None: await mcp_client.discover_tools() tools.extend(await mcp_client.get_langchain_tools()) agent = _create_langchain_agent( model=cast(Any, self._llm_provider.create_chat_model()), tools=tools, system_prompt=(request.system_prompt), ) MCP 工具必须真实发现成功才会注册，Skill 工具也只在本次请求有选中 Skills 时出现。所有 LangChain 工具共享同一 started/completed/failed 适配路径；只有输出中实际含 citations 的工具才产生引用，provider 不提供 reasoning 时也不会合成推理事件。\n知识检索和当前时间工具始终加入普通聊天 Agent；load_skill 只有选择了 Skill 时加入。MCP 工具来自当前 owner 的 connection service：先创建用户 client，再真实 discover，之后转成 LangChain tools。发现失败不会伪造工具定义。模型看到的工具集合因此由请求身份和服务器配置共同决定，而不是前端提交任意工具名。\n📷 [图片 token=XmMZbjOwComlwmxrfefct4mrnBg（未能下载，见飞书原文）]\n所有 LangChain 工具共享 on_tool_start、on_tool_end、on_tool_error 适配路径，所以知识、时间、Skill 与 MCP 都使用同一种 tool.call SSE 和审计生命周期。适配器并不按工具名称限制引用：任意工具输出只要包含可解析的 citations 数组，就会额外产生 reference.source；当前典型来源是知识检索。普通时间工具的现有输出不含 citations，因此不会凭空生成引用。load_skill 的完整正文也被压缩成首行 summary 再放入 UI 可见 output，减少敏感指令正文直接展示。\n工具 input 和 output 可以通过实时 SSE 到达前端，审计也保存 arguments 与有限摘要。应用日志规则更严格，不记录完整工具参数值或输出。前端组件应以折叠摘要展示过程，不能把任意原始对象直接当 HTML 渲染；聊天 Markdown 组件另有安全渲染测试，引用与工具结果走结构化组件。\n📷 [图片 token=BLawb3B4nomdGDxDzMfcjkEOn6d（未能下载，见飞书原文）]\nreasoning.delta 的处理同样强调真实来源。适配器支持 OpenAI-compatible chunk 的 additional_kwargs，并从明确字段取值；服务只累积实际返回的文本。推理随 assistant metadata 持久化，可在重开会话后显示，但它与最终答案内容分离。若 provider 不提供推理，界面应没有该区块，而不是使用模板文案假装模型进行过某些思考。\n数据、契约与状态 助手消息 metadata 保存 citations、reasoning、toolCallIds。最终正文只由 content delta 拼接，reasoning 不混入回答。工具输出在 SSE 中可以是结构化 object，但审计只保存最多 2000 字符的 JSON 摘要；前端历史读取的工具详情来自单独 GET /chat/sessions/{session_id}/tool-call-audits。\n前端发送时先乐观追加 user message，再为 assistant 创建 draft ID。每个内容字符之间等待 28 毫秒形成可感知的打字机节奏；reasoning 追加到 draft metadata；tool.call 按 ID 合并状态；reference 单独显示。没有 complete 即使网络正常结束也被当作异常，草稿会被移除并显示统一错误。\n权限、安全与失败边界 路由在构造服务前按 owner_user_id 读取 session，跨用户访问返回 403 且 runner.requests 保持为空。知识检索工具与 MCP client 也绑定同一 owner。用户消息在 Agent 执行前写入；如果模型或工具失败，历史会保留这条 user message，帮助用户知道哪次请求失败，但不保存部分 assistant message。\n流中异常由 _error_event 转换为统一错误目录中的安全结构。模型已经产生的字符可能先到达浏览器，随后才出现 error；测试明确覆盖这种情形。错误响应不会把 provider 的 sk- 密钥带入 SSE。工具审计的失败摘要额外对常见 sk- 和 AKID 形态做替换，但这不是任意秘密格式的完整检测。\n聊天 SSE 没有持久事件序列、重放 API 或 Last-Event-ID 处理。断线后的恢复方式是重新读取已完成的 SQLite 会话；尚未完成且未持久化的 assistant 草稿无法续传。不要把 AIOps durable job 的事件恢复能力外推到普通聊天。\n📷 [图片 token=Wq54bXZzNoUSg0xRdgjcn96CnX8（未能下载，见飞书原文）]\n阅读顺序与小结 先读 sse.ts，把 type 当作整个协议的主键。\n再读 _agent_event_from_langchain_event，理解外部框架事件如何被收敛。\n随后跟进 ChatStreamingService.stream_message 的持久化与发流顺序。\n最后阅读 sseClient 与 chat store，确认浏览器中的流式草稿如何回归服务端持久事实。\n可靠的流式 Agent 不是简单地把 token 推到浏览器。OncallAgent 用判别式 SSE 建立过程协议，用 owner-scoped SQLite 保存最终事实，用工具审计和引用连接过程与证据，并在 complete 后让前端重新对账。它既提供即时体验，也明确承认普通聊天流尚不具备事件重放能力。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/09.%20LangChain%20%E6%B5%81%E5%BC%8F%20Chat%20Agent%20%E4%B8%8E%E5%89%8D%E7%AB%AF%20SSE/","summary":"OncallAgent 的普通聊天是一条由 LangChain Agent 驱动的流式链路。后端不会先无条件执行 RAG，而是把知识检索、当前时间、渐进式 Skill 和当前用户已发现的 MCP 工具交给模型，由模型决定是否调用。最终答案、","title":"09. LangChain 流式 Chat Agent 与前端 SSE"},{"content":"上一章你已经搞清楚了大模型的底层逻辑：它是一个超级助理，你通过 messages 列表把内容传给它，它根据你给的上下文预测并生成回答。\n但这里有个现象你可能早就发现了：同一个大模型，不同的人用，结果天差地别。有人问出来的答案精准、稳定，拿来就能用；有人问出来的东西东拉西扯，废话连篇，完全不符合预期。\n差在哪里？差在你怎么跟它说话。\n这件事有个专门的名字，叫做 Prompt（提示词）。你写给大模型的那些文字，你的指令、你的问题、你给的背景、你的格式要求，这一切统称 Prompt。这一章，就是专门讲怎么把这件事做好。\n什么是 Prompt Prompt 就是你发给大模型的所有输入内容，不只是一句简单的问题，而是你塞进 messages 列表里的所有文字：指令、背景信息、参考资料、示例、格式要求……全部算在内，总称 Prompt。\n📷 [图片 token=DGaqbg8HVoebcsxRgaLcwEk8nUb（未能下载，见飞书原文）]\n回想上一节中的 messages 结构：\nmessages = [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一个 OnCall 助理，回答必须简洁\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;昨晚数据库报警是什么原因？\u0026#34; }, ] 这里的每一个字，system 里的那句规则，user 里的那个问题，都是 Prompt 的一部分。\n用点菜来类比：你去餐厅，跟服务员说的那句话就是 Prompt。你说「随便来一份」，端上来的菜纯靠运气；你说「来一份不辣、不放葱、多加豆腐的麻婆豆腐」，端上来的才是你真正想要的。Prompt 就是这句\u0026quot;点菜的话\u0026quot;，你说得越清楚，大模型给你的结果越符合预期。\n很多初学者以为 Prompt 就是\u0026quot;问一个问题\u0026quot;，实际上它包含的范围要宽得多：角色设定、行为规则、背景信息、参考材料、输出格式……所有你给模型的输入，都是 Prompt。\n为什么 Prompt 很重要？ 要理解 Prompt 的重要性，得先想起大模型的本质：它是个 next-token 预测机器，根据上下文预测\u0026quot;接下来最可能出现什么词\u0026quot;。\n你给的上下文越模糊，它能\u0026quot;往哪里走\u0026quot;的方向就越多，输出越随机。你给的上下文越精准，它的搜索范围越窄，输出越稳定、越可预期。\n📷 [图片 token=TZpWbHDK7orX27xQJNAcWazlnMd（未能下载，见飞书原文）]\n直接看对比，同样是让大模型帮你总结一段内容：\n❌ 「帮我写个总结」：大模型不知道总结什么、给谁看、多长、什么风格，它只能乱猜。运气好凑对了，运气不好答非所问。\n✅ 「用 3 句话，为以下技术文档写一段面向非技术管理者的摘要，重点突出业务影响」目标明确、受众明确、格式明确、侧重点明确，输出稳定且可重复。\n这个差距，在平时\u0026quot;用用 AI\u0026quot;的场景下只是体验好不好的问题；但在 Agent 开发里，性质完全不同。\nAgent 里，大模型的每次输出都要被你的程序解析和使用。输出稳定才能可靠解析，解析可靠才能触发正确的下一步动作。一个不稳定的 Prompt，会导致整条 Agent 流水线频繁出错。\n所以 Prompt 不是什么\u0026quot;小技巧\u0026quot;，它是 Agent 开发的地基。地基没打好，上面盖再复杂的楼都是危房。\nUser Prompt，你说给模型的话 对应 messages 里 role: \u0026quot;user\u0026quot; 的部分，就是你每次发给大模型的具体内容：你的问题、你的指令、你要它处理的原始材料（日志、文档、代码……）。\nGPT发布初期，用户通过聊天框发送消息（user prompt）与大模型交互，但大模型缺乏人设，回复通用且仅能聊天，无法执行任务（比如上传PDF让它解析成中文再返回给你）。\n📷 [图片 token=LFogbd5RdodhACxAN0FcvyO4nhc（未能下载，见飞书原文）]\n好的 User Prompt 有三个要素：\n目标明确：要做什么？分析、总结、改写、生成代码，得明说。\u0026ldquo;帮我看看这个日志\u0026quot;不是目标，\u0026ldquo;帮我分析这段日志里的报错原因并给出排查方向\u0026quot;才是。\n背景充足：大模型不知道你的业务、你的系统架构、你的团队惯例，它只能靠你给的信息推断。背景给得越充分，它越不需要乱猜，幻觉越少。\n输出要求：格式、长度、风格，不说，它随心所欲。你想要 Markdown 列表？想要 JSON？想要 3 条以内？一定要在 Prompt 里讲清楚。\n来看一个 OnCall 场景的对比：\n❌ 简单版：「数据库怎么了？」\n大模型不知道你说的是哪个数据库、什么时间发生的、有什么报警信息、你想要什么样的答案。\n✅ 完整版：\n以下是今天凌晨 2 点的数据库报警日志： [日志内容] 请分析可能的根本原因，并给出 3 条排查建议，以 Markdown 列表格式输出。 两个 Prompt，你自己体会差距在哪。\n三个要素缺一都会影响结果，目标不明确，模型不知道要做什么；背景不充分，模型只能靠猜；输出要求没有，模型随意发挥，你还得二次整理。\nSystem Prompt，给模型的\u0026quot;规则手册\u0026rdquo; System Prompt 是什么？ 就是在对话开始之前，开发者写好的一段\u0026quot;底层指令\u0026rdquo;。它出现在 messages 列表的最前面，用户看不见它，但模型每次对话都会\u0026quot;记在心里\u0026quot;，严格遵守。\n为给大模型加上人设，将人设信息从user prompt中单独拎出形成system prompt。用于描述大模型的角色、性格等非用户直接表达的内容。\n📷 [图片 token=EwHAbWb6Qoj8VXxgvzLckCSdnjb（未能下载，见飞书原文）]\n每次用户发送 user prompt，系统自动将 system prompt 一起发给AI模型，使对话更自然\n📷 [图片 token=Ll6rbo6nYorNHGxAErmcBVFqnVh（未能下载，见飞书原文）]\nSystem Prompt 有三个核心用途：\n身份设定：给模型一个具体的角色 你是一个 OnCall 助理，专门帮助工程师排查和分析系统故障。 你只处理与系统稳定性、报警分析、故障排查相关的问题。 行为规则：定义模型\u0026quot;能做什么、不能做什么\u0026quot; 不允许回答与系统故障无关的话题。 如果你不确定某个判断，必须说明\u0026#34;我不确定，建议进一步验证\u0026#34;。 不要自行假设任何背景信息，只根据用户提供的内容作答。 输出格式约束：让模型每次都按固定格式输出 你的回答必须是 JSON 格式，包含以下字段： summary：对问题的一句话概括 root_cause：可能的根本原因列表 action_items：建议的排查步骤列表 把三者合在一起，就是一个 OnCall Agent 的完整 System Prompt：\n你是一个专业的 OnCall 助理，帮助工程师分析系统故障和报警。\n你是一个专业的 OnCall 助理，帮助工程师分析系统故障和报警。 【行为规则】 - 只回答与系统故障、报警分析相关的问题，其他话题不处理 - 只根据用户提供的信息作答，不要自行补充假设的背景 - 如果信息不足以判断原因，明确说明还需要哪些信息 【输出格式】 每次回答必须是如下 JSON 格式： { \u0026#34;summary\u0026#34;: \u0026#34;一句话概括问题\u0026#34;, \u0026#34;root_cause\u0026#34;: [\u0026#34;可能原因1\u0026#34;, \u0026#34;可能原因2\u0026#34;], \u0026#34;action_items\u0026#34;: [\u0026#34;排查步骤1\u0026#34;, \u0026#34;排查步骤2\u0026#34;, \u0026#34;排查步骤3\u0026#34;] } 为什么 System Prompt 比 User Prompt 更\u0026quot;稳\u0026quot;？ User Prompt 每次都不一样，但 System Prompt 每次对话都在，每次都一样。它是 Agent 行为可预测的核心保障。\n用职场来类比：System Prompt 是给员工写的\u0026quot;岗位职责说明书\u0026quot;，User Prompt 是\u0026quot;每天的具体工作任务\u0026quot;。说明书确定了员工的边界和工作方式，任务单在这个框架下执行。两者缺一不可，但说明书是更基础的那个。\n怎么写好 Prompt，6 个核心原则 理解了是什么之后，来看怎么做。以下 6 个原则，每一条都有其背后的道理，不是规则背诵，是能说清楚\u0026quot;为什么这样有效\u0026quot;的方法。\n📷 [图片 token=HyasbzqahoZG9VxgBzoc8GqQnib（未能下载，见飞书原文）]\n原则 1：给模型\u0026quot;角色\u0026quot;，效果立刻不同 ❌ 「分析一下这个系统报错」\n✅ 「你是一个资深 SRE 工程师，专门排查分布式系统故障，请分析以下报错」\n加了角色之后，效果为什么会更好？不是玄学。\n大模型在训练时见过海量各行各业的文字，\u0026ldquo;资深 SRE 工程师写的内容\u0026quot;和\u0026quot;普通人随便聊的内容\u0026rdquo;，在语言风格、专业度、思考方式上差别很大。你指定了角色，就相当于告诉模型\u0026quot;往那个方向走\u0026quot;，缩小了 next-token 预测的搜索空间，输出自然更专业、更贴合你的期望。\n原则 2：正向约束 \u0026gt; 负向禁止 ❌ 「不要太啰嗦」→ \u0026ldquo;啰嗦\u0026quot;是主观的，模型不知道你的标准在哪\n✅ 「回答控制在 3 句话以内」→ 明确，模型知道怎么执行\n告诉模型\u0026quot;要做什么\u0026rdquo;，比告诉它\u0026quot;不要做什么\u0026quot;更有效。原因很直接：模型是在\u0026quot;生成\u0026quot;内容，知道要生成什么比知道不要生成什么更容易执行。负向的禁止在某些场景有用，但大多数情况下，直接说你想要的结果，效果更稳定。\n原则 3：喂给它足够的背景信息 大模型不知道你的业务逻辑，不知道你的系统架构，不知道这条报警对你的团队意味着什么。它只能靠你在 Prompt 里提供的信息来推断。\n❌ 「帮我分析问题」→ 什么问题？\n✅ 「以下是系统日志，背景是我们的 MySQL 集群在高峰期（每天 12:00-14:00）出现连接超时，集群规模是 3 主 6 从，请分析可能原因：[日志内容]」\n信息给得越充分，答案越靠谱，幻觉越少。大模型的\u0026quot;幻觉\u0026quot;有很大一部分来自\u0026quot;信息不足时的强行推断\u0026quot;，你给够了信息，它就不需要猜了。\n原则 4：指定输出格式（Agent 开发的重中之重） 这一条在 Agent 开发中尤其关键，单独强调。\nAgent 需要程序来解析大模型的输出，然后决定下一步做什么。如果输出格式每次不一样，解析代码就会频繁出错。所以你必须明确告诉模型\u0026quot;以什么格式输出\u0026quot;：JSON、Markdown 列表、固定字段……\n光说\u0026quot;输出 JSON\u0026quot;还不够稳定。最可靠的方式是在 Prompt 里直接给一个 JSON 示例：\n请按以下 JSON 格式输出，不要输出其他内容：\n{ \u0026#34;severity\u0026#34;: \u0026#34;high/medium/low\u0026#34;, \u0026#34;summary\u0026#34;: \u0026#34;一句话概括\u0026#34;, \u0026#34;actions\u0026#34;: [\u0026#34;步骤1\u0026#34;, \u0026#34;步骤2\u0026#34;] } 有了示例，模型的格式几乎不会跑偏。\n另外，这一条直接铺垫了我们后面要学的 Function Calling：让大模型按照指定格式输出\u0026quot;要调用哪个工具、传什么参数\u0026quot;，本质上就是在做精确的格式约束，先理解这一条，Function Calling 的原理你就秒懂了。\n原则 5：Few-shot，给几个例子，胜过写一堆规则 Few-shot 是指：在 Prompt 里直接给几组\u0026quot;输入→输出\u0026quot;的示范样本，让模型照着这个模式来做。\n❌ 写一大段规则描述：\u0026ldquo;回答时要简洁、专业、聚焦根因、避免技术术语过多……\u0026rdquo;\n✅ 直接给 3 个样本，每个样本是一条报警 + 对应的分析结论，然后让模型对第 4 条做同样的分析\n与其写一本厚厚的\u0026quot;员工行为手册\u0026quot;，不如直接给他看 3 份\u0026quot;合格工作成果的样本\u0026quot;，哪个更直接，一目了然。\n什么时候用 Few-shot：当你要模型输出特定风格、特定格式、特定思维方式的时候，用样本比用文字规则更直接、更有效。尤其是当你发现光靠描述说不清楚\u0026quot;我想要什么\u0026quot;时，直接给例子。\n原则 6：别急着要答案，先让模型把思路捋清楚 碰到复杂问题，如果一上来就让模型直接给结论，它很容易漏掉条件，或者中间想当然。可以在 Prompt 里加一句：\n先梳理关键条件和分析步骤，最后再给出结论。\n这样做不是什么玄学。大模型本来就是一个词一个词往下生成的，前面先把条件和逻辑关系理清楚，后面的回答自然更容易接得上，也不容易突然跑偏。\n这就像学生做数学题：只写最终答案，很可能算错了都不知道；把解题步骤写出来，哪里出了问题一眼就能看见。真正有用的不是「步骤」这两个字，而是梳理步骤的过程。\nPrompt 的迭代与调试 Prompt 没有\u0026quot;一次写好\u0026quot;这件事。写完之后必须测试，测完之后根据结果调整，这个过程跑几轮是正常的。\n基本节奏：多准备几个典型输入（覆盖正常情况、边界情况、你最担心出错的情况），每次修改后都跑一遍，看输出是否稳定、是否符合预期。\n常见问题排查：\n输出格式乱、每次不一样 → 在 System Prompt 里加更严格的格式约束，或者在 Prompt 里直接给 JSON 示例\n回答跑偏、答非所问 → 检查角色设定是否清晰，行为规则是否有遗漏，背景信息是否给足\n输出不稳定，时好时坏 → 先优化 Prompt，不要上来就换模型。大多数\u0026quot;输出不稳定\u0026quot;的问题，根源在 Prompt 写得不够精确\n一个重要原则：每次只改一个变量，测多个 case，观察稳定性。如果你一次性改了角色设定、格式要求、背景信息三件事，输出变了，你也不知道是哪个起了作用，下次遇到同类问题还是无从下手。\n总结，Prompt 是 Agent 的控制面板 在 Agent 开发中，你能控制大模型行为的核心手段只有一个：Prompt。\nSystem Prompt 定义了 Agent 的\u0026quot;身份和边界\u0026quot;，它是谁、能做什么、不能做什么、必须以什么格式输出\nUser Prompt 和注入的内容是 Agent 的\u0026quot;任务输入\u0026quot;，每次要处理的具体数据、具体问题\nPrompt 学到位了，后续章节里所有技术的\u0026quot;为什么要这么写\u0026quot;你都能看明白：\nFunction Calling：本质是让大模型按精确格式输出\u0026quot;工具调用指令\u0026quot;，靠的是严格的格式约束 Prompt\nRAG：检索到的知识片段要注入到 Prompt 里，怎么组织这段内容，直接影响模型能不能用对信息\nAgent：多步骤任务的\u0026quot;决策逻辑\u0026quot;、\u0026ldquo;工具使用规范\u0026rdquo;、\u0026ldquo;异常处理方式\u0026rdquo;，全写在 System Prompt 里\n一句话：大模型的能力是固定的，Prompt 决定你能调动它多少。写好 Prompt，是用好大模型的前提，是 Agent 开发的基础功。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AFPrompt%EF%BC%9F/","summary":"上一章你已经搞清楚了大模型的底层逻辑：它是一个超级助理，你通过 messages 列表把内容传给它，它根据你给的上下文预测并生成回答。  但这里有个现象你可能早就发现了： 同一个大模型，不同的人用，结果天差地别 。有人问出来的答案精准、稳定","title":"什么是Prompt？"},{"content":" 📷 [图片 token=CB09bMdC5oFLi6xqxnucL7Xinmf（未能下载，见飞书原文）]\n📷 [图片 token=NuHhbFYeuoW8PExUYK9ctiNRnN2（未能下载，见飞书原文）]\n注意，运行程序之前请先看：\n[环境准备教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/环境准备教程/)\n[运行项目教程](/oncall/智能 OnCall Agent 项目/第八章 _ 实战演练与运行项目/运行项目教程(Go)/)\n前言 上一节我们实现了知识库Agent的上半部分，这一节我们来实现知识库的召回功能。\n核心代码：SuperBizAgent/internal/ai/cmd/recall_cmd/main.go\n📷 [图片 token=NQ6zblccCoZOQOxomXkc6H2bnbg（未能下载，见飞书原文）]\n实战 观察代码，首先创建retriever组件，然后调用它的Retrieve方法进行查询，最后打印召回的内容。\n我们直接先运行代码，来看看执行后的效果。\nfunc main() { ctx := context.Background() r, err := retriever2.NewMilvusRetriever(ctx) if err != nil { panic(err) } query := \u0026#34;服务下线是什么原因\u0026#34; docs, err := r.Retrieve(ctx, query) if err != nil { panic(err) } fmt.Println(\u0026#34;Q：\u0026#34;, query) for _, doc := range docs { fmt.Println(\u0026#34;A：\u0026#34;, doc.Content) } } 代码路径：SuperBizAgent/internal/ai/cmd/recall_cmd/main.go\n通过输出可以看到，确实召回了我们上一节上传的文件内容。下面我们就来看看核心组件Retrieve到底做了什么。\n(base) ➜ recall_cmd git:(main) ✗ go run main.go Q： 服务下线是什么原因 A： # 服务下线 告警解释：服务下线可能因为服务panic，导致pod重启造成的 解决方案： 1. 根据关键字\u0026#34;panic\u0026#34;进行最近1小时的日志搜索 2. 根据panic日志内容分析是什么bug导致的panic 召回-Retriever组件 我们之前是将文档存储到了Milvus向量数据库里面，所以召回的时候也是从这个数据库去查询。\n首先我们对Milvus客户端进行一些配置，指定向量字段为vector，需要返回的字段有id、content、metadata。\nfunc NewMilvusRetriever(ctx context.Context) (rtr retriever.Retriever, err error) { r, err := milvus.NewRetriever(ctx, \u0026amp;milvus.RetrieverConfig{ Client: client.NewMilvusClient(ctx), Collection: common.MilvusCollectionName, VectorField: \u0026#34;vector\u0026#34;, OutputFields: []string{ \u0026#34;id\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;metadata\u0026#34;, }, TopK: 10, Embedding: embedder.DoubaoEmbedding(ctx), }) if err != nil { return nil, err } return r, nil } 在Eino框架里面，Retriever组件是用来实现召回的，所以我们来看看Retriever这个接口。可以看到返回值是一个Retriever接口，需要实现这个接口的Retrieve方法：\n对输入的问题进行向量化，计算出向量\n调用Milvus数据库sdk的相似度查询接口\n构造返回结构体，返回\nfunc (r *Retriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) (docs []*schema.Document, err error) { // 1. 首先对输入的问题进行向量化 // embedding the query vectors, err := emb.EmbedStrings(r.makeEmbeddingCtx(ctx, emb), []string{query}) // 2. 调用Milvus的相似度查询接口 results, err = r.config.Client.Search( r.config.OutputFields, // 查询的向量 vectors, // 向量字段 r.config.VectorField, // 查询相似度前多少个 *co.TopK, ) // convert the search result to schema.Document documents := make([]*schema.Document, 0, len(results)) for _, result := range results { // 3. 构造结构体 document, err := r.config.DocumentConverter(ctx, result) documents = append(documents, document...) } // 4. 返回 return documents, nil } type Retriever interface { Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error) } 总结：对问题先进行向量化，然后根据向量，调用数据库的向量查询接口进行查询。\n总结 至此，RAG的分片、索引、召回功能我们都实现完了。后续会介绍其他Agent是怎么使用知识库，怎么结合召回来与大模型进行交互的。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%EF%BC%9ARAG%E5%8F%AC%E5%9B%9E%E5%AE%9E%E6%88%982%28Go%29/","summary":"\u0026lt;grid cols=\u0026ldquo;2\u0026rdquo; \u0026lt;column \u0026lt;image token=\u0026ldquo;CB09bMdC5oFLi6xqxnucL7Xinmf\u0026rdquo; width=\u0026ldquo;2614\u0026rdquo; height=\u0026ldquo;2072\u0026rdquo; align=\u0026ldquo;left\u0026rdquo;/ \u0026lt;/column \u0026lt;col","title":"实战演练：RAG召回实战2(Go)"},{"content":"Go语言项目源码 [项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\nJava语言项目源码 [项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\nPython语言项目源码 [项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\npython-harness重构版本 [项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E5%85%AB%E7%AB%A0%20_%20%20%E5%AE%9E%E6%88%98%E6%BC%94%E7%BB%83%E4%B8%8E%E8%BF%90%E8%A1%8C%E9%A1%B9%E7%9B%AE/%E9%A1%B9%E7%9B%AE%E6%BA%90%E7%A0%81%EF%BC%88Go%E3%80%81Java%E3%80%81Python%EF%BC%89/","summary":"Go语言项目源码 \u0026lt;mention-doc token=\u0026ldquo;UfYIwTsNKi6wopkYErTcfb8mnTc\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 项目源码下载\u0026lt;/mention-doc  Java语言项目源码 \u0026lt;mention-doc token","title":"项目源码（Go、Java、Python）"},{"content":"OncallAgent 作为本地优先 AIOps Agent 工作台，需要同时解决三类上下文问题：Prompt 决定 Agent 的长期行为约束，Skill 提供按任务启用的专业操作说明，会话记忆控制历史在模型窗口中的占用。它们都会影响下一次 Agent 执行，但数据生命周期不同，不能混成一个“大字符串配置”。\n📷 [图片 token=Ns0ybOKpzoeoY1x62MTcPCrLnCz（未能下载，见飞书原文）]\n当前实现把用户可编辑 Prompt、上传的标准 SKILL.md、Prompt 与 Skill 选择持久化在 user scope 下；每次流式请求由服务端重新装配。Skill 采用渐进式披露：system prompt 只出现 name 和 description，完整正文必须由模型通过 load_skill 工具按需读取。\n会话记忆则是 session scope。压缩不会删除 SQLite 原始消息，而是写入摘要、推进已压缩消息边界，并只把摘要与边界后的消息交给模型。这让“模型看到什么”和“用户历史里保存什么”保持可解释的分离。\n📷 [图片 token=QWWAb2CiHovfMmxuaMvcBSlwn0f（未能下载，见飞书原文）]\n学习目标 理解 user-scoped Prompt、Skill 资产和选择配置的持久化模型。\n掌握标准 Skill 上传校验与 load_skill 渐进式加载。\n理解强制系统指令、用户 Prompt 和轻量 Skill catalog 的装配顺序。\n区分三种 session memory mode、70% 自动阈值与 95% 硬上限。\n理解摘要、压缩边界、token 占用和完整聊天历史之间的关系。\n功能入口与完整调用链 配置入口由 GET /chat/configuration 和 PUT /chat/configuration 提供。首次读取时，服务为当前用户创建默认 Prompt 和空 Skill 选择。用户可通过 POST /chat/prompts、PUT /chat/prompts/{prompt_id}、删除路由维护 Prompt，通过 POST /chat/skills 上传一个严格命名为 SKILL.md 的文件，再选择零个或多个属于自己的 Skill。\n📷 [图片 token=GAh3b0P0ZosC39xblNMcHJs9nOb（未能下载，见飞书原文）]\n发送消息时，ChatStreamingService.build_agent_configuration 读取 user-scoped 配置、Prompt 和 Skills。build_chat_system_prompt 拼接不可省略的安全与工具指令、用户选中的 Prompt 正文，以及只含 Skill name 和 description 的 Available Skills catalog。LangChainChatAgentRunner 只有在存在选中 Skills 时才注册 load_skill 工具。\n进入 Agent 之前，ChatMemoryService.prepare_message 读取当前会话的 memory_mode、memory_summary、compacted_message_count 和历史。它估算候选上下文，必要时调用模型生成新摘要；达到 95% 硬上限则在用户消息落库前拒绝。成功时只把压缩边界后的消息、候选用户消息和摘要化 system prompt 交给 Agent。\n📷 [图片 token=JIsLb0lyboxjDjxHcbocRVy3nEe（未能下载，见飞书原文）]\n配置阶段： Prompt CRUD + SKILL.md upload → owner-scoped Repositories → user_chat_configurations 选择 promptId / skillIds 聊天阶段： build_agent_configuration → 强制指令 + 用户 Prompt + 轻量 Skill catalog → ChatMemoryService.prepare_message → 可选摘要 + 未压缩消息 → create_agent → 需要时 load_skill(name) 取得选中 Skill 正文 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/chat/configuration.py build_chat_system_prompt、validate_skill_upload、validate_chat_prompt_content Prompt 校验、Skill 标准校验与服务端 system prompt 装配。 apps/backend/src/super_ai/chat/streaming.py build_agent_configuration、create_load_skill_tool 加载当前用户资产并注册仅含选中 Skills 的运行时工具。 apps/backend/src/super_ai/chat/memory.py ChatMemoryService、estimate_context_tokens、memory_payload 会话压缩策略、token 估算、硬上限和状态序列化。 apps/backend/src/super_ai/api/app.py get_chat_configuration、Prompt/Skill 路由、update_chat_memory、compact_chat_memory 受保护配置和会话记忆 HTTP 表面。 apps/backend/src/super_ai/memory/sqlite.py SQLiteUserChatPromptRepository、SQLiteUserChatSkillRepository、SQLiteChatMemoryRepository owner 与 session 范围的 SQLite 实现。 apps/backend/src/super_ai/memory/models.py UserChatConfigurationModel、UserChatPromptModel、UserChatSkillModel、ChatSessionModel 选择、资产、摘要、边界与 token 状态模式。 packages/api-contracts/src/chat-configuration.ts ChatAssemblyConfigurationResponse、ChatSkillAsset 共享 Prompt、Skill 和选择 DTO。 packages/api-contracts/src/chat.ts ChatMemoryMode、ChatMemoryState 共享三种模式、token、窗口、占用率和压缩状态。 apps/backend/tests/test_chat_memory.py test_thirty_turn_mode_compacts_without_deleting_history 证明压缩推进边界但保留所有原始消息。 apps/backend/tests/test_stream_rag_chat_api.py test_chat_configuration_is_validated_and_isolated_by_owner、test_load_skill_tool_progressively_discloses_only_selected_content 验证资产隔离、装配和渐进式正文披露。 openspec/specs/chat-memory-management/spec.md Compression preserves full history 规定 session 模式、自动阈值、手动压缩与硬上限。 代码调用流程图 Prompt、Skill 与 memory 分别属于用户配置、按需工具和会话状态。流程图展示它们如何在一次请求中汇合，同时保留各自的权限和生命周期。\n关键实现拆解 Prompt 不是唯一系统指令 **看什么：**看 system prompt 的拼接顺序：强制指令始终在前，用户 Prompt 是独立段落，Skill 目录只含 name 与 description。\n# 1. 强制指令不会被用户 Prompt 替换。 sections = [ MANDATORY_CHAT_SYSTEM_PROMPT, \u0026#34;用户选择的系统提示词：\\n\u0026#34; + prompt_content.strip(), ] if skills: # 2. 初始 prompt 只披露 Skill name 与 description。 skill_catalog = \u0026#34;\\n\u0026#34;.join( f\u0026#34;- **{skill.name}**: {skill.description}\u0026#34; for skill in skills ) sections.append( \u0026#34;\\n\u0026#34;.join( [ \u0026#34;## Available Skills\u0026#34;, skill_catalog, \u0026#34;以上仅为当前会话允许使用的 Skill。判断用户任务与某个 Skill 的描述匹配时，\u0026#34; \u0026#34;必须先调用 `load_skill` 并传入该 Skill 的 name，再依据返回的完整指令回答。\u0026#34; \u0026#34;不要猜测或声称已加载未调用的 Skill。\u0026#34;, ] ) ) return \u0026#34;\\n\\n\u0026#34;.join(section for section in sections if section.strip()) 这段装配证明用户 Prompt 是偏好层，不是删除强制安全、引用和时间工具约束的覆盖层。完整 Skill Markdown 没有被预先拼入 system prompt；配置只影响后续请求，既有 SQLite 消息和工具审计不会被重写。\n📷 [图片 token=XdRcbDyU5oN6qCxgRHJckjtjncf（未能下载，见飞书原文）]\n用户 Prompt 名称与正文都会 trim，不能为空；名称最长 160 字符，正文最长 12000 字符。默认 Prompt 由 ensure_default 为每个 owner 单独创建。UserChatConfigurationModel 以 owner_user_id 为主键，保存 system_prompt_id 和 JSON skill_ids，因此每个用户只有一份当前装配选择，但可以拥有多份 Prompt 和 Skill 资产。\nbuild_chat_system_prompt 始终先加入 MANDATORY_CHAT_SYSTEM_PROMPT。其中包含按需使用工具、引用知识来源、不编造工具结果、CLS 查询参数和时间查询前调用 get_current_time 等约束；用户 Prompt 被放在“用户选择的系统提示词”段落中。用户自定义内容不会覆盖或删除这些强制指令。\n修改配置只影响后续 Agent 请求。SQLite 中既有消息、引用和工具审计不会被改写。删除当前选择的 Skill 时，API 从 selection 中移除它；删除当前 Prompt 后，配置回退到该用户重新确保存在的默认 Prompt。提交配置前，后端逐个按 owner 读取 prompt 和 skill，未知或跨用户 ID 返回统一参数错误。\n📷 [图片 token=XjDtbhqvwoRPsZxYw0Dce0gmnab（未能下载，见飞书原文）]\n标准 Skill 校验与渐进式披露 **看什么：**先看上传校验在 Repository 写入前如何固定文件名、字节上限、UTF-8 和 YAML frontmatter 边界。\nnormalized_filename = PurePath(filename or \u0026#34;\u0026#34;).name # 1. basename 与原始文件名都必须严格等于 SKILL.md。 if normalized_filename != (filename or \u0026#34;\u0026#34;) or normalized_filename != \u0026#34;SKILL.md\u0026#34;: raise ValueError(\u0026#34;Skill 文件名必须严格为 SKILL.md。\u0026#34;) if not content: raise ValueError(\u0026#34;Skill 文件不能为空，请上传 UTF-8 Markdown 文本。\u0026#34;) if len(content) \u0026gt; MAX_CHAT_SKILL_BYTES: raise ValueError(\u0026#34;Skill 文件不能超过 64 KB。\u0026#34;) try: decoded = content.decode(\u0026#34;utf-8\u0026#34;) except UnicodeDecodeError as exc: raise ValueError(\u0026#34;Skill 文件必须是 UTF-8 编码的 Markdown 文本。\u0026#34;) from exc normalized_content = decoded.strip() if not normalized_content: raise ValueError(\u0026#34;Skill 文件不能为空，请写入可读的 Markdown 指令。\u0026#34;) # 2. frontmatter 必须位于正文开头并可解析成键值对象。 match = _SKILL_FRONTMATTER_PATTERN.match(normalized_content) if match is None: raise ValueError(\u0026#34;SKILL.md 必须以包含 name 和 description 的 YAML frontmatter 开头。\u0026#34;) try: parsed_metadata: object = yaml.safe_load(match.group(1)) except yaml.YAMLError as exc: raise ValueError(\u0026#34;SKILL.md 的 YAML frontmatter 格式无效。\u0026#34;) from exc 这里校验的是结构与 metadata，不是对 Skill 正文做语义安全认证。未知格式会在保存前失败；通过校验的完整正文仍是用户可影响 Agent 的指令文本，所以后续必须继续依赖 owner scope、选择范围和服务端工具权限。\n**看什么：**再看运行时 registry 的闭包范围；只有本次配置已加载的 SelectedChatSkill 能被按 name 取回。\n# 1. registry 只由当前请求已选中的 Skills 构建。 skill_registry = {skill.name: skill for skill in skills} available_names = \u0026#34;, \u0026#34;.join(skill_registry) async def load_skill(skill_name: str) -\u0026gt; str: requested_name = skill_name.strip() skill = skill_registry.get(requested_name) if skill is None: # 2. 未选择的名称不会回退查询全局目录。 return ( f\u0026#34;Skill \u0026#39;{requested_name}\u0026#39; 不可用。当前可加载的 Skill: {available_names or \u0026#39;无\u0026#39;}。\u0026#34; ) return f\u0026#34;Loaded skill: {skill.name}\\n\\n{skill.content}\u0026#34; 渐进式披露减少初始上下文，但不构成代码沙箱：成功调用后完整正文会进入模型工具上下文。未选择、已删除或属于其他用户的 Skill 不会出现在 registry；UI 可见工具摘要又会压缩成首行，避免把全文直接展示为审计结果。\nvalidate_skill_upload 要求文件 basename 与原文件名都精确等于 SKILL.md，非空、UTF-8、最大 65536 bytes。正文必须以 YAML frontmatter 开头，并包含字符串 name 与 description。name 长度 1 到 64，只允许小写字母、数字和单连字符，不能以连字符开头或结尾，也不能含连续双连字符；description 非空且不超过 1024 字符。\n📷 [图片 token=EeakbDlsqobaMyxsZWbcwkYNnAd（未能下载，见飞书原文）]\nRepository 对 owner 与 name 建唯一约束，同一用户重复 name 会转成明确 ValueError。不同用户可以拥有同名 Skill，因为唯一键包含 owner。服务保存完整标准 Markdown，但配置响应只暴露 filename、name、description、label、contentPreview、大小和时间，不把完整 content 放进共享 ChatSkillAsset。\n装配时 SelectedChatSkill 在服务端携带正文，但 system prompt 只渲染 name 与 description，并要求模型先调用 load_skill。create_load_skill_tool 用当前请求的选中 Skills 构建 registry；传入未选择或其他用户的 name，只返回“不可用”以及当前可用 names，不访问全局目录。成功调用才返回 Loaded skill: name 和完整 content。\n📷 [图片 token=I0jzbMt8jo1MN4xiACEc2HQknty（未能下载，见飞书原文）]\n工具的 SSE output 对 load_skill 特别处理，只给 UI 一个首行摘要，不把完整 Skill 正文直接作为工具结果展示。需要注意，正文会进入模型工具上下文，这是渐进加载的目的；“不预先注入 system prompt”不等于正文永远不进入模型。\n📷 [图片 token=CoQRbECrhoi1VkxME1Ichlgsnrc（未能下载，见飞书原文）]\n三种会话记忆模式 **看什么：**看切换 memory mode 时的真实分支：manual 会立即调用 compact，另外两种只刷新使用量，自动压缩要等下一次 prepare_message。\n# 1. mode 先持久化到当前 owner 的 session。 updated = await self._repositories.chat.update_memory_state( owner_user_id=owner_user_id, session_id=session.id, memory_mode=mode, ) current = updated or session if mode == \u0026#34;manual\u0026#34;: # 2. 切换 manual 时立即尝试压缩现有未压缩历史。 return await self.compact( owner_user_id=owner_user_id, session=current, history=history, system_prompt=system_prompt, ) return await self.refresh_usage( owner_user_id=owner_user_id, session=current, history=history, system_prompt=system_prompt, ) manual 没有未压缩消息时，compact 只刷新 token 使用量，不调用摘要模型。memory_mode 存在 session 上，所以同一用户不同会话可以不同；默认 every_30_turns 与 context_70_percent 的触发判断发生在候选 user message 进入 Agent 之前。\n新会话默认 every_30_turns。prepare_message 从 compacted_message_count 切出未压缩历史，把候选 user message 加入估算，并统计未压缩区间的 assistant 数量。达到 30 条 assistant 消息时，在下一次 Agent 调用前压缩这一段。context_70_percent 则在候选上下文达到或超过窗口 70% 时先压缩。\nmanual 不按轮数或 70% 自动触发。当前 API 的 set_mode 在切换为 manual 时会立即调用一次 compact；以后可通过 POST /chat/sessions/{session_id}/memory:compact 再次压缩新增历史。另一个容易忽略的边界是：如果切换 manual 时没有未压缩消息，服务只刷新使用量，不调用摘要模型。\n📷 [图片 token=Qk2IbuCrZo9BNGxkdJ0cqjWdnCb（未能下载，见飞书原文）]\n摘要 prompt 要求保留用户目标、事实、偏好、决策、未完成事项、工具结果和引用，最多 1200 汉字。模型返回非空摘要后，服务把 compacted_message_count 增加当前批次的消息数，更新 memory_summary、context_tokens 和 last_compacted_at。下一次上下文由强制和用户 system prompt、摘要指令、边界后的消息组成。\n📷 [图片 token=FdzHbibBNogrMJxwZe1c0j9snfg（未能下载，见飞书原文）]\ntoken 估算与 95% 硬上限 **看什么：**看 memory 服务先判自动压缩，再用摘要和候选消息重新估算；95% 检查位于 user message 持久化之前。\n# 1. 默认模式按 assistant 数，阈值模式按候选上下文占用。 completed_turns = sum(message.role == \u0026#34;assistant\u0026#34; for message in uncompressed) should_compact = ( current.memory_mode == \u0026#34;every_30_turns\u0026#34; and completed_turns \u0026gt;= 30 ) or ( current.memory_mode == \u0026#34;context_70_percent\u0026#34; and _usage_percent(candidate_tokens, self.context_window_tokens) \u0026gt;= AUTO_CONTEXT_THRESHOLD_PERCENT ) if should_compact and uncompressed: current = await self._compact_messages( owner_user_id=owner_user_id, session=current, messages=uncompressed, system_prompt=system_prompt, ) candidate_messages = [candidate] candidate_tokens = estimate_context_tokens( system_prompt=system_prompt, memory_summary=current.memory_summary, messages=candidate_messages, ) # 2. 压缩后仍达到硬上限就明确拒绝，不截断输入。 if ( _usage_percent(candidate_tokens, self.context_window_tokens) \u0026gt;= HARD_CONTEXT_THRESHOLD_PERCENT ): raise ChatContextLimitReached 硬上限以当前 chat model 对应的 context window 计算，后端才是最终门槛；前端的 95% 快速阻止可能基于上一次刷新而略有滞后。拒绝发生在 append user message 之前，且代码不会继续压缩摘要本身或静默截断用户输入。\nestimate_context_tokens 使用 LangChain 的 count_tokens_approximately 对 system、摘要和 user/assistant 消息估算。窗口大小由应用当前 chat model 的能力配置传入 ChatMemoryService，并在响应中返回 contextWindowTokens。占用率保留一位小数且最多 100。\nprepare_message 会先尝试适用的自动压缩，再重新计算。若候选上下文仍达到或超过 95%，抛出 ChatContextLimitReached。ChatStreamingService 在写用户消息前捕获它并发出 CHAT_CONTEXT_LIMIT_REACHED SSE，所以绕过前端也不能把被拒绝消息持久化。前端 store 也在已有 session 占用率达到 95% 时阻止 send 并提示手动压缩。\n📷 [图片 token=OCpJbQksioWtBtxT6J6cVqr3nmc（未能下载，见飞书原文）]\n用一次配置变更观察服务端装配 **看什么：**看配置更新先逐个以当前 owner 读取资产，全部验证通过后才写 selection；不存在部分保存。\nprompt_repository = _chat_prompt_repository(request) skill_repository = _chat_skill_repository(request) configuration_repository = _chat_configuration_repository(request) # 1. Prompt 必须能在当前 owner 范围内读取。 prompt = await prompt_repository.get( owner_user_id=user.id, prompt_id=body.system_prompt_id, ) if prompt is None: raise ApiErrorException(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;) skill_ids = list(dict.fromkeys(body.skill_ids)) # 2. 所有 Skill 逐个验证后才更新配置记录。 for skill_id in skill_ids: skill = await skill_repository.get(owner_user_id=user.id, skill_id=skill_id) if skill is None: raise ApiErrorException(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;) await configuration_repository.update( owner_user_id=user.id, system_prompt_id=prompt.id, skill_ids=skill_ids, ) prompts, skills, record = await _read_chat_configuration(request, owner_user_id=user.id) return success_response(request, _chat_configuration_payload(prompts, skills, record)) 配置表只保存资产 ID，下一次发送消息时才读取最新正文；因此编辑 Prompt 不需要重存 selection。任一未知或跨用户 ID 会在 update 前失败并保留旧配置，运行时还会过滤遗留的无效 Skill ID 并回写有效列表。\n假设用户创建“故障排查回答规范”Prompt，上传一个 name 为 log-analysis 的 Skill，并保存选择。PUT 配置时，后端先在当前 owner 的 Prompt 表中读取 prompt ID，再逐个读取 Skill ID；任何一个不存在就立即返回验证错误，旧 selection 不会被部分更新。验证通过后，配置表只保存 ID，不复制资产正文。\n下一次发送消息时，stream service 才读取 selection 和资产。这意味着修改 Prompt 正文后，不必重存 selection，后续请求会取得新正文；删除 Skill 后，删除路由同时把 ID 从 selection 移除。若数据库中因旧数据仍有无效 ID，装配代码也会过滤并修复。多层校验让请求运行时不会把空引用悄悄当成已启用能力。\n📷 [图片 token=SJSdbH6XxoBy5UxAtbrcLYFznxf（未能下载，见飞书原文）]\nAgent 初始 system prompt 能看到 log-analysis 的 description，却看不到正文。只有模型判断当前问题与 description 匹配并调用 load_skill，完整指令才作为工具结果加入当前轮次上下文。若问题只是普通寒暄，Skill 正文不会消耗 token，也不会干扰回答。配置是用户级的，所以同一用户所有会话的后续执行都共享选择；当前实现没有会话级 Prompt 或 Skill 覆盖。\n📷 [图片 token=KtAJbHzr6oZqf8xyVCXcAUojnrw（未能下载，见飞书原文）]\n压缩边界如何改变模型视野 **看什么：**看摘要生成如何同时接收已有摘要与新增 transcript，并只推进边界计数，不删除原始 messages。\ntranscript = \u0026#34;\\n\u0026#34;.join( f\u0026#34;{message.role}: {message.content}\u0026#34; for message in messages ) # 1. 新摘要显式合并已有摘要与本批未压缩历史。 prompt = ( \u0026#34;请将以下对话压缩为可供后续模型继续对话的中文记忆摘要。保留用户目标、\u0026#34; \u0026#34;明确事实、偏好、决策、未完成事项、工具结果和引用来源；删除寒暄与重复内容。\u0026#34; \u0026#34;只输出摘要正文，不超过 1200 个汉字。\\n\\n\u0026#34; f\u0026#34;已有摘要：\\n{session.memory_summary or \u0026#39;无\u0026#39;}\\n\\n\u0026#34; f\u0026#34;新增对话：\\n{transcript}\u0026#34; ) response = await self._llm_provider.create_chat_model().ainvoke(prompt) summary = _extract_model_text(response).strip() if not summary: raise RuntimeError(\u0026#34;The model returned an empty memory summary.\u0026#34;) # 2. 边界累加本批数量，原始消息表没有删除操作。 compacted_count = session.compacted_message_count + len(messages) tokens = estimate_context_tokens( system_prompt=system_prompt, memory_summary=summary, messages=[], ) 模型摘要为空会失败且不推进边界。成功后只更新 session 的 memory_summary、compacted_message_count、token 和时间；完整历史 API 仍读取所有原消息。摘要是模型生成的压缩表达，不应被当作新的独立证据或用户逐字原话。\n设一个会话已有 60 条未压缩消息，也就是 30 次 user-assistant 往返，默认模式下用户再发第 31 个问题。prepare_message 先构造一个尚未持久化的 candidate，统计未压缩区域中的 assistant 条数为 30，于是把 60 条历史交给摘要模型。摘要成功后，compacted_message_count 变为 60，candidate 成为当前传给 Agent 的唯一普通消息，memory_summary 通过额外 system 指令加入。\n服务随后才把真实用户消息保存到 SQLite，并用 candidate 所在位置替换为真实 message record 交给 Agent。完整历史 API 仍能返回此前 60 条加上新消息；只有模型上下文缩短。回答完成后 refresh_usage 再按摘要和边界后的真实消息估算 token。因此 contextTokens 是当前模型上下文的估算状态，不是数据库全部历史的 token 总和。\n📷 [图片 token=JFLFbjyIDoFAdoxiSXKc2IhnnMh（未能下载，见飞书原文）]\n下一次压缩不会丢掉旧摘要。_compact_messages 的 prompt 同时包含已有摘要和新增未压缩 transcript，要求模型合并。边界增加的是当前批次的 messages 数量，而不是重置为列表长度。这个累积规则使多轮手动压缩可持续推进；如果消息被清空，Repository 则显式把摘要和边界一起归零，防止旧记忆污染空会话。\n📷 [图片 token=ATq4bVhSNoUiq8x9br7cYrMqnac（未能下载，见飞书原文）]\n阈值模式的细微差别 **看什么：**这张状态图把候选消息、70% 自动压缩与 95% 硬拒绝放在同一请求内，突出两次估算的先后顺序。\nevery_30_turns 统计边界后的 assistant 数，context_70_percent 则把 candidate 纳入占用；manual 不自动触发。自动压缩只执行一次，重新估算后若仍达 95% 就拒绝，不会再次压摘要或丢弃输入。\nevery_30_turns 判断的是压缩边界之后完成的 assistant 数，不是简单按消息总数除以二。只有 user 消息没有对应 assistant 时，不算完成一轮。context_70_percent 在加入候选消息后估算，因此能够在真正越过阈值的那次请求进入 Agent 前先压缩，而不是等回答结束才处理。\n自动压缩只有在存在未压缩消息时执行。压缩后系统重新用摘要和 candidate 估算；若仍达到 95%，请求被拒绝。代码没有在这一刻再次压缩摘要本身，也不会截断用户输入来强行通过。用户必须调整内容、窗口配置或显式管理记忆，系统不会隐式丢弃事实。\n前端用会话响应里的 contextUsagePercent 做快速阻止，但这个值来自上一次刷新，可能在发送前略有滞后。后端用实际候选重新估算才是最终门槛。双层检查改善体验但不依赖前端安全性。错误通过共享目录返回固定中文消息，客户端绕过 UI 也得到同样结果。\n📷 [图片 token=TDeQb3jnFoFR9qxo8l5c8rcgnxf（未能下载，见飞书原文）]\nPrompt、Skill 与摘要的信任层级 **看什么：**这张图按来源与约束关系区分四类上下文，避免把“进入同一个模型请求”误解成“可信度和权限相同”。\n强制指令由代码控制；用户 Prompt 与 Skill 是 owner 资产；摘要是模型生成的历史压缩；真实工具权限仍由服务端注册和 tenant scope 决定。Skill 文本不能凭文字创建未注册工具或扩大知识库/MCP 范围，摘要也不能替代原消息、工具结果或引用证据。\n强制 system 指令由代码维护，优先于用户资产；用户 Prompt 是用户主动选择的行为偏好；Skill 是按需加载的操作说明；memory_summary 是模型对历史的压缩表达。它们虽然都进入模型上下文，却来源不同。审计问题时应能回答某句话来自哪一层，不能把摘要当成用户原话，也不能把 Skill 指令当成已执行工具结果。\n📷 [图片 token=USfibGLZwo41MfxHuu2cALB0ncd（未能下载，见飞书原文）]\nSkill 内容在上传时只做格式和 metadata 校验，不做语义安全认证。用户可以上传影响 Agent 行为的文本，所以只有资产 owner 能选择它，load registry 也只包含当次 selection。强制指令要求不编造工具结果，实际工具仍由服务端注册与权限控制。Skill 无法凭文字创建一个未注册工具，也无法扩大知识库或 MCP 的 tenant scope。\nPrompt 正文和 Skill 正文保存在本地 SQLite。配置响应会返回完整 Prompt content，便于编辑，但 Skill 响应只返回 description 作为 contentPreview。日志不应记录这些正文。当前没有资产版本历史：更新 Prompt 覆写同一记录，Skill 没有更新路由，修改需要删除后重新上传。既有聊天消息不会记录当时完整装配快照，因此回溯时可看到工具审计和消息 metadata，但不能仅凭当前 Prompt 证明历史请求使用了相同正文。\n📷 [图片 token=OCFJbcomXo4HZIxBFaTc0Cd4n9b（未能下载，见飞书原文）]\n数据、契约与状态 Prompt 记录包含 id、owner、label、content、is_default 和时间；Skill 记录包含 id、owner、filename、name、description、完整 content、size 和时间；选择记录包含 owner、system_prompt_id、skill_ids 与时间。三者是 user scope，不绑定某个会话。因此用户改选后，所有后续会话请求都会使用新配置。\nmemory_mode、memory_summary、compacted_message_count、context_tokens、last_compacted_at 位于 ChatSessionModel，是 session scope。共享响应的 memory 还包含 contextWindowTokens、contextUsagePercent 和 canCompact。摘要并不是一条 chat message，完整历史仍由 ChatMessageModel 保存；清空会话消息时，Repository 同时把摘要、边界、token 和最后压缩时间复位。\n权限、安全与失败边界 配置、Prompt、Skill 路由全部依赖当前认证用户。Repository 的 get、list、update、delete 都过滤 owner；selection 更新前再次验证每个资产 ID。Skill registry 只从已经验证、仍存在、属于 owner 的 selection 构建；如果配置中遗留已删除 ID，build_agent_configuration 会过滤并回写有效列表。\n记忆操作先按 owner 读取 session 和消息。跨用户更新模式或手动压缩返回 403。压缩调用真实聊天模型；模型失败或返回空摘要时，不会推进压缩边界。当前摘要是模型生成文本，代码没有把它标记为独立证据来源，因此后续回答仍应依赖持久消息、工具结果和引用，不应把摘要当作新事实。\n📷 [图片 token=IOW6b8bNdo8AZMxktHScBti5nIg（未能下载，见飞书原文）]\n渐进式 Skill 降低了初始 token 成本和不相关指令干扰，但不是代码级沙箱。Skill 是给 Agent 的文本指令，仍受强制 system prompt、工具权限、owner-scoped registry 和现有安全边界约束；上传成功不代表自动启用，仓库中的示例也不会在启动时自动导入。\n阅读顺序与小结 先读 chat-configuration.ts 与 models，区分资产、选择和会话状态。\n再读 validate_skill_upload 与 build_chat_system_prompt，理解输入与装配边界。\n随后跟进 build_agent_configuration 和 create_load_skill_tool，确认正文何时进入模型。\n最后逐行阅读 ChatMemoryService.prepare_message 与 _compact_messages，核对自动压缩、手动压缩和硬限制分支。\nPrompt、Skill 和 memory 共同决定 Agent 上下文，却各自承担不同责任：Prompt 提供持续偏好，Skill 按需提供专业流程，memory 在不删历史的前提下压缩会话。OncallAgent 通过 user scope、session scope、渐进式工具和明确阈值把它们组合起来，使上下文工程成为可持久、可测试、可解释的运行机制。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/10.%20Prompt%E3%80%81%E6%B8%90%E8%BF%9B%E5%BC%8F%20Skill%20%E4%B8%8E%E4%BC%9A%E8%AF%9D%E8%AE%B0%E5%BF%86/","summary":"OncallAgent 作为本地优先 AIOps Agent 工作台，需要同时解决三类上下文问题：Prompt 决定 Agent 的长期行为约束，Skill 提供按任务启用的专业操作说明，会话记忆控制历史在模型窗口中的占用。它们都会影响下一","title":"10. Prompt、渐进式 Skill 与会话记忆"},{"content":"上一章 RAG 里，在介绍 RAG 工作阶段的时候，说把向量存进「向量数据库」，当时一带而过。\n📷 [图片 token=LJjZbg1WpoCxPVxndRkcz7Gynfc（未能下载，见飞书原文）]\n你可能当时就想问：为什么要专门用向量数据库？MySQL 不能存吗？\n这章就专门把这个问题讲清楚。\n一、先别急，普通数据库能不能存向量？ 能存，但有个很大的问题。\n普通数据库存向量没问题，向量本质上就是一串浮点数，MySQL 建个字段存下来完全没难度。\n难的是查。\n二、普通数据库为什么查不了向量？ 咱们先聊聊 MySQL 是怎么查数据的。\n你写 SELECT * FROM users WHERE id = 5，MySQL 走 B+ 树索引，几次跳转就找到了，极快。你写 WHERE age \u0026gt; 18，同样走索引，范围扫描，也很快。\nMySQL 的索引本质上是为两类操作优化的：精确匹配 和 范围比较。\n📷 [图片 token=TYG8bGVKropXEqxYk6fcy3RlnXc（未能下载，见飞书原文）]\n但向量搜索是完全另一回事。\n向量搜索要做的事情是：给你一个向量，找出和它最相似的 top-k 个向量。\n「最相似」意味着要算距离，余弦相似度、欧氏距离……这类计算，需要拿着你的查询向量，和库里每一条向量都算一遍距离，然后排序，取最小的几个。\nMySQL 的 B+ 树索引完全帮不上这个忙。它压根不知道怎么在多维空间里找「最近邻」。\n那就暴力遍历呗？\n来感受一下规模：\nOpenAI 的 text-embedding-ada-002 模型，每个 chunk 转化出来的向量是 1536 维。你的知识库有 100 万条 chunk，那么每次查询要算的浮点运算量是：\n1536 维 × 100 万条 = 15.36 亿次浮点运算\n这还只是算距离这一步。每次用户提问，都要触发一次这个计算量，普通数据库完全扛不住。\n数据量涨到 1 亿条呢？1536 亿次浮点运算，一次查询可能要好几秒，完全不可用。\n总结：普通数据库存向量没问题，但查向量相似度完全没有优化，规模一上去就垮了。\n三、向量数据库是什么 向量数据库就是专门为解决这个问题设计的，在海量向量中，毫秒级找到最相似的 top-k 条。\n📷 [图片 token=LTXFbY9UTo0GMKx59NMcWNXQnSe（未能下载，见飞书原文）]\n它存的东西和普通数据库差不多：向量 + 对应的原文（元数据）。但它有专门为向量搜索设计的索引结构，能把那个「15.36 亿次浮点运算」的暴力遍历，优化到几十毫秒甚至几毫秒完成。\n关键问题就来了：它怎么做到的？\n四、向量数据库怎么做到「又快又准」 秘诀是 ANN（Approximate Nearest Neighbor，近似最近邻） 搜索。\n注意这个词：近似。\n向量数据库并不保证找到绝对最相似的那几条，而是找到「差不多最相似的」几条，精度换速度。但在实际场景里，「差不多最相似的」和「绝对最相似的」效果几乎没有区别，因为语义检索本身就不需要那么精确。\n那它用的什么算法？目前最主流的是 HNSW（Hierarchical Navigable Small World，分层可导航小世界图）。\n📷 [图片 token=VKw2brbEqoxPUaxtmQscbBH5nYd（未能下载，见飞书原文）]\n名字很复杂，但原理用一个例子就能讲清楚。\n图书馆的多级目录类比 你去一个大图书馆，想找一本叫《MySQL 索引优化》的书。\n笨方法：从第一排书架的第一本书开始，一本一本翻，直到找到它。100 万本书的图书馆，你翻到猴年马月。\n图书馆的实际做法：\n第一步，看大类目录牌：理工科 → 计算机科学 → 数据库技术。三步跳转，你已经排除了 90% 的书架区域。\n第二步，看中类目录：性能优化 → 索引与查询。再两步，进一步缩小范围。\n第三步，直接在这一小块书架上找。扫几眼，拿到书。\n整个过程不超过 2 分钟，而你总共只「看过」几十本书的标签，不是 100 万本。\nHNSW 就是这个逻辑。\n它把所有向量组织成一个多层的图结构：\n高层（稀疏层）：只有少量「节点」，每个节点连接着几个「邻居」。高层负责大范围的快速跳转，帮你快速缩小搜索范围。\n低层（稠密层）：所有向量都在这里，精细定位最终的近邻候选。\n查询的时候，从高层入口进，快速跳转几步找到大致区域，然后下到低层精细扫描。全程只需要访问整个图的一小部分节点，而不是所有节点。\n结果：牺牲极少量精度（通常 95%+ 的召回率），换来 100 倍以上的速度提升。100 万条向量，几毫秒搞定。\n五、主流向量数据库介绍 市面上向量数据库已经有不少选择了，咱们介绍五个最常见的。\nChroma 本地轻量级向量数据库，Python 原生 API，几行代码就能跑起来，不需要任何部署配置。\n适合干什么：本地开发、功能验证、快速上手原型。你想跑通 RAG 的完整流程，Chroma 是最低摩擦的选择，装个 pip 包直接用。\n缺点：不适合生产环境，没有高可用、分布式这些特性。\nPinecone 全托管的云向量数据库，你不需要管任何基础设施，服务器、扩容、备份全都是 Pinecone 的事，你只需要调 API。\n适合干什么：快速上线，不想自己运维。创业公司、小团队，想把 RAG 功能上生产但没有专职 DBA，Pinecone 是最快的路径。\n缺点：数据放在第三方，有数据合规顾虑的场景不适合；按用量收费，规模大了成本不低。\nMilvus 开源的生产级向量数据库，功能最全，支持多种索引类型、分布式部署、数据持久化。背后是 Zilliz 公司（国产）维护，社区活跃，文档完善。\n适合干什么：私有化部署的生产环境。数据不能出内网、需要精细控制、规模较大的场景，Milvus 是目前开源里功能最完整的选择。\n缺点：部署和运维有一定复杂度，学习曲线比 Chroma 陡。\nWeaviate 开源向量数据库，特色是内置了 Embedding 集成，你可以直接把原始文本丢进去，Weaviate 自动帮你调 Embedding 模型转向量，不用自己处理这一步。同时支持图片、音频等多模态数据。\n适合干什么：需要多模态搜索、或者想简化 Embedding 步骤的场景。\nQdrant Rust 实现的高性能向量数据库，内存占用低，查询速度快，适合资源受限或高并发场景。接口设计简洁，HTTP/gRPC 都支持。\n适合干什么：对性能和资源消耗敏感的生产环境，比如在有限硬件上跑大规模向量检索。\n六、怎么选？ 讲了五个，选哪个？给你一个明确的决策树，不说「看情况」：\n学习 / 跑原型 → 用 Chroma\n零配置，pip 安装，几行代码跑通完整 RAG 流程。你现在就在学 RAG，直接用 Chroma，不要想太多。\n生产环境，不想管运维 → 用 Pinecone\n不想操心服务器、扩容、备份这些事，数据放云上也没问题，Pinecone 拿起来就用。\n生产环境，需要私有化部署 → 用 Milvus 或 Qdrant\n数据必须在自己服务器上，选这两个。团队有运维能力、需要最全功能选 Milvus；对性能和资源敏感、希望部署简单点选 Qdrant。\n用厨房来类比这四个选择：\nChroma：家用厨房。够用，方便，不用专门装修，在家做饭首选。\nPinecone：叫外卖。你只管点菜，后厨、配送、洗碗全不用管，就是要花外卖费。\nMilvus：专业餐厅厨房。功能齐全、可定制，但你得有专业厨师来管。\nQdrant：高效快餐厨房。出菜快、省资源，适合高并发大量出餐的场景。\n七、普通数据库 vs 向量数据库 C2oDs8 对比维度 普通数据库（MySQL） 向量数据库（Milvus/Pinecone…） 核心用途 结构化数据存储和精确查询 向量存储和相似度搜索 索引结构 B+ 树（适合精确匹配/范围查询） ANN 索引，如 HNSW（适合近邻搜索） 查询类型 id = 5、age \u0026gt; 18、LIKE 匹配 找 top-k 个最相似向量 向量相似度查询 不支持（只能暴力全表扫描） 原生支持，毫秒级 百万级向量查询速度 数秒（暴力遍历，不可用） 毫秒级（ANN 索引加速） 数据形式 表格（行列结构） 向量 + 元数据 适合 RAG 吗 存能存，查不行 专为这个场景设计 总结 这章的核心认知：\n普通数据库存向量没问题，但查向量相似度完全没有优化。百万级向量 + 高维（1536 维）的暴力遍历，每次查询几十亿次浮点运算，压根跑不起来。\n向量数据库专门解决「海量向量中毫秒级找最近邻」这个问题，靠的是 ANN 索引。\nHNSW 是最主流的 ANN 索引，核心思路是多层图结构：高层大范围快速跳转，低层精细定位，就像图书馆的多级目录，不用翻所有书就能找到目标区域。代价是牺牲极少量精度，换来 100 倍以上的速度。\n选型建议：学习用 Chroma，生产不想运维用 Pinecone，生产私有化部署用 Milvus 或 Qdrant。\n理解了向量数据库，RAG 的离线建库这条链路就完整了：文档 → 切块 → 向量化 → 存进向量数据库。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%EF%BC%9F/","summary":"上一章 RAG 里，在介绍 RAG 工作阶段的时候，说把向量存进「向量数据库」，当时一带而过。   \u0026lt;image token=\u0026ldquo;LJjZbg1WpoCxPVxndRkcz7Gynfc\u0026rdquo; width=\u0026ldquo;1620\u0026rdquo; height=\u0026ldquo;930\u0026rdquo; a","title":"什么是向量数据库？"},{"content":" [!WARNING] 《进阶设计：RAG进阶》\n跳转至这里阅读：[08. Milvus、BM25L、RRF 与 rerank 混合检索](/oncall/AI Native 工程化实战（智能OnCall Agent）/05｜OncallAgent 功能与源码解析/08. Milvus、BM25L、RRF 与 rerank 混合检索/) 这次 RAG 升级的核心是：从“单路向量召回 ”，升级成一套可用于生产环境的“混合召回 + RRF 融合 + 真实 rerank + 权限与证据链”系统。\n升级后的完整检索链路是：\nAgent 判断是否需要知识库 → 校验 query、topK、过滤器和知识库权限 → 并行召回 ├─ query embedding → Milvus 语义召回 Top 20 └─ scoped chunks → BM25L 词项召回 Top 20 → owner / tenant / 文档 / metadata 二次过滤 → RRF 融合并去重，保留 Top 20 → qwen3-vl-rerank 真实精排 → 返回最多 5 条结果及对应引用 几个特别重要的设计变化：\n混合检索解决两类问题 向量检索负责“意思相近”，BM25L 负责“字符必须精确”。即使某个片段只在一路命中，也能进入候选；两路都命中的片段会受到 RRF 奖励。\n不直接相加不同分数 COSINE 和 BM25L 的分数量纲不同，因此不相加原始分数，而是按照名次计算：\nRRF score = Σ 1 / (60 + rank) rerank 从理论概念变成真实工程实现 基础文档已经介绍过 Cross Encoder，但升级版真正接入了 qwen3-vl-rerank，加入有限重试、响应索引校验、分数范围校验，并以 rerankScore 作为最终相关性分数。\n权限参与检索和排名 未授权文档不仅不能出现在结果中，也不能参与 BM25 的 IDF 统计，避免其他租户的语料间接改变当前用户的排名。\n结果具备可解释证据链 每条结果都保留：\nvectorRank / vectorScore bm25Rank / bm25Score rrfScore rerankRank / rerankScore 前端可以解释“为什么进入候选”和“为什么最终排在这里”，但高分仍不能直接当作故障根因证据。\n采用失败关闭策略 Embedding、Milvus、BM25L 或 rerank 任一环节失败，都会返回安全的系统错误，不会偷偷回退到单路结果。只有正常执行后确实无候选，才返回空 results。\n一句话概括：这次主要升级的不是 RAG 的“生成”，而是检索中间层——召回更全面、排序更准确、权限更严格、结果更可解释。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%89%E7%AB%A0%EF%BD%9C%E7%9F%A5%E8%AF%86%E5%BA%93%20RAG%20%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/%E8%BF%9B%E9%98%B6%E8%AE%BE%E8%AE%A1%EF%BC%9ARAG%E8%BF%9B%E9%98%B6/","summary":"!WARNING  《进阶设计：RAG进阶》  - 跳转至这里阅读：\u0026lt;mention-doc token=\u0026ldquo;RSRGwmRMjiTLR7klRdWcr8o4nXf\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 08. Milvus、BM25L、RRF 与 re","title":"进阶设计：RAG进阶"},{"content":"在 OncallAgent 中，MCP 不是一个只负责展示“可用工具列表”的装饰层，而是聊天 Agent 和 AIOps 诊断访问外部系统的真实执行边界。用户在界面中保存连接，后端按当前用户读取启用项，向对应 MCP Server 发起初始化、工具发现和工具调用。连接检查失败时，系统把失败状态、空工具列表和安全错误保存到连接的 lastCheck；工具真正进入聊天或诊断生命周期后，调用成功与失败才会另行写入工具审计。两类记录不能混为一谈，系统也不会用模拟结果把流程伪装成成功。\n📷 [图片 token=YsqSbjs6Ho7brGxkAnPcc0mbnVV（未能下载，见飞书原文）]\n理解这条链路的关键，是把“连接配置”“工具发现”“运行时装配”“真实调用”和“调用审计”看成五个连续环节。任意一环缺失，Agent 都不应声称已经获得日志或完成外部操作。OncallAgent 当前支持 SSE 与 Streamable HTTP 两种 transport，并把腾讯云官方 CLS MCP Server 作为未配置用户连接时的项目默认入口。\n学习目标 理解一条 MCP 连接如何从前端表单进入 SQLite，并被当前用户的 Agent 运行时加载。\n区分“保存连接”“检查连接”“发现工具”和“调用工具”四种不同动作。\n掌握多连接下的同名工具保护、超时重试和 owner 范围隔离。\n能够沿源码和测试判断一次工具结果是否来自真实 MCP Server。\n功能入口与完整调用链 用户从前端 /mcp 工作区创建或编辑连接。apps/frontend/src/stores/mcp.ts 中的 useMcpStore 负责页面状态，initialize、create、update、check 和 remove 分别对应列表、保存、连通性检查和删除动作。网络请求由 apps/frontend/src/mcp/mcpClient.ts 的 createMcpClient 发往 /mcp/connections 系列接口。\nFastAPI 路由位于 apps/backend/src/super_ai/api/app.py。每个接口都先通过 _current_user 得到认证用户，再把 user.id 作为 owner_user_id 传给 McpConnectionService。服务层完成字段校验和业务判断，SQLite repository 则在查询、更新、删除和保存检查结果时再次应用 owner 条件。这样，连接 ID 即使被猜到，也不能脱离用户范围单独访问。\n执行路径可以概括为：前端表单 → 共享请求契约 → FastAPI 认证路由 → McpConnectionService → owner-scoped SQLite 记录 → LocalMcpClient → MCP session 初始化 → list_tools 或 call_tool → SSE/HTTP 工具结果 → Agent 事件与 SQLite 审计。聊天和 AIOps 并不读取一个全局工具清单，而是通过 client_for_user 装配当前用户启用的连接。\n📷 [图片 token=RhTvbVe4Jo7ehyxoP4XcNbudnNe（未能下载，见飞书原文）]\n/mcp 页面 → useMcpStore.check(connectionId) → POST /mcp/connections/{connectionId}:check → McpConnectionService.check(owner_user_id, connection_id) → LocalMcpClient.discover_tools() → ClientSession.initialize() + list_tools() → 保存 lastCheck 与真实 tools 聊天或诊断执行 → McpConnectionService.client_for_user(owner_user_id) → 仅装配 enabled 连接 → discover_tools() / call_tool() → 工具生命周期事件 + owner-scoped 审计 核心源码地图 源码位置 关键符号 职责 apps/frontend/src/stores/mcp.ts useMcpStore 维护连接列表、选中项、保存中和检查中状态，并把错误转换成用户可读提示。 apps/frontend/src/mcp/mcpClient.ts createMcpClient 按共享契约调用连接列表、创建、更新、删除和检查接口。 packages/api-contracts/src/mcp.ts McpConnection、McpToolSummary 定义 transport、连接字段、最近检查和发现工具的前后端共同结构。 apps/backend/src/super_ai/api/app.py list_mcp_connections、check_mcp_connection 提供认证后的 HTTP 入口，并统一映射成功与错误响应。 apps/backend/src/super_ai/mcp_connections.py McpConnectionService 校验连接、管理 owner 范围生命周期、保存检查结果并为运行时装配客户端。 apps/backend/src/super_ai/mcp_client.py LocalMcpClient、McpServerConnection 建立 SSE 或 Streamable HTTP session，发现工具、路由调用并执行超时重试。 apps/backend/src/super_ai/memory/extended_sqlite.py SQLiteMcpConnectionRepository 持久化连接、启停状态、最近检查、工具摘要和安全错误。 apps/backend/src/super_ai/memory/models.py McpConnectionModel 定义 mcp_connections 表以及 owner 与名称的唯一约束。 openspec/specs/mcp-connection-management/spec.md Owner-scoped lifecycle 规定连接管理、真实发现、禁用连接不装配和中文工作区行为。 openspec/specs/real-mcp-tools/spec.md Real MCP tools 规定真实调用、失败显式化、审计和项目配置边界。 📷 [图片 token=FhJDbDzJCoTTnZxjWOZcJAagnUh（未能下载，见飞书原文）]\n代码调用流程图 MCP 链路要区分连接检查、聊天工具装配和 AIOps 直接调用。它们共享 owner-scoped 连接配置，但运行时调用方式并不完全相同。\n关键实现拆解 连接管理不是简单的 URL 保存 McpConnectionService.create 和 update 都先调用 _validated_connection。名称不能为空且长度受限；transport 只能是 sse 或 streamable_http；URL 必须是 HTTP/HTTPS、必须有 hostname，并且禁止携带 username 或 password；超时限制在 1 到 300 秒，重试次数限制在 0 到 5。校验失败会产生 VALIDATION_INVALID_ARGUMENT，错误通过统一 API envelope 返回。\n看什么：先看服务层在任何 Repository 写入之前如何一次性收紧名称、transport、URL、超时和重试范围；尤其注意 URL 的 userinfo 检查不是前端提示，而是后端保存边界。\nnormalized_name = name.strip() normalized_url = url.strip() parsed = urlsplit(normalized_url) # 1. 名称和 transport 在落库前完成规范化与白名单校验。 if not normalized_name or len(normalized_name) \u0026gt; 120: raise McpConnectionError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;MCP 连接名称无效。\u0026#34;) if transport not in SUPPORTED_MCP_TRANSPORTS: raise McpConnectionError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;MCP transport 无效。\u0026#34;) # 2. HTTP(S) URL 必须有 hostname，且禁止 username/password userinfo。 if ( parsed.scheme not in {\u0026#34;http\u0026#34;, \u0026#34;https\u0026#34;} or not parsed.hostname or parsed.username or parsed.password ): raise McpConnectionError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;MCP URL 必须是安全的 HTTP 地址。\u0026#34;) if not 1 \u0026lt;= timeout_seconds \u0026lt;= 300 or not 0 \u0026lt;= retries \u0026lt;= 5: raise McpConnectionError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;MCP 超时或重试参数无效。\u0026#34;) 这段代码证明校验失败不会产生半条连接记录，且 transport 与重试策略不能由任意字符串扩展。它同时说明这里的“安全 URL”只落实到 scheme、hostname 与 userinfo；文档不能进一步声称已做内网地址阻断或完整 SSRF 防护。\n📷 [图片 token=JXylbP3wvoZhALxsdQAceqpwn9f（未能下载，见飞书原文）]\nlist 还有一个容易忽略的默认行为：当前用户完全没有连接记录时，会为该用户创建一条“腾讯云 CLS”默认连接。只要用户已经存在记录，即使全部被禁用，也不会再次偷偷补一条启用连接。client_for_user 也遵守这个区别：没有任何记录时使用项目默认 CLS 地址；已有记录时只选取 enabled 为真的项。因而“禁用全部自定义连接”不会绕回默认连接。\n看什么：把 list 的“首次可见默认记录”和 client_for_user 的“运行时回退”放在一起读，观察判断条件都是“是否存在任何记录”，而不是“是否存在启用记录”。\nrecords = await repository.list(owner_user_id=owner_user_id) if records: return records # 1. 仅当 owner 尚无任何记录时，列表入口创建默认连接。 await repository.create( owner_user_id=owner_user_id, connection_id=f\u0026#34;mcp_{uuid4().hex}\u0026#34;, name=\u0026#34;腾讯云 CLS\u0026#34;, transport=\u0026#34;sse\u0026#34;, url=self._default_url, enabled=True, timeout_seconds=self._default_timeout_seconds, retries=self._default_retries, ) return await repository.list(owner_user_id=owner_user_id) async def client_for_user(self, *, owner_user_id: str) -\u0026gt; LocalMcpClient: records = await self._repository().list(owner_user_id=owner_user_id) if not records: # 2. 运行时只在“零记录”时使用项目默认地址。 return LocalMcpClient( self._default_url, timeout_seconds=self._default_timeout_seconds, retries=self._default_retries, ) return _client_from_records([record for record in records if record.enabled]) 它证明“全部禁用”会装配一个零连接客户端，而不是重新启用 CLS 默认连接。边界也很清楚：默认记录创建发生在列表请求中，运行时回退本身不会写库；两条路径最终都仍受当前 owner 的记录集合控制。\n📷 [图片 token=AvlobqQ0no2cTVxvGzAcU6C9nOf（未能下载，见飞书原文）]\n看什么：下面的状态图专门区分零记录、已有启用记录和已有但全禁用三种状态，避免把“没有配置”与“用户主动禁用”混为一谈。\n这张图证明连接状态的分支点是 owner-scoped 持久记录，而不是全局配置是否存在。失败与一致性边界是：禁用状态不会被默认回退覆盖，但零连接客户端自然无法提供 MCP 工具，调用方必须把这一事实显式呈现。\n工具发现先于可信调用 LocalMcpClient.discover_tools 对每条连接执行 ClientSession.initialize 和 session.list_tools，把服务端返回的名称、描述、输入 schema 和连接标识转换成 McpToolDefinition。当前用户连接服务把持久化记录的 ID 作为这个内部 server name。这是真实网络发现，不是根据本地配置拼出一组静态工具。检查接口捕获 McpClientError 后保存失败状态和安全提示，工具列表保持为空，不会为了让页面“看起来正常”而填入预设名称。\n📷 [图片 token=Z2KjbqunWooSuUxl8vpcbQsfnid（未能下载，见飞书原文）]\n看什么：连接检查先按 owner 读取指定记录，再对这一条记录创建客户端；发现异常只转换为固定中文错误，最后把真实工具摘要或空列表写入 lastCheck。\nrecord = await repository.get( owner_user_id=owner_user_id, connection_id=connection_id, ) if record is None: raise McpConnectionError(\u0026#34;AUTH_FORBIDDEN\u0026#34;, \u0026#34;MCP connection is not accessible.\u0026#34;) client = _client_from_records([record]) tools: list[McpToolDefinition] = [] error: str | None = None try: # 1. 检查动作发起真实 initialize 与 list_tools。 tools = await client.discover_tools() except McpClientError: error = \u0026#34;MCP Server 不可用或工具发现失败。\u0026#34; # 2. 失败时 tools 仍为空，且只保存安全错误。 updated = await repository.save_check( owner_user_id=owner_user_id, connection_id=connection_id, ok=error is None, tools=[_tool_payload(tool) for tool in tools], error=error, ) 这段代码证明检查接口不会把其他 owner 的连接拿来探测，也不会把底层异常原文保存到连接状态。它的边界是：固定错误提高了秘密安全性，却有意牺牲底层网络错误细节；排障时应结合脱敏运行日志，而不是期待 lastCheck.error 含堆栈。\n多连接场景还要处理工具名冲突。discover_tools 使用 seen 集合拒绝重复名称；call_tool 在多连接时重新确认名称只能匹配一个定义，再按 server_name 找到目标连接。如果工具缺失或同名歧义，调用直接失败。这种保护比“随便选择第一个 Server”更可靠，因为它不会把日志查询发送到错误系统。\n看什么：关注 seen 与 connection.name 两个字段。前者把工具名当作跨连接唯一键，后者把发现结果绑定回具体 Server。\ndefinitions: list[McpToolDefinition] = [] seen: set[str] = set() for connection in self._connections: result = await self._run_connection( connection, lambda session: session.list_tools(), ) for tool in result.tools: # 1. 任意两个连接暴露同名工具时，整次发现明确失败。 if tool.name in seen: raise McpClientError(f\u0026#34;Duplicate MCP tool name: {tool.name}\u0026#34;) seen.add(tool.name) definitions.append( McpToolDefinition( tool.name, tool.description or \u0026#34;MCP tool\u0026#34;, tool.inputSchema, # 2. server_name 保留后续调用所需的归属。 connection.name, ) ) return definitions 它证明多连接不是简单拼接清单：发现阶段就建立“工具名唯一、定义可追到 Server”的不变量。失败边界是任一连接发现失败或任意同名冲突都会使整次发现失败；当前实现没有命名空间降级或部分成功模式。\n📷 [图片 token=OohEb7nEaoDoNJxPc1GcJxkAnyd（未能下载，见飞书原文）]\n看什么：这张时序图把显式检查和运行时装配并列起来。两者都以真实发现为信任门槛，但只有检查路径会保存最近检查结果。\n这张图证明工具定义来自 Server 会话，而不是 SQLite 中上次保存的摘要；保存的 lastCheck 是展示与诊断状态，不是运行时可信调用的静态白名单。权限边界始终在最前面的 owner 查询处。\n📷 [图片 token=FFkrbOlCQoRBN1xvFQ4cztornXb（未能下载，见飞书原文）]\n真实调用、重试和日志边界 单连接调用通过 session.call_tool 执行，多连接调用先完成工具归属解析。_run_connection 按 retries + 1 次尝试执行，重试间隔为递增的短等待；SSE 走 sse_client，Streamable HTTP 走 streamable_http_client。两条路径最终都进入 _run_initialized_session，在 session 初始化后用 asyncio.wait_for 施加超时。\n看什么：单连接可以直接调用；多连接必须先重新发现，再要求工具名恰好匹配一个定义，并按定义中的 server_name 选择连接。\nif len(self._connections) == 1: connection = self._connections[0] # 1. 单连接无需归属消歧，直接进入统一运行器。 result = await self._run( lambda session: session.call_tool( name, arguments, read_timeout_seconds=timedelta( seconds=connection.timeout_seconds ), ) ) else: matching = [tool for tool in await self.discover_tools() if tool.name == name] # 2. 多连接只接受唯一匹配，缺失与歧义都失败。 if len(matching) != 1: raise McpClientError(f\u0026#34;MCP tool is unavailable or ambiguous: {name}\u0026#34;) connection = next( item for item in self._connections if item.name == matching[0].server_name ) 这段代码证明调用不会在多连接之间“任选一个”。它也暴露一个一致性取舍：多连接调用前会再次做网络发现，因此能反映 Server 当前清单，但发现失败会在真正调用之前终止本次执行。\n📷 [图片 token=OIu5bzQcbo2WwFxUaENczX4un9d（未能下载，见飞书原文）]\n看什么：统一运行器把重试、transport 分派、session 初始化和操作超时串成一条链；重试耗尽后只抛统一的 McpClientError，原异常作为 cause 保留在服务端异常链中。\nerror: Exception | None = None for attempt in range(connection.retries + 1): try: # 1. 每次尝试按持久化 transport 建立新的客户端会话。 if connection.transport == \u0026#34;streamable_http\u0026#34;: return await self._run_streamable_http(connection, operation) return await self._run_sse(connection, operation) except Exception as exc: error = exc if attempt \u0026lt; connection.retries: await asyncio.sleep(0.2 * (attempt + 1)) raise McpClientError(f\u0026#34;MCP server unavailable at {connection.url}\u0026#34;) from error # … 省略两个 transport 的流创建代码 async with ClientSession(read_stream, write_stream) as session: # 2. initialize 完成后，具体操作仍受 asyncio 超时保护。 await session.initialize() return await asyncio.wait_for(operation(session), timeout=timeout_seconds) 它证明配置中的 retries 表示额外重试次数，总尝试数是 retries + 1。失败边界包括连接、初始化、列举与调用异常，都会进入同一重试循环；当前实现没有按异常类型区分“可重试”和“不可重试”。\n📷 [图片 token=LlHqbL042oXVbtxXEkRcAccwn1g（未能下载，见飞书原文）]\n当 MCP 返回 isError 时，客户端抛出 McpClientError，不会把错误内容当成成功输出。结构化运行日志只记录工具名、参数键名、结果条数、耗时和错误类别；apps/backend/tests/test_mcp_observability.py 明确断言参数值和工具输出不会进入日志。需要区分的是：运行日志为脱敏运维信号，工具审计是受 owner 保护的业务记录，两者职责不同。\n看什么：最后用局部时序图观察“失败后重试”与“成功后日志”的分叉；注意 MCP 返回 isError 时不会发 completed。\n这张图证明运行日志和业务返回在调用完成后才分流：前者刻意不含参数值与输出，后者保留真实 payload 供 Agent 和 owner-scoped 审计使用。安全边界不是“工具输出无敏感信息”，而是运行日志不复制这些内容；工具参数与审计仍应避免承载凭据。\n📷 [图片 token=WHOYbb5hFoFPjix9Ld0cBdQHnWg（未能下载，见飞书原文）]\n数据、契约与状态 McpConnectionModel 保存连接 ID、owner、名称、transport、URL、启用状态、超时、重试次数以及最近检查字段。last_check_ok、last_tool_count、last_tools、last_error 和 last_checked_at 共同表达检查结果。共享契约 McpConnectionCheck 将其整理为 ok、toolCount、tools、error、checkedAt，前端无需猜测数据库状态。\n“保存成功”不等于“连接可用”。创建或更新只证明字段通过校验并进入 SQLite；只有显式 :check 调用完成真实工具发现后，页面才有最近检查结果。类似地，“发现到工具”也不等于“某次调用成功”，每次 call_tool 仍可能因网络、超时、服务端错误或工具参数失败。教学和排障时应保留这三层状态，不能把它们合并成一个绿色标记。\n📷 [图片 token=UcrebRd37o46XRxXcQIcjEnyn5d（未能下载，见飞书原文）]\nAgent 使用的工具定义来自运行时发现。聊天侧会把 MCP 工具与知识检索、当前时间等工具一起交给 LangChain Agent；AIOps 侧则在 Planner 中发现可用工具，在 Executor 中调用选定工具。两条链路都以当前 owner_user_id 调用 client_for_user，连接状态不会跨用户共享。\n📷 [图片 token=Bf5ObvD6Xo9oluxRPeOckioznEd（未能下载，见飞书原文）]\n权限、安全与失败边界 第一道边界是认证路由，第二道边界是 service/repository 的 owner 条件。更新或检查不存在于当前用户范围的连接时，服务返回 AUTH_FORBIDDEN；删除返回 false 时，API 同样映射为统一权限错误。这样不会通过“存在返回 404、不存在返回 403”的差异泄露其他用户是否拥有某个连接。\n📷 [图片 token=JScWbmV0Fo94ELxfeVScVVdwnrf（未能下载，见飞书原文）]\nURL 校验禁止 userinfo，但它并不等于完整的网络出口安全体系。当前实现允许用户配置 HTTP/HTTPS hostname，因此部署到更开放的环境时仍需结合网络策略考虑 SSRF 和私网访问范围。OncallAgent 的定位是本地优先工作台，文档不能把这一校验夸大成通用企业网关。\n📷 [图片 token=XODNbRtt4oj8CZxkOa4cHbKUnvh（未能下载，见飞书原文）]\n如果 MCP Server 不可用，连接检查保存明确失败；运行时发现或调用抛出错误；AIOps 会生成失败工具事件、失败审计和证据不足报告。代码没有“工具失败后返回一份示例日志”的兜底。项目默认连接也只是配置回退，不是可用性回退：默认 CLS Server 未启动时，readiness 和调用都必须如实失败。\n📷 [图片 token=RqR3by8jaow94DxLGP2ctt6FnDj（未能下载，见飞书原文）]\n阅读顺序与小结 先读 packages/api-contracts/src/mcp.ts，建立页面和 API 看到的数据结构。\n再沿 mcpClient.ts、useMcpStore 和 api/app.py 看一次连接检查。\n进入 mcp_connections.py 理解校验、默认连接和启用过滤。\n最后读 mcp_client.py，确认工具发现、真实调用、重试、冲突和脱敏边界。\n这套实现的核心不是“让 Agent 拥有更多按钮”，而是让每个外部能力都有明确来源、用户归属、真实调用和失败证据。只有沿连接记录、发现结果、工具调用、SSE 事件和审计记录逐段核对，才能判断一次外部取证是否可信。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/11.%20%E7%94%A8%E6%88%B7%E7%BA%A7%20MCP%20%E8%BF%9E%E6%8E%A5%E4%B8%8E%E7%9C%9F%E5%AE%9E%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8/","summary":"在 OncallAgent 中，MCP 不是一个只负责展示“可用工具列表”的装饰层，而是聊天 Agent 和 AIOps 诊断访问外部系统的真实执行边界。用户在界面中保存连接，后端按当前用户读取启用项，向对应 MCP Server 发起初始","title":"11. 用户级 MCP 连接与真实工具调用"},{"content":"学 Agent 开发，第一件事是认识它的\u0026quot;大脑\u0026quot;： 大模型。\n你不需要先搞懂它背后的数学原理，但你必须清楚它能做什么、不能做什么，以及它是怎么\u0026quot;看\u0026quot;你发给它的文字的。搞清楚这些，后面学 Prompt、Function Calling、RAG、Agent，你才能知道\u0026quot;为什么要这么做\u0026quot;。\n大模型是什么 我们平时说的「AI 对话」「AI 助手」，核心都是大模型，英文叫 LLM（Large Language Model，大语言模型）。\n用大白话讲，大模型就像一个从小读了海量书、见识极广、逻辑又好的超级助理。\n📷 [图片 token=XDlGbF3FDoN4n5x4dQTcr3IAnPh（未能下载，见飞书原文）]\n它\u0026quot;大\u0026quot;在哪里？两点：\n参数量大：模型内部有数千亿个参数，可以理解为它\u0026quot;记住了\u0026quot;海量知识的精华\n训练数据大：研发人员提前给它喂了互联网上的书籍、文章、新闻、代码、论坛讨论等大量内容，让它学会了语言规律、通用常识和各行各业的基础知识\n学会之后，它能干什么？说白了就是根据你给的上下文，预测接下来最合理的文字是什么。你问它「今天天气怎么样」，它不是真的去查天气，而是根据过去学过的语言模式，生成一个\u0026quot;看起来最合理的回答\u0026quot;。\n我们平时用的 ChatGPT、豆包、通义千问、文心一言，背后核心都是这样的大模型。\nToken：大模型读文字的方式 在讲大模型能做什么之前，有一个基础概念必须先搞清楚，因为后面所有的东西都跟它有关，那就是 Token。\nToken 是大模型处理文本的\u0026quot;基本单位\u0026quot;，它既不是一个字，也不是一个词，你可以理解为把文字\u0026quot;切碎\u0026quot;后的一个个小碎片。\n📷 [图片 token=QnGvb75sYocNXwxbxb6c0eOGnjd（未能下载，见飞书原文）]\n举个例子，「我今天心情很好」这句话，大模型不是一个字一个字地读，而是先把它切成若干个 Token，比如可能切成：「我」「今天」「心情」「很好」，每个碎片就是一个 Token。\n英文的切法更直观：\u0026quot;Hello world\u0026quot; 通常会被切成 [\u0026quot;Hello\u0026quot;, \u0026quot; world\u0026quot;]，大约每个单词对应 1 个 Token。中文切法不太一样，通常每个汉字对应 1～2 个 Token。\n你为什么要了解 Token？有两个原因：\n决定模型能处理的文本长度上限：每个大模型都有\u0026quot;最多能处理多少 Token\u0026quot;的上限，后面马上会讲到\n决定 API 调用费用：调用大模型的 API 是按 Token 计费的，你发给它的文字越长、它回复的越多，花的钱越多\n记住这个概念，我们继续。\n上下文窗口：大模型的\u0026quot;短期记忆\u0026quot; 理解了 Token，你就能理解大模型最重要的一个限制，那就是上下文窗口（Context Window）。\n上下文窗口，指的是大模型每次对话时，能\u0026quot;看到\u0026quot;的内容总量上限，用 Token 来衡量。\n你可以把它想象成一块固定大小的白板。每次对话，你说的话、模型的回复、系统提示、工具调用结果……全都要写在这块白板上。白板写满了，就得从最开头的地方擦掉，给新内容腾地方。\n所以，和大模型聊天聊得越久，它越容易\u0026quot;忘掉\u0026quot;前面发生的事，因为早期的对话内容已经被从白板上擦掉了。\n这块白板有多大？不同模型差别很大：\nGPT-4 有 128K Token 的窗口，大约相当于 10 万汉字，差不多是一本中等厚度的小说\nClaude 3.5 的上下文窗口有 200K Token，更大一些\n一些面向长文档场景的模型，窗口可以达到 1M Token 以上\n听起来好像很大？但这块白板里装的东西可不少：你发出的所有消息 + 模型所有的回复 + 开头的系统提示词 + 每次工具调用的结果……全都要占地方。\n📷 [图片 token=WmuNbNfrqoFXryxD4s9ceuyPnad（未能下载，见飞书原文）]\n实际影响是什么？\n聊天聊久了，模型会\u0026quot;失忆\u0026quot;，忘掉对话前期的细节\n你把一本几百页的产品手册直接丢给它，它要么记不全，要么读了后面忘了前面，回答错漏百出\n你的知识库内容再多，也没法全塞进这个窗口\n这就是为什么我们后面要学 RAG（检索增强生成），它的核心思路就是：不把全部知识塞进上下文窗口，而是每次用的时候，按需检索最相关的片段放进去。上下文窗口的限制，是 RAG 存在的根本原因。\n大模型是\u0026quot;无状态\u0026quot;的，没有任何记忆 理解了上下文窗口，还有一件事必须说清楚，因为很多初学者在这里会踩坑。\n大模型本身没有任何记忆。\n它是完全\u0026quot;无状态\u0026quot;的。每次你通过 API 调用它，对它来说都是第一次见面，它完全不记得你们上次聊过什么、你叫什么名字、你有什么偏好。\n📷 [图片 token=CnfkbYHN2oXdrJxENLgcUkmJn4c（未能下载，见飞书原文）]\n那为什么我们用 ChatGPT 聊天，它好像能记住之前说过的话？\n因为是你的应用代码在负责\u0026quot;记忆\u0026quot;。每次你发消息，应用都会把你们之前所有的对话历史，打包成一个列表，一起传给大模型。大模型是\u0026quot;当场\u0026quot;看完这份历史记录，才\u0026quot;看起来\u0026quot;记住了你说过的话。\n类比一下：就像一个每次见面都失忆的助理。你必须在每次见面时，把之前聊过的所有内容重新告诉他，他才能\u0026quot;接着上次\u0026quot;继续工作。你以为他记住了，其实是你每次都递给了他一份完整的\u0026quot;历史记录\u0026quot;。\n这直接解释了两件事：\n为什么上下文窗口会写满：历史对话越来越多，白板自然越来越满\n为什么 Agent 需要管理记忆：Agent 在执行长任务时，必须主动决定\u0026quot;把哪些历史传进去\u0026quot;，不然窗口很快就塞满了\n对话的角色结构：system、user、assistant 既然多轮对话是靠\u0026quot;传历史列表\u0026quot;实现的，那这个列表长什么样？\n调用大模型 API 时，你传入的不是一段裸文字，而是一个有结构的消息列表，每条消息都有一个角色（role）：\nsystem：系统提示，给模型的\u0026quot;身份设定和行为规则\u0026quot;。比如「你是一个 OnCall 助理，只能回答和系统故障相关的问题，回答必须简洁」。这部分用户看不见，但模型会严格遵守\nuser：用户的输入，就是你说的话\nassistant：模型之前的回复，多轮对话时，把历史回复也打包进来，模型才知道\u0026quot;之前说了什么\u0026quot;\n用伪代码表示，大概长这样：\nmessages = [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一个 OnCall 助理，回答必须简洁\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;昨晚数据库报警是什么原因？\u0026#34; }, { role: \u0026#34;assistant\u0026#34;, content: \u0026#34;是慢查询导致的连接池耗尽。\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;怎么预防？\u0026#34; }, // 当前问题 ] 模型看到这整个列表，才能知道\u0026quot;用户在问什么、之前说过什么、我应该怎么回答\u0026quot;。\n这个结构非常重要，后面学 Prompt 的时候，我们会专门讲怎么用好 system 角色，它是控制模型行为最核心的手段。\n大模型能帮我们做什么 有了上面的基础认知，现在来看大模型真正擅长什么。核心是 4 件事，也是我们后续开发全程都会用到的能力：\n📷 [图片 token=TQ5ubUpu6oUM68x6dFpcsfDDnle（未能下载，见飞书原文）]\n1. 听得懂人话，精准理解意图 你不用写复杂的代码指令，不用抠严谨的格式，只用日常大白话讲需求，它就能精准 get 到你想干嘛。\n举个例子，你说「帮我把这段生硬的概念，改成小白能听懂的口语化教程」，它能立刻明白你的要求 ， 你不需要定义什么叫\u0026quot;生硬\u0026quot;、什么叫\u0026quot;小白\u0026quot;、什么叫\u0026quot;口语化\u0026quot;，它自己就能理解。\n这是所有 AI 应用能运转的基础。\n2. 会逻辑思考，能拆解任务、做决策 给它一个明确的目标，它能拆解出一步步的执行步骤，还能判断「下一步该做什么」。\n举个例子，你说「帮我规划一场周末的短途旅行」，它能拆解出查天气、定路线、找住宿、做预算这些步骤，并按顺序推进。这种拆解任务和做决策的能力，就是我们后面 Agent 的「决策大脑」。\n3. 能生成符合要求的各类内容 写文案、写代码、写总结、回答问题、梳理逻辑，只要你给它明确的要求和参考信息，它就能生成贴合需求的内容。\n这是它最终给用户\u0026quot;交付结果\u0026quot;的核心能力。\n4. 能看懂规则，严格按要求执行 你给它定好明确的规矩，比如「必须严格按照我给的参考资料回答，不许自己瞎编」，或者「必须按照我给的固定格式，告诉我要调用哪个工具」，它就能严格遵守，不会乱发挥。\n这个能力是我们后面学 Function Calling、RAG 的核心前提 ， 如果模型不能严格按规则执行，整个系统就会乱套。\n大模型做不到什么 这部分尤其重要。因为它直接对应了我们后面要学的所有技术的价值。\n你搞懂了大模型的短板，就能立刻明白「我们为什么要开发 Agent」「为什么要学这么多技术」。\n📷 [图片 token=OIKYbrDoaoK22IxxgdWchWD0nFd（未能下载，见飞书原文）]\n1. 没有执行能力，不能和外部世界交互 这是最核心的短板。大模型只有\u0026quot;嘴\u0026quot;，没有\u0026quot;手\u0026quot;。你让它帮你读电脑里的文件、查今天的实时天气、给客户发一封邮件、在数据库里查一条记录，它只能给你写步骤，没法自己去做这些事。它也没法主动连接数据库、调用第三方 API、浏览网页，或者和外部的任何系统交互，只能在自己的\u0026quot;脑子里\u0026quot;转。\n这就是我们后面要学 Agent + Tool 工具调用、Function Calling 和 MCP 协议的核心原因：给大模型配上能动手的工具，打通它和真实世界之间的通道，让它从只会出主意，变成能干活。\n2. 知识有\u0026quot;保质期\u0026quot;，还容易瞎编 大模型的知识库是在某个时间点截止的，训练截止日期之后发生的事，它不知道。而且你公司内部的产品手册、专属的制度文档、你自己写的私人笔记，它根本没见过，也不知道。\n更麻烦的是，遇到它不知道的内容，它不一定会说\u0026quot;我不知道\u0026quot;，而是很可能一本正经地给你编一堆看起来很专业、实际全是错的内容。行业里管这个叫**「幻觉」（Hallucination）**。\n这就是我们后面要学 **RAG（检索增强生成）**的核心原因：把最新的、专属的知识存起来，让模型回答的时候能调取正确的参考资料，而不是靠\u0026quot;记忆\u0026quot;瞎猜。\n3. 上下文窗口有上限，记不住太长的内容 前面已经讲过了。白板就这么大，你把一本几百上千页的电子书、产品手册扔给它，它根本记不住，会读了后面忘前面，回答错漏百出。\n这也是 RAG 要解决的问题：不把全部内容塞进窗口，而是按需检索最相关的片段。\n总结 讲到这里，整个画面就清晰了：\n大模型是一个超级能思考、能理解、能生成内容的\u0026quot;大脑\u0026quot;。\n但它没有\u0026quot;手\u0026quot;（不能执行操作，不能对接外部世界），没有\u0026quot;专属知识库\u0026quot;（不知道你的私有数据），没有持久记忆（每次调用对它都是全新的），而且单次对话能看到的内容也有上限（上下文窗口）。\n单靠它自己，只能做聊天、出主意这类事，没法落地完成复杂的、具体的任务。\n而我们要学的 Agent 开发，本质上就是：\n给这个强大的\u0026quot;大脑\u0026quot;，配上能干活的\u0026quot;手\u0026quot;（Tool 工具调用、Function Calling、MCP）、能存专属知识的\u0026quot;知识库\u0026quot;（RAG），让它从一个只能聊天的助理，变成一个能全自动落地完成任务的智能体。\n后面要学的每一个技术，都是在补大模型的某块短板。带着这个认知往下学，你会发现每件事都有\u0026quot;为什么要这么做\u0026quot;的清晰答案。\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BD%9CAI%20%E5%90%8D%E8%AF%8D%E5%A4%A7%E6%89%AB%E7%9B%B2/%E4%BB%80%E4%B9%88%E6%98%AF%E5%A4%A7%E6%A8%A1%E5%9E%8B%EF%BC%9F/","summary":"学 Agent 开发，第一件事是认识它的\u0026quot;大脑\u0026quot;：  大模型 。  你不需要先搞懂它背后的数学原理，但你必须清楚它能做什么、不能做什么，以及它是怎么\u0026quot;看\u0026quot;你发给它的文字的。搞清楚这些，后面学 Prompt、Function Calling、","title":"什么是大模型？"},{"content":" [!NOTE] 现在很多大厂不仅对AI能力的考察，不局限于Agent开发的方向，还会额外考察 AI 编程的能力。 为了让林友们，不仅能学到 Agent 项目，还能掌握 AI 编程的工程化。 所以，特意加餐实现了「智能OnCall Agent项目」 AI 编程开发实战教程，这里会用 Codex+OpenSpec 工程实战来用 AI 来开发项目。\n[!WARNING] 教程地址：[AI Native 工程化实战（智能OnCall Agent）](/oncall/AI Native 工程化实战（智能OnCall Agent）/)\n📷 [图片 token=DrzLbVsg2oGUyXxi59Nc1MzpnoN（未能下载，见飞书原文）]\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E5%8A%A0%E9%A4%90%E7%AF%87%EF%BC%9AAI%20Native%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98/","summary":"!NOTE  现在很多大厂不仅对AI能力的考察，不局限于Agent开发的方向，还会额外考察 AI 编程的能力。  为了让林友们，不仅能学到 Agent 项目，还能掌握  AI 编程的工程化。  所以，特意加餐实现了「智能OnCall Age","title":"加餐篇：AI Native工程化实战"},{"content":"OncallAgent 的 AIOps 诊断不是把一条告警直接交给大模型，然后把生成文字当成根因。真实链路先读取外部活跃告警，再检索当前用户有权访问的知识文档，随后调用真实工具取证，并把步骤、工具审计、证据、checkpoint 和报告分别持久化。检索结果可能来自 SOP、诊断案例或普通知识文档；报告提示词要求只总结已有事实，并在证据不足时明确表达不确定性。当前结构校验并不能从语义上证明每句话都由证据支撑，阅读报告时仍要回看证据链接。\n这条链路由 FastAPI、SQLite durable job、LangGraph、RAG、MCP 和 SSE 共同完成。理解它时不要只盯着 Planner → Executor → Replanner → Report 四个节点名称，而要继续追问：输入从哪里来，工具是否真实调用，哪些记录进入 SQLite，报告和证据如何关联，浏览器断开后任务是否继续，以及失败怎样被用户看到。\n📷 [图片 token=YFiEbWDmfo56qGxattQcJcvTnye（未能下载，见飞书原文）]\n学习目标 掌握从活跃告警到诊断任务、后台执行、证据报告和案例沉淀的完整调用链。\n理解 LangGraph 四个节点在当前实现中的真实职责和边界。\n区分实时 SSE 事件、持久化步骤、工具审计、证据记录与最终报告。\n能够从代码判断一个根因结论是否有知识引用或真实工具结果支撑。\n功能入口与完整调用链 活跃告警入口由 GET /aiops/alerts/active 提供。apps/backend/src/super_ai/alerts.py 可以读取 Alertmanager v2 或 Prometheus v1 API，只保留 active 或 firing 状态，并规范化为名称、服务、级别、开始时间、摘要和来源上下文。多个数据源中只要有一个成功，就返回成功源的真实告警；全部失败才返回统一的服务不可用错误。\n📷 [图片 token=As70bI2tOocbBPxwvr2c367qn8e（未能下载，见飞书原文）]\n前端 apps/frontend/src/stores/aiops.ts 的 diagnoseAlert 把规范化告警转为诊断查询，同时保留 alertSource、alertName、service、severity、status、startsAt 和原始 context。POST /aiops/diagnostics 先创建 owner-scoped 诊断任务，再入队一个 aiops_diagnosis 后台任务并返回 202。真正的 LangGraph 执行发生在后台 worker，不占住创建请求。\n📷 [图片 token=UOnpbAOMFoQtotxfmzYcV9p9n3g（未能下载，见飞书原文）]\n浏览器随后订阅 POST /aiops/diagnostics/{diagnostic_id}:stream。这个端点不是直接调用图，而是持续读取持久化的 background job events 并编码为 SSE。即使页面刷新或网络断开，worker 仍可继续；重新打开任务时，前端再通过 /evidence-chain 读取已经落库的步骤、审计、证据、报告、链接和 checkpoint。\n📷 [图片 token=FKTMbnCLmowq8wxIWxXcK9WGnhb（未能下载，见飞书原文）]\n外部 Alertmanager / Prometheus → GET /aiops/alerts/active → 规范化活跃告警并保留来源 context → POST /aiops/diagnostics → SQLite diagnostic task + background job → Planner：tenant 知识检索 + MCP 工具发现 + 有界计划 → Executor：真实 knowledge_retrieval 或 MCP 调用 → Replanner：根据计划位置和失败状态决定继续或转 Report → Report：证据约束的 Markdown 报告 + fallback → SQLite steps / audits / evidence / checkpoints / report links → 持久事件 SSE + evidence-chain 回读 → 成功时自动生成诊断案例并安排普通文档索引 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/alerts.py AggregatedAlertProvider、_normalize_alert 读取真实 Prometheus/Alertmanager API，过滤并规范化活跃告警，保留来源上下文。 apps/backend/src/super_ai/api/app.py create_aiops_diagnostic、stream_aiops_diagnostic 创建 owner-scoped 任务和后台 job，并从持久事件提供 SSE 订阅。 apps/backend/src/super_ai/aiops/diagnostics.py AiopsDiagnosticService 构建并运行 LangGraph，组织知识引用、工具证据、报告和案例触发。 apps/backend/src/super_ai/retrieval/tool.py KnowledgeRetrievalTool 在当前用户和授权知识库范围内检索已索引文档，返回结果和可追溯引用。 apps/backend/src/super_ai/mcp_connections.py McpConnectionService.client_for_user 为当前诊断 owner 装配已启用 MCP 连接。 apps/backend/src/super_ai/mcp_client.py LocalMcpClient.call_tool 调用真实 MCP 工具，执行超时、重试和明确失败。 apps/backend/src/super_ai/memory/repositories.py DiagnosticMemoryRepository 定义任务、步骤、证据、报告、链接、案例与 checkpoint 的持久化边界。 packages/api-contracts/src/protected-data.ts AiopsDiagnosticEvidenceChain 定义前端读取的完整诊断证据链结构。 packages/api-contracts/src/sse.ts SseEvent 定义任务状态、工具调用、引用、报告、完成和错误等判别事件。 apps/frontend/src/stores/aiops.ts useAiopsStore 串联告警、创建任务、消费 SSE、回载证据链和案例列表。 代码调用流程图 诊断链路把规划、真实执行、状态判断和报告拆成四个节点，并把每一步产生的事件、审计、证据和 checkpoint 保存下来。\n📷 [图片 token=LLp3b6cxVouM24xk6eSc7ONOnmh（未能下载，见飞书原文）]\n关键实现拆解 Planner 先检索授权知识，再规划真实工具步骤 AiopsDiagnosticService.stream 首先把任务状态改为 running。如果输入包含告警，它会创建一条 kind=alert 的证据，保留原始输入。随后 _planner 以当前用户 ID 和 accessible_knowledge_base_ids 调用 KnowledgeRetrievalTool，查询 top 3 结果；该调用本身也创建 knowledge_retrieval 工具审计。\n📷 [图片 token=IQ0ZbccgfoltPsxHXZecV3LznjS（未能下载，见飞书原文）]\n看什么：Planner 的首个真实动作不是问模型，而是创建审计并在 owner 与授权知识库集合内调用检索工具。调用参数里没有扩大范围的默认知识库。\nawait self._create_audit( owner_user_id=owner_user_id, task_id=task_id, audit_id=retrieval_audit_id, tool_name=\u0026#34;knowledge_retrieval\u0026#34;, arguments={\u0026#34;query\u0026#34;: query}, ) retrieval_result: KnowledgeRetrievalToolResult | None = None retrieval_error: str | None = None try: # 1. query 与 top_k 不能替代 owner 和授权知识库边界。 retrieval_result = await self._retrieval_tool.run( KnowledgeRetrievalToolInput(query=query, top_k=3), owner_user_id=owner_user_id, accessible_knowledge_base_ids=cast( Sequence[str], state[\u0026#34;accessible_knowledge_base_ids\u0026#34;] ), ) except KnowledgeRetrievalError as exc: # 2. 检索失败进入显式失败状态，不伪造命中。 retrieval_error = exc.message 这段代码证明 tenant 范围是检索调用的必需输入，而不是报告阶段的事后过滤。失败边界也很明确：检索异常被记录为 retrieval_error，后续仍可生成受限计划和失败说明，但不能把空结果改写成一条示例 SOP。\n检索成功时，Planner 记录知识命中、引用事件和 knowledge_reference 证据；无命中时设置 no_sop_matched，后续报告必须说明使用通用计划；检索失败则保存安全错误，并发出规范化错误事件。这里的 sop_hits 与 no_sop_matched 是当前内部字段名，但调用只传入 query 和 topK，并没有附加 knowledgeType=sop 过滤条件，因此这些字段可能承载 SOP、案例或普通知识文档。之后 Planner 从当前用户 MCP 客户端发现真实工具名，并把这些名称作为模型规划的白名单。\n📷 [图片 token=HzXCbCy7fo6Xk4xK7cWcVFQinac（未能下载，见飞书原文）]\n当前实现对模型规划做了较强约束。_validated_plan 只接受发现到的工具或 knowledge_retrieval；_create_plan 最终只保留一个 SearchLog 步骤；_normalized_search_log_step 使用项目配置的 CLS region、topic 和最近 24 小时时间窗，只允许模型在 1 到 100 之间调整 Limit。若模型响应无效或没有可执行的 SearchLog，系统回退到同样受限的通用日志查询，而不是执行模型任意生成的工具名和参数。\n📷 [图片 token=XheYbWOeqoqxD8xp3Snc6rvHnJd（未能下载，见飞书原文）]\n看什么：模型输出先通过“已发现工具”白名单，随后 _create_plan 又只选择第一条 SearchLog；没有可用步骤时返回本地构造的通用计划。\ngeneric_plan = [self._generic_search_log_step(query)] # … 省略 prompt 拼接 try: response = await self._llm_provider.create_chat_model().ainvoke(prompt) # 1. 模型步骤先受真实发现工具名白名单约束。 plan = _validated_plan(_model_text(response), available_tools) except Exception: plan = [] search_log_steps = [step for step in plan if step.get(\u0026#34;tool\u0026#34;) == \u0026#34;SearchLog\u0026#34;] if not search_log_steps: # 2. 无合法 SearchLog 时使用确定性的通用步骤。 return generic_plan, \u0026#34;generic\u0026#34; return [self._normalized_search_log_step(search_log_steps[0], query)], ( \u0026#34;SOP-backed\u0026#34; if sop_hits else \u0026#34;generic\u0026#34; ) 它证明当前 Planner 并不是开放式多工具计划器：即使白名单允许其他真实工具，最终执行计划仍被收窄为一个 SearchLog。模型不可用、JSON 无效或缺少该工具时都会回退，不会把模型原始参数直接交给 MCP。\n看什么：再看参数归一化。项目侧生成 region、topic、24 小时时间窗与查询表达式，模型只可能改变合法范围内的 Limit。\n\u0026#34;\u0026#34;\u0026#34;Keep model planning bounded to an executable real CLS search step.\u0026#34;\u0026#34;\u0026#34; generic_step = self._generic_search_log_step(query) model_arguments = _json_dict(step.get(\u0026#34;arguments\u0026#34;)) limit_value = model_arguments.get(\u0026#34;Limit\u0026#34;) arguments = _json_dict(generic_step[\u0026#34;arguments\u0026#34;]) # 1. 只有 1 到 100 的整数 Limit 能覆盖本地参数。 if isinstance(limit_value, int) and 1 \u0026lt;= limit_value \u0026lt;= 100: arguments[\u0026#34;Limit\u0026#34;] = limit_value return { \u0026#34;id\u0026#34;: str(step.get(\u0026#34;id\u0026#34;) or generic_step[\u0026#34;id\u0026#34;]), \u0026#34;tool\u0026#34;: \u0026#34;SearchLog\u0026#34;, # 2. 其余 arguments 始终来自 generic_step。 \u0026#34;arguments\u0026#34;: arguments, \u0026#34;purpose\u0026#34;: str(step.get(\u0026#34;purpose\u0026#34;) or generic_step[\u0026#34;purpose\u0026#34;]), } 这段代码证明模型不能改写 CLS region、topic、时间窗或 Query。边界是：当前实现固定查询最近 24 小时且 Query 为本地通用值，这提高了可执行性，却也意味着文档不能声称模型已能根据 SOP 自由组合任意诊断参数。\n看什么：下面的局部流程图把“知识检索结果”和“工具发现结果”如何共同进入受限计划画开，避免误读成一次模型调用同时完成检索和执行。\n这张图证明 SOP 命中影响 planOrigin 与报告语义，但不会绕过真实工具发现或参数归一化。检索失败、无命中、发现失败和模型失败都有独立落点，任何一个都不能制造不存在的证据。\n📷 [图片 token=QbLXbVrspo1n1yxZNHZcCvMOnxg（未能下载，见飞书原文）]\nExecutor 将工具结果转换成证据，而不是直接变成结论 _executor 读取当前计划步骤，先发出 tool.call started，并在 SQLite 创建状态为 started 的审计。知识检索步骤继续走 tenant 过滤的 KnowledgeRetrievalTool；其他非空工具名通过当前用户的 LocalMcpClient.call_tool 执行。成功结果会产生完成事件、结果摘要、executor step、带工具关联的 evidence 记录和 checkpoint。\n📷 [图片 token=KveEbd2rYoTqSwxx3QxcWUkmnwg（未能下载，见飞书原文）]\n看什么：Executor 保留两条不同的执行边界——知识检索必须再次携带 tenant 参数，其他工具则从当前 owner 的 MCP provider 取得客户端后真实调用。\ntry: if tool_name == \u0026#34;knowledge_retrieval\u0026#34;: result = await self._retrieval_tool.run( KnowledgeRetrievalToolInput( query=str(arguments.get(\u0026#34;query\u0026#34;) or state[\u0026#34;query\u0026#34;]), top_k=_optional_int(arguments.get(\u0026#34;topK\u0026#34;)), ), # 1. 诊断内的再次检索仍显式携带 owner 与授权知识库。 owner_user_id=owner_user_id, accessible_knowledge_base_ids=cast( Sequence[str], state[\u0026#34;accessible_knowledge_base_ids\u0026#34;] ), ) output: object = { \u0026#34;results\u0026#34;: [_sop_hit_payload(hit) for hit in result.results], \u0026#34;citations\u0026#34;: [_citation_payload(citation) for citation in result.citations], } elif tool_name: # 2. MCP 客户端同样按当前诊断 owner 装配。 mcp_client = await self._mcp_client_for(owner_user_id) output = await mcp_client.call_tool(tool_name, arguments) else: raise ValueError(\u0026#34;Diagnostic plan did not specify a tool.\u0026#34;) 它证明 Executor 不会从全局工具池执行计划，也不会因为计划里出现 knowledge_retrieval 就跳过知识库授权。失败或空工具名进入异常分支，不能被转换为 completed 证据。\nSearchLog 的摘要并不是把 MCP 原始返回全文无限保存。_search_log_records 只解析可识别的日志列表，并抽取 timestamp、level、service、host、event、message、latency、exception 和 request ID 等有限字段；_tool_result_summary 最多保留十条记录。若没有可解析日志，它明确记录“CLS 未返回可解析日志”，不会创造一条示例异常。\n看什么：摘要函数只为 SearchLog 进入字段级解析，返回记录最多取前十条；其他工具仍走长度受限的 JSON 编码。\ndef _tool_result_summary(tool_name: str, output: object) -\u0026gt; str: if tool_name != \u0026#34;SearchLog\u0026#34;: return _bounded_json(output) records = _search_log_records(output) if not records: # 1. 无法解析时保存明确的零记录事实。 return json.dumps( {\u0026#34;recordCount\u0026#34;: 0, \u0026#34;records\u0026#34;: [], \u0026#34;message\u0026#34;: \u0026#34;CLS 未返回可解析日志。\u0026#34;}, ensure_ascii=False, separators=(\u0026#34;,\u0026#34;, \u0026#34;:\u0026#34;), ) # 2. 审计与证据摘要最多保留十条结构化记录。 return json.dumps( {\u0026#34;recordCount\u0026#34;: len(records), \u0026#34;records\u0026#34;: records[:10]}, ensure_ascii=False, separators=(\u0026#34;,\u0026#34;, \u0026#34;:\u0026#34;), ) 这段代码证明“没有可解析日志”与“工具调用失败”是不同事实：前者可形成 completed 的零记录摘要，后者进入 failed 分支。它也说明十条是摘要上限而非 MCP 查询上限，原始工具 payload 与摘要的保留范围不同。\n📷 [图片 token=MjjUbrvchot8TzxkirQcZl3YnZk（未能下载，见飞书原文）]\n调用抛错时，Executor 通过 _safe_error 清理常见凭据格式，完成失败审计，创建失败步骤和失败 evidence，并把 execution_failed 设为真。工具失败之后仍会进入 Report，目的是输出一份说明失败和证据缺口的报告，而不是让界面停在无解释的空白状态。\n📷 [图片 token=U8AubzL6go0Tiyxh27Gc1e2Tnyd（未能下载，见飞书原文）]\n看什么：失败分支同时写四类事实——失败 SSE、审计终态、executor step 与 evidence；返回状态中的 execution_failed=True 决定后续 Replanner 不再继续。\nexcept Exception as exc: safe_error = _safe_error(exc) evidence: JsonDict = { \u0026#34;stepId\u0026#34;: str(step.get(\u0026#34;id\u0026#34;) or f\u0026#34;step_{plan_index + 1}\u0026#34;), \u0026#34;tool\u0026#34;: tool_name or \u0026#34;unknown\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;failed\u0026#34;, \u0026#34;summary\u0026#34;: safe_error, } # … 省略失败事件和审计 finalize executor_step = await self._create_step( owner_user_id=owner_user_id, task_id=task_id, phase=\u0026#34;executor\u0026#34;, status=\u0026#34;failed\u0026#34;, payload={\u0026#34;planStep\u0026#34;: step, \u0026#34;tool\u0026#34;: tool_name, \u0026#34;error\u0026#34;: safe_error}, ) # … 省略同 owner/task 的失败 evidence 创建 # 1. 失败仍推进索引，但设置终止后续执行的状态。 return { \u0026#34;plan_index\u0026#34;: plan_index + 1, \u0026#34;execution_failed\u0026#34;: True, \u0026#34;evidence\u0026#34;: [evidence], \u0026#34;evidence_ids\u0026#34;: [evidence_record.id], \u0026#34;events\u0026#34;: events, } 它证明失败不是静默丢弃，也不会被包装成成功结果。安全边界是 _safe_error 只清理当前规则识别的常见 key 形式并截断到 500 字符；调用方仍不应把凭据放进异常文本。\n看什么：下面的时序图强调工具输出先转为审计、步骤和 evidence，Report 最后才消费这些来源；这正是“证据不等于结论”的代码结构。\n这张图证明报告之前存在可独立查询的持久化执行轨迹。若某次写入本身异常，图节点可能整体失败并由外层任务处理；文档不能假设 SSE、审计、步骤和 evidence 在任意基础设施故障下仍必然同时成功。\n📷 [图片 token=UkwvbudSKo5f7Wx84R1cvmDGnXe（未能下载，见飞书原文）]\nReplanner 和 Report 的实际边界 当前 _replanner 并不会重新调用模型生成一份新计划。它根据 plan_index、计划长度和 execution_failed 判断是否继续下一个既有步骤；存在失败或步骤执行完毕时转入 Report。由于当前计划又被归一化为单个 SearchLog，常见路径是执行一次真实日志查询后进入报告。阅读文档时必须以这段现有代码为准，不能把它描述成已经实现任意多轮动态改写计划。\n📷 [图片 token=XiWdbscwYoqb30xsR5JcyG9dnAg（未能下载，见飞书原文）]\n看什么：Replanner 只有一个布尔判断；它保存决策步骤与 checkpoint，但代码中没有第二次 ainvoke 或计划内容更新。\nplan = cast(list[JsonDict], state.get(\u0026#34;plan\u0026#34;) or []) plan_index = int(state.get(\u0026#34;plan_index\u0026#34;) or 0) execution_failed = bool(state.get(\u0026#34;execution_failed\u0026#34;)) # 1. 仅当还有既有步骤且此前未失败时继续。 continue_execution = plan_index \u0026lt; len(plan) and not execution_failed decision = ( \u0026#34;continuing with the next bounded step\u0026#34; if continue_execution else \u0026#34;moving to Report\u0026#34; ) # … 省略 task.status 事件 await self._create_step( owner_user_id=str(state[\u0026#34;owner_user_id\u0026#34;]), task_id=task_id, phase=\u0026#34;replanner\u0026#34;, status=\u0026#34;completed\u0026#34;, payload={ \u0026#34;planIndex\u0026#34;: plan_index, \u0026#34;planLength\u0026#34;: len(plan), \u0026#34;executionFailed\u0026#34;: execution_failed, # 2. 决策只在 executor 与 report 之间选择。 \u0026#34;decision\u0026#34;: \u0026#34;executor\u0026#34; if continue_execution else \u0026#34;report\u0026#34;, }, ) 它证明“Replan”在当前实现中是对既有有界计划的路由判断，而不是动态生成新步骤。失败边界也很保守：一旦 execution_failed 为真，即使计划中仍有步骤，也会直接进入 Report。\n_report 先构造确定性的 fallback，再请求配置的聊天模型生成规定结构的中文 Markdown。_report_prompt 明确禁止编造告警、日志、根因和执行结果；缺失字段要写“未获取”，证据不足要写“证据不足，无法确认根因”。_clean_markdown_report 还会拒绝 JSON 或缺少必要标题的输出。模型异常或结构不合格时，系统持久化 fallback 报告，保留知识未命中、工具失败和证据不足状态。\n📷 [图片 token=RTtdbpH5ho3orTxxO5tcVoNEnFd（未能下载，见飞书原文）]\n看什么：报告生成先算出 fallback，再尝试模型；模型异常、空结构、JSON 或缺标题最终都会落回同一份确定性 Markdown。\nfallback = _fallback_report_content( alert=_json_dict(state.get(\u0026#34;alert\u0026#34;)), no_sop_matched=bool(state.get(\u0026#34;no_sop_matched\u0026#34;)), sop_hits=cast(list[JsonDict], state.get(\u0026#34;sop_hits\u0026#34;) or []), evidence=cast(list[JsonDict], state.get(\u0026#34;evidence\u0026#34;) or []), execution_failed=bool(state.get(\u0026#34;execution_failed\u0026#34;)), ) prompt = _report_prompt(state) try: # 1. 模型失败不阻止已有证据形成报告。 response = await self._llm_provider.create_chat_model().ainvoke(prompt) except Exception: return fallback, \u0026#34;fallback\u0026#34; report = _clean_markdown_report(_model_text(response)) return (report, \u0026#34;llm\u0026#34;) if report is not None else (fallback, \u0026#34;fallback\u0026#34;) # … 省略其他报告辅助函数 # 2. JSON 或缺少必需标题的内容不进入持久报告。 if report.startswith((\u0026#34;{\u0026#34;, \u0026#34;[\u0026#34;)): return None if not all(heading in report for heading in AIOPS_REPORT_REQUIRED_HEADINGS): return None return report 这段代码证明模型只是报告呈现的一种实现，持久化完成并不依赖模型一定成功。它不证明 fallback 已确认根因；相反，fallback 会根据 execution_failed 和证据缺口明确保留“未获取”与“无法确认”。\n看什么：最后用状态图读取真实图路由。当前单步计划通常一次执行后就报告；图中保留回到 Executor 的通路，是为了既有计划长度大于当前索引时使用，而不是模型重新规划。\n这张图证明终态由 Executor 是否失败决定，而不是“最终是否成功写出一篇报告”决定；失败任务仍可拥有 fallback 报告。只有状态为 succeeded 时，后续自动案例沉淀才会被触发。\n数据、契约与状态 一项诊断同时存在多个不同粒度的记录。DiagnosticTaskRecord 保存总体状态、查询、输入和结果；DiagnosticStepRecord 保存 Planner、Executor、Replanner、Report 的顺序和结构化 payload；AgentToolCallAuditRecord 保存工具调用生命周期；DiagnosticEvidenceRecord 保存 alert、knowledge reference 或 log 等证据；ReportEvidenceLinkRecord 将报告和证据显式关联；GraphCheckpointRecord 保存每个图节点的 checkpoint。\n📷 [图片 token=UNBnbJV5eoT7PsxXKg6cIhRen1g（未能下载，见飞书原文）]\nSSE 是实时传输协议，不是唯一事实来源。packages/api-contracts/src/sse.ts 用 type 和 channel 区分 task.status、tool.call、reference.source、report、complete 和 error。后台运行时先把这些 payload 存入 job event，再由订阅接口按 sequence 读取。浏览器断开不会删除任务；重连后的最终状态通过 SQLite 证据链恢复。\n📷 [图片 token=VY2DblDwCocymtxwyVZcIjmLnih（未能下载，见飞书原文）]\n报告完成时，任务只有在未发生执行失败时才标为 succeeded；否则为 failed，但仍可能有一份 fallback 报告解释失败。成功报告会逐一创建 report-evidence link，并可触发自动案例沉淀。因而“有报告”不必然表示诊断成功，“任务成功”也不表示系统已经执行处置动作：当前链路负责取证和建议，不会把建议写成已经执行的变更。\n权限、安全与失败边界 诊断 task、历史、流、证据链、报告、案例和工具审计都以 owner_user_id 查询。访问其他用户的 diagnostic ID 会得到统一权限错误。知识检索还必须携带当前用户可访问的知识库集合；授权集合为空时应返回空结果，而不是扩大到全局搜索。MCP 工具同样从当前用户启用连接装配。\n📷 [图片 token=HmJmbJ2mAoLegJxlUoscQebHnie（未能下载，见飞书原文）]\n外部告警列表是配置数据源的实时视图，不是每个用户独立保存的资源，但该接口仍要求认证。数据源部分成功时只返回成功来源；全部失败时不展示旧告警或模拟告警。普通启动流程也不会自动发布演示告警、上传 CLS 日志或写入 SOP，这些都必须由开发者显式执行。\n最重要的可信边界是：工具结果、日志、根因和成功状态都不应被伪造。检索失败、MCP 发现失败、工具调用失败、模型报告失败都在代码中有独立分支。_report_prompt 明确要求模型只使用已有事实，处置建议也不代表真实操作已经执行；但 _clean_markdown_report 只检查基本 Markdown 结构，不执行基于 evidence 的语义 grounding 校验。只要必要标题齐全，一段内容并不会因为通过结构校验就自动成为可信事实，因此最终结论必须与持久证据逐项对照。\n阅读顺序与小结 从 alerts.py 和 useAiopsStore.diagnoseAlert 看告警输入如何保留来源。\n阅读 api/app.py 的创建、流和证据链路由，理解后台任务与 SSE 的分离。\n按 _planner、_executor、_replanner、_report 顺序阅读诊断服务。\n最后对照 repository 与共享契约，核对每项用户可见结果是否都有持久证据。\nOncallAgent 的诊断闭环真正有价值的部分，是把“模型给出的文字”拆回一条可查询的事实链。告警有来源，知识命中有引用，工具有审计，日志有摘要，报告有证据链接，失败也有状态。只有这些对象能够互相对应，AIOps Agent 才能成为工程系统，而不只是一次性问答。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/12.%20%E5%91%8A%E8%AD%A6%E5%88%B0%E8%AF%81%E6%8D%AE%E6%8A%A5%E5%91%8A%E7%9A%84%20LangGraph%20%E8%AF%8A%E6%96%AD%E9%97%AD%E7%8E%AF/","summary":"OncallAgent 的 AIOps 诊断不是把一条告警直接交给大模型，然后把生成文字当成根因。真实链路先读取外部活跃告警，再检索当前用户有权访问的知识文档，随后调用真实工具取证，并把步骤、工具审计、证据、checkpoint 和报告分别","title":"12. 告警到证据报告的 LangGraph 诊断闭环"},{"content":"windows跑java项目make init失败 📷 [图片 token=ZJQUbRjWfo91L1xBLcJcQUEdngb（未能下载，见飞书原文）]\n这是因为你的终端不是git bash。请把终端换成git bash运行：https://blog.csdn.net/w8y56f/article/details/127152802\ngit bash可能不带make 和gcc工具，所以make命令也不成功\n这时只需要下载GNUwin32-make工具就行，然后将其bin目录配置到系统环境变量中\n这里不要安装在默认路径，默认路径Program Files (x86)带空格，会让make init报错\n即可在git bash命令行中使用make命令。\nwindows跑java项目执行乱码 📷 [图片 token=GGk9biW0aoJZ5Sx1dGkcSR7LnKh（未能下载，见飞书原文）]\n这是字符集的问题，按照下面路径修改即可：\n📷 [图片 token=Edlrb9TPuozkJAx6JSDcE42unpc（未能下载，见飞书原文）]\n这个项目大概要花多久学完？ 静下心来好好看文档，遇到看不懂的地方话题群提问，或者划词评论。2周学完肯定没问题\n📷 [图片 token=BIevblXVtoy29dxcHy5cQ7Ubn8c（未能下载，见飞书原文）]\n源码在哪里下载？ 点击下面链接：[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\n后面会出python版本的吗？ 会出，langchain正在路上\u0026hellip;\nEino Dev插件导入workflow.json生成的节点是redis？ 是redis没错，文章的图片里是Milvus是我p上去的。为了文章连贯没有误解。实际上插件里只能增加redis\n项目会使用git进行管理管理吗？ 不会的，项目不会进行大的改动。项目本意是教会大家怎么进行大模型应用开发和Agent的套路，并不是一个持续迭代的商业项目。\nPrometheus连接失败 因为项目里prometheus给的是localhost，你本地并没有启动，当然失败。直接mock数据吧，因为即使你本地启动了prometheus，还要想方设法制造Alarm\nGo版本：（原谅我当时开发的时候没有写mock开关，后面有时间补上）\n路径：SuperBizAgent/internal/ai/tools/query_metrics_alerts.go 函数：NewPrometheusAlertsQueryTool 随便打开一个AI软件说：帮我修改NewPrometheusAlertsQueryTool函数，mock一些数据返回出去 Java版本：直接把mock- enabled改成true\n📷 [图片 token=KIvCbZHv1oykYIxqxhYcHECknTh（未能下载，见飞书原文）]\nMCP CLS报错 CLS service is unregistered 如果你想用CLS，那必须去腾讯云官网创建一个日志集。然后再随便假日志塞进去。不过我感觉没有必要，把项目跑起来，知道怎么回事就行了。重点是掌握项目，Agent开发的套路。\n📷 [图片 token=BlbBbNvSqofJKlxVIvOce3tRnNr（未能下载，见飞书原文）]\nCollection not loaded 这是因为之前创建了错误的数据库导致的，手动删除即可\n📷 [图片 token=KA3QbS75FozFyhxEuDmcdSNnnQf（未能下载，见飞书原文）]\n如下：http://localhost:8000/#/\n📷 [图片 token=PDLpbfMBlormR0xCBrfcXEYfntf（未能下载，见飞书原文）]\n如果删除后重启服务，还是报错，进入agent库，手动点击加载\n📷 [图片 token=HF6zbN4VSoGON6xI8e9c2jxYnld（未能下载，见飞书原文）]\ndocker拉镜像报错MediaType 在 Docker Desktop 4.x 版本中，默认启用了 containerd 来拉取镜像，而这个引擎对 OCI 媒体类型的校验非常严格。如果网络有抖动，就会报 MediaType 错误。\n打开 Docker Desktop 设置 (Settings)。\n点击 General 或 Develop 选项卡。\n找到 \u0026ldquo;Use containerd for pulling and storing images\u0026rdquo;。\n取消勾选该选项，点击 \u0026ldquo;Apply \u0026amp; Restart\u0026rdquo;。\n取消勾选并应用+重启docker后，尝试在管理员终端输入\ndocker run hello-world\n如果出现以下样式，大概率就解决了。随后重新make init试试\n📷 [图片 token=LZ2DbiXLUole6Sxuns0coeyDn9c（未能下载，见飞书原文）]\n📷 [图片 token=CL45bhHUwoFa9RxUM29cmDp4naf（未能下载，见飞书原文）]\n经验沉淀自动化闭环代码在哪？ 越用越聪明，自动总结沉淀在哪？\n代码里没有实现\n实现逻辑1：把输出写入文档，然后文档走一遍上传RAG的逻辑，很简单 实现逻辑2：实际操作应该要考虑哪些对话应该被总结写进文库，否则会污染文库。具体实现可以加一个让用户反馈是否解决问题。如果解决问题，再走文库沉淀的流程。 Exception: 401 - ｛\u0026ldquo;code\u0026rdquo;：\u0026ldquo;InvalidApiKey\u0026rdquo;，\u0026ldquo;message\u0026rdquo;：\u0026ldquo;Invalid API-key provided.\u0026quot;｝ 一般都是你在配置文件里面修改了正确的api-key后，但是没有重启服务！！！ search result has error: extra output fields ［content metadatal found and result does not dynamic field 在前端对话框左边上传一个项目里面的文档即可 ","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E6%B1%87%E6%80%BB%EF%BC%88%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0%E4%B8%AD%EF%BC%89/","summary":"windows跑java项目make init失败 \u0026lt;image token=\u0026ldquo;ZJQUbRjWfo91L1xBLcJcQUEdngb\u0026rdquo; width=\u0026ldquo;2774\u0026rdquo; height=\u0026ldquo;480\u0026rdquo; align=\u0026ldquo;center\u0026rdquo;/ 这是因为你的终端不","title":"常见问题汇总（持续更新中）"},{"content":"一次诊断如果只停留在任务历史里，它对下一次相似故障的帮助很有限。OncallAgent 会在有最终报告且任务成功时，把诊断结果转换成当前用户拥有的结构化案例、Markdown 知识文档和普通文档索引任务。索引完成后，案例 chunk 与 SOP、普通知识文档一起进入受 tenant 约束的检索链路，后续聊天或 AIOps Planner 才有机会重新找到它。\n📷 [图片 token=P9dqbjHcwopIQyx24VRcbr9onyg（未能下载，见飞书原文）]\n这不是“把模型回答复制到一个列表”这么简单。自动沉淀需要同时保证成功门槛、幂等性、证据来源、文档元数据、索引状态和 owner 隔离。任何一步失败都不能被悄悄抹平：例如索引失败时，结构化案例仍然可以查询，但不能声称它已经能够被向量检索命中。\n📷 [图片 token=BTzebBTQfotbujxk2bTcvueMnPf（未能下载，见飞书原文）]\n学习目标 理解成功诊断如何自动生成结构化案例、知识文档和索引任务。\n区分案例库记录、知识文档、向量 chunk 和手动“保存到知识库”接口。\n掌握案例幂等、owner scope、索引失败和复用可追溯性边界。\n能够沿引用中的 knowledgeType=diagnostic-case 找回案例来源。\n功能入口与完整调用链 自动入口位于 apps/backend/src/super_ai/aiops/diagnostics.py 的 AiopsDiagnosticService._report。Report 节点先持久化报告、证据链接和终态任务；只有状态为 succeeded 且服务装配了 DiagnosisCasePersistor 时，才调用 persist。诊断执行失败时仍可能保存一份解释失败的报告，但不会自动创建案例。\nDiagnosisCasePersistor.persist 首先按 owner 和 task 查询已有案例，避免同一任务重复生成。没有已有记录时，它读取任务的全部证据，提取结构化字段，生成案例 Markdown，创建 source=aiops-diagnostic 的知识文档，创建标准 document index task，再创建关联 task、report、document、index task 和 evidence IDs 的案例记录，最后安排索引任务。\n📷 [图片 token=XQ8cbfTqro4pK7xhNDmcZLCinJg（未能下载，见飞书原文）]\n索引逻辑继续使用普通文档管线。apps/backend/src/super_ai/documents/indexing.py 从文档 metadata 的 indexableText 取得正文，切分并生成 embedding，写入 Milvus 时补充 owner、tenant、knowledge base、document 和 chunk 标识，并把 knowledgeType 固定为 diagnostic-case。后续 KnowledgeRetrievalTool 召回该 chunk 时，citation 会把这个分类返回给前端。\n📷 [图片 token=DQHRbCPbwoyMQdxX3ouc8mHznsf（未能下载，见飞书原文）]\n成功 Report → DiagnosisCasePersistor.persist(owner task, report) → 查询同任务已有案例 → 读取 owner-scoped evidence → 提取 alert / service / keywords / summary 等字段 → 创建 source=aiops-diagnostic 的 Markdown 知识文档 → 创建标准 document index task → 创建结构化 DiagnosticCaseRecord → 调度普通索引任务 → chunk metadata 标记 knowledgeType=diagnostic-case → 后续 tenant-scoped 混合检索 → citation 展示为“故障案例”并保留文档、chunk 和排名信息 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/aiops/diagnostics.py AiopsDiagnosticService._report 在报告与任务终态持久化后，仅为成功诊断触发自动案例沉淀。 apps/backend/src/super_ai/aiops/cases.py DiagnosisCasePersistor 生成结构化字段、知识正文、文档、索引任务和案例记录，并保证同任务幂等。 apps/backend/src/super_ai/memory/repositories.py DiagnosticCaseRecord、DiagnosticMemoryRepository 定义案例与任务、报告、文档、索引任务和证据之间的存储契约。 apps/backend/src/super_ai/memory/sqlite.py SQLiteDiagnosticMemoryRepository.create_case 校验所有父资源属于同一 owner，并按 owner/task 防止重复案例。 apps/backend/src/super_ai/documents/indexing.py _vector_chunk_record、_knowledge_type 将案例文档切分、向量化，并给 chunk 写入 diagnostic-case 分类和租户字段。 apps/backend/src/super_ai/retrieval/tool.py KnowledgeRetrievalTool、_citation_from_hit 在授权知识库内召回案例，并生成包含来源、分类和各阶段排名的 citation。 apps/backend/src/super_ai/api/app.py list_aiops_diagnostic_cases、get_aiops_diagnostic_case 提供 owner-scoped 案例列表和详情接口；另有独立的手动知识保存接口。 packages/api-contracts/src/protected-data.ts AiopsDiagnosticCase 定义案例库展示所需的结构化字段和关联 ID。 apps/frontend/src/components/AiopsCaseLibrary.vue caseSummary、select、open-document 展示当前用户案例，并允许回到原诊断或打开生成的知识文档。 openspec/specs/automated-diagnosis-case-library/spec.md Automatic case persistence 规定成功门槛、失败不创建、幂等、索引与 owner 隔离。 代码调用流程图 案例沉淀不是把报告直接复制到列表里，而是创建标准 Markdown 知识文档、索引任务和案例记录，再复用普通文档索引链路进入 Milvus。\n关键实现拆解 只有成功报告进入自动案例库 Report 节点把 execution_failed 映射为任务终态。只要 Executor 发生失败，任务就标为 failed；即使 fallback 报告成功写入，也不会触发 DiagnosisCasePersistor。这个条件很重要：案例库面向可复用的已完成诊断，不应把“工具不可用、证据不足”的失败任务自动包装成已验证经验。\n看什么：Report 先根据 execution_failed 计算任务终态，持久化报告和任务后，才用 status == \u0026quot;succeeded\u0026quot; 作为自动案例的唯一入口条件。\nstatus: Literal[\u0026#34;succeeded\u0026#34;, \u0026#34;failed\u0026#34;] = ( \u0026#34;failed\u0026#34; if bool(state.get(\u0026#34;execution_failed\u0026#34;)) else \u0026#34;succeeded\u0026#34; ) # … 省略报告、证据链接与 result_payload 持久化 updated_task = await self._repositories.diagnostics.update_task( owner_user_id=owner_user_id, task_id=task_id, status=status, result_payload=result_payload, completed_at=_now(), ) if updated_task is None: raise RuntimeError(\u0026#34;Diagnostic task disappeared during report persistence.\u0026#34;) # 1. 写出 fallback 报告不等于成功；失败任务不会自动沉淀案例。 if status == \u0026#34;succeeded\u0026#34; and self._case_persistor is not None: case = await self._case_persistor.persist(task=updated_task, report=report) refreshed_task = await self._repositories.diagnostics.update_task( owner_user_id=owner_user_id, task_id=task_id, status=status, # 2. 成功沉淀后把结构化案例 ID 回写任务结果。 result_payload={**result_payload, \u0026#34;diagnosticCaseId\u0026#34;: case.id}, completed_at=_now(), ) 这段代码证明“有报告”和“可进入案例库”是两个条件：工具失败后仍可持久化报告，但任务终态阻断自动案例。边界是当 persistor 未注入时，即使任务成功也不会自动创建案例；文档不能把案例生成描述成 Report 模型调用自身的必然副作用。\n📷 [图片 token=XCPXbUZd8oQ3WOxKyvncS2AmnsE（未能下载，见飞书原文）]\n成功也不等于根因字段一定有内容。_case_fields 从告警输入提取 alert name 和 service，从诊断 query 提取最多十二个关键词，从 report payload 尝试读取 rootCause、remediation 等字段，并从 Markdown 正文生成最多 240 个字符的纯文本摘要。当前 Report payload 主要保存计划、证据和状态，并不保证存在独立的 root cause 或 remediation 字段，所以这两个字段允许为空。教学文档不能把它们描述为始终由模型准确抽取。\n📷 [图片 token=AjxmbjOydoBAsjxqvq7cD1QWnDg（未能下载，见飞书原文）]\n看什么：下面的状态图把“报告生成方式”和“任务终态”拆成两条维度。无论 LLM 还是 fallback，只要 Executor 失败，自动案例门都保持关闭。\n这张图证明 fallback 是报告可用性保障，不是绕过成功门槛的通道。失败任务保留报告用于解释证据缺口，但不会被包装成可复用的自动经验。\n结构化案例和知识文档承担不同职责 DiagnosticCaseRecord 是案例库索引：它保存案例 ID、owner、原任务、报告、生成文档、索引任务、告警名、服务、关键词、根因、处置建议、摘要和 evidence IDs。它便于 AIOps 页面快速列出案例，并通过关联 ID 回到原任务、报告和文档。\n📷 [图片 token=BklnbwusLow9ukxsDWIcDr6qnsb（未能下载，见飞书原文）]\n知识文档则保存实际可索引正文。_case_content 组合任务和报告 ID、告警、服务、关键词、原查询、根因、处置建议、完整证据报告以及每条 evidence 的有限摘要。metadata 同时保存 knowledgeType=diagnostic-case、诊断和报告 ID、证据 ID 列表、结构化字段和 evidence count。这样向量 chunk 即使脱离案例表参与检索，仍能追踪回其来源。\n看什么：文档创建复用普通知识 Repository；正文放入 indexableText，metadata 则携带分类、诊断、报告、证据和结构化字段，供后续标准索引链读取。\ndocument = await self._repositories.documents.create_document( owner_user_id=task.owner_user_id, document_id=f\u0026#34;doc_{uuid4().hex}\u0026#34;, knowledge_base_id=knowledge_base_id, filename=f\u0026#34;diagnostic-case-{task.id[-12:]}.md\u0026#34;, size_bytes=len(content.encode()), mime_type=\u0026#34;text/markdown\u0026#34;, content_hash=f\u0026#34;sha256:{sha256(content.encode()).hexdigest()}\u0026#34;, source=\u0026#34;aiops-diagnostic\u0026#34;, metadata={ # 1. 正文沿用普通文档索引入口。 \u0026#34;indexableText\u0026#34;: content, \u0026#34;knowledgeType\u0026#34;: \u0026#34;diagnostic-case\u0026#34;, \u0026#34;diagnosticTaskId\u0026#34;: task.id, \u0026#34;diagnosticReportId\u0026#34;: report.id, \u0026#34;evidenceIds\u0026#34;: [item.id for item in evidence], # 2. 结构化字段同时留在文档 metadata。 \u0026#34;alertName\u0026#34;: fields.alert_name, \u0026#34;service\u0026#34;: fields.service, \u0026#34;keywords\u0026#34;: fields.keywords, \u0026#34;rootCause\u0026#34;: fields.root_cause, \u0026#34;remediation\u0026#34;: fields.remediation, \u0026#34;summary\u0026#34;: fields.summary, \u0026#34;evidenceCount\u0026#34;: fields.evidence_count, }, ) 这段代码证明自动案例不是直接向 Milvus 写一段向量，而是先成为 owner-scoped 标准知识文档。安全边界是文档 metadata 与正文都会持久化报告和证据来源，因此它们必须继续由知识库权限保护，不能当作公开摘要。\n看什么：文档之后先创建标准索引任务，再创建结构化案例，最后调度该索引任务；案例记录保存的是关联 ID 和用于列表展示的字段，而不是替代知识正文。\nindex_task = await self._repositories.document_index_tasks.create_task( owner_user_id=task.owner_user_id, task_id=f\u0026#34;index_task_{uuid4().hex}\u0026#34;, knowledge_base_id=knowledge_base_id, document_id=document.id, ) # 1. 结构化案例把任务、报告、文档、索引任务和证据串起来。 case = await self._repositories.diagnostics.create_case( owner_user_id=task.owner_user_id, case_id=f\u0026#34;diagnostic_case_{uuid4().hex}\u0026#34;, task_id=task.id, report_id=report.id, document_id=document.id, index_task_id=index_task.id, alert_name=fields.alert_name, service=fields.service, keywords=fields.keywords, root_cause=fields.root_cause, remediation=fields.remediation, summary=fields.summary, evidence_ids=[item.id for item in evidence], ) # 2. 案例记录提交后才调度普通文档索引任务。 scheduled = self._index_task_scheduler.schedule( owner_user_id=task.owner_user_id, task_id=index_task.id, ) 它证明案例“可列出”和文档“已完成向量索引”不是同一状态：创建案例后调度任务，索引仍可能排队或失败。后续读取必须查看关联索引任务的真实状态，不能仅凭 DiagnosticCaseRecord 存在就断言 RAG 已可命中。\n📷 [图片 token=ZWyDbMW5SoR8I3xLsQxc0mHonDe（未能下载，见飞书原文）]\n二者不能相互替代。案例表创建成功但索引任务失败时，页面仍能列出案例及其关联文档；但 Milvus 中没有完成的 chunk，就不能说后续 RAG 已经可以命中。反过来，只保留向量而没有结构化案例，也无法在案例库中稳定关联原任务和证据。\n看什么：局部数据流图强调三个持久对象及其不同状态：结构化案例负责导航，知识文档负责正文，索引任务负责把文档送入普通 chunk/向量链路。\n这张图证明失败索引不会自动删除结构化案例或文档；这正是可观察性边界。只有索引任务完成后，向量侧才有可检索 chunk，案例列表本身不是索引完成证明。\n📷 [图片 token=A3CEbN7nnoUD2yxkwS2cTVewnqh（未能下载，见飞书原文）]\n幂等性建立在 owner 与 task 上 persist 一开始调用 get_case_for_task(owner_user_id, task_id)，已有记录时直接返回。SQLiteDiagnosticMemoryRepository.create_case 在写入事务中再次检查同 owner/task，并验证报告属于该任务、索引任务关联的文档 ID 一致、文档位于相同 owner 范围。双层检查防止重复进入时创建多份案例，也阻止把其他用户的报告或文档拼接成一个案例。\n📷 [图片 token=KlUwbAal1oPk6zxgkf1cICZ8nWf（未能下载，见飞书原文）]\n看什么：第一层检查发生在创建文档之前，适合拦截同一任务的顺序重复处理；返回的是已有案例，而不是重新生成正文或索引任务。\n# 1. 幂等查询同时携带诊断 owner 与 task。 existing = await self._repositories.diagnostics.get_case_for_task( owner_user_id=task.owner_user_id, task_id=task.id, ) if existing is not None: # 2. 顺序重入直接返回已有结构化案例。 return existing evidence = await self._repositories.diagnostics.list_evidence( owner_user_id=task.owner_user_id, task_id=task.id, ) 这段代码证明正常重试不会在已有案例时继续创建文档。它不是分布式锁：两个并发调用都可能在案例尚未写入时读到空，因此还必须理解 Repository 内层检查及跨资源事务边界。\n看什么：第二层检查位于案例 Repository 的事务中；在真正插入前，它重新找同 owner/task 的案例，并验证报告、索引任务和文档的父子关系。\nasync with self._session_factory() as session: # 1. 内层检查防止第二条结构化案例记录提交。 existing = await _find_diagnostic_case_for_task(session, owner_user_id, task_id) if existing is not None: return _diagnostic_case_record(existing) await _require_diagnostic_report(session, owner_user_id, task_id, report_id) index_task = await _require_document_index_task(session, owner_user_id, index_task_id) if index_task.document_id != document_id: raise TenantScopeError( f\u0026#34;Document index task does not match document: {index_task_id}\u0026#34; ) # 2. 文档也必须位于相同 owner 与知识库范围。 await _require_document( session, owner_user_id, index_task.knowledge_base_id, document_id, ) session.add(row) await session.commit() return _diagnostic_case_record(row) 它证明跨 tenant 拼接报告、索引任务或文档会在 Repository 边界被拒绝，结构化案例也不会重复提交。但文档与索引任务在调用此方法之前已分别提交，所以整个 persist 不是单事务；极端并发重入仍可能产生未被最终案例引用的额外文档或索引任务，不能把内层检查描述成全流程原子幂等。\n📷 [图片 token=HP9ZbuafQo55trxprBgcWN28nje（未能下载，见飞书原文）]\n这里的幂等对象是“结构化自动案例”，不是任意正文哈希。不同诊断任务即使内容相近，仍可各自形成案例；同一任务重复触发则只保留一个。它与手动保存接口的 SHA-256 重复检测不是同一种规则。\n📷 [图片 token=NEJDbsPZCo5vOPxdp0ScGxWunzb（未能下载，见飞书原文）]\n看什么：下面的时序图把两层检查与非原子窗口放在同一张图里。教学重点不是承诺绝无多余工件，而是明确哪一层保护哪一种对象。\n这张图证明外层检查优化顺序重入，内层检查保护结构化案例与 owner 关联；两次检查之间仍存在文档和索引任务已提交的窗口。排查重复工件时应分别查看三类表，而不是只数案例行。\n自动沉淀与手动保存是两条路径 POST /aiops/diagnostics/{diagnostic_id}:save-to-knowledge 是单独的手动导出路径。它要求任务为 succeeded，读取最新报告与证据，生成另一份 Markdown，按内容哈希检查重复，然后创建知识文档和索引任务。这个路由本身不创建 DiagnosticCaseRecord；自动案例库由 Report 节点中的 DiagnosisCasePersistor 负责。\n看什么：手动路由先按当前 user 读取成功任务和最新报告，再按生成正文的 SHA-256 在该用户知识库中查重；冲突时在创建文档之前返回业务错误。\ntask = await repositories.diagnostics.get_task(owner_user_id=user.id, task_id=diagnostic_id) if task is None or task.status != \u0026#34;succeeded\u0026#34;: raise ApiErrorException(\u0026#34;AUTH_FORBIDDEN\u0026#34;) reports = await repositories.diagnostics.list_reports( owner_user_id=user.id, task_id=task.id, ) report = reports[-1] if reports else None if report is None: raise ApiErrorException(\u0026#34;BUSINESS_NOT_FOUND\u0026#34;) evidence = await repositories.diagnostics.list_evidence( owner_user_id=user.id, task_id=task.id, ) content = _diagnostic_case_content(task, report, evidence) content_hash = f\u0026#34;sha256:{sha256(content.encode()).hexdigest()}\u0026#34; knowledge_base_id = f\u0026#34;kb_{user.id}\u0026#34; # 1. 手动保存以 owner、知识库和正文哈希查重。 duplicate = await repositories.documents.find_active_by_hash( owner_user_id=user.id, knowledge_base_id=knowledge_base_id, content_hash=content_hash, ) if duplicate is not None: # 2. 冲突不会创建第二份手动知识文档。 raise ApiErrorException(\u0026#34;BUSINESS_CONFLICT\u0026#34;) 这段代码证明手动保存的幂等键是正文哈希，而非诊断 task；内容相同会冲突，内容变化后可能生成新文档。权限边界也更严格：不可访问与非成功任务都使用统一禁止响应，不向其他用户泄露任务状态。\n因此页面中的“案例库”与手动“保存到知识库”不能在文档里合并成一个按钮逻辑。前者对成功诊断自动发生并带结构化案例；后者是用户显式创建知识文档的补充入口，并以内容哈希防重复。区分两条路径有助于排查“案例已经出现，但手动保存又返回冲突”或“文档已生成，但案例表没有新增”等问题。\n📷 [图片 token=WiMtbIhmKo75xJxlIDhcRLWanjd（未能下载，见飞书原文）]\n看什么：对照图从同一个成功诊断分成两条互不替代的路径，特别注意只有自动路径写结构化案例。\n这张图证明两条路径可以同时围绕同一诊断产生不同知识文档语义，且手动路径不会补建结构化案例。查看问题时应以触发入口、幂等键和实际关联 ID 为准，不能用“都进知识库”推断它们是同一次写入。\n数据、契约与状态 AiopsDiagnosticCase 共享契约向前端提供 taskId、reportId、documentId、indexTaskId 和结构化摘要字段。AiopsCaseLibrary.vue 用这些字段展示告警、服务、关键词和摘要；点击案例回到原诊断，点击文档入口跳转知识库页面。页面展示的案例来自 GET /aiops/diagnostic-cases，按当前用户和创建时间倒序查询。\n生成文档进入标准文档状态机。创建时会有 document metadata 和待执行的 index task；索引服务再更新排队、执行、成功或失败状态。案例记录保存 indexTaskId，但当前案例列表契约没有直接展开索引任务最新状态，需要通过关联文档或索引 API 检查。不能仅凭案例存在就推断索引成功。\n📷 [图片 token=FTjtbkhZMoL9Frx8zoRcQErtnPc（未能下载，见飞书原文）]\n索引成功后，_vector_chunk_record 将 knowledgeType、owner、tenant、knowledge base、document 和 chunk ID 写入 metadata。KnowledgeRetrievalCitationSource 再把分类和完整排名阶段输出到 citation。前端 ChatCitationDetail.vue 把 diagnostic-case 显示为“故障案例”，读者可以区分它来自历史案例而不是 SOP 或普通文档。\n📷 [图片 token=DNh1b5RQPommkexCzlhcWxWAnPd（未能下载，见飞书原文）]\n权限、安全与失败边界 自动沉淀沿用诊断任务 owner。文档目标知识库 ID 为 kb_{owner_user_id}，案例、文档、索引任务和 evidence 查询都携带相同 owner。repository 写入案例时还验证 report、index task 和 document 的父子关系；跨 tenant 拼接会抛出 TenantScopeError。列表和详情接口也只按当前用户查询，其他用户得到统一权限错误。\n📷 [图片 token=SZS4bS4DAo7NRJxj9KMcPBXNnfc（未能下载，见飞书原文）]\nMilvus 只保存知识 chunk 向量和用于过滤、追踪的标量/metadata，不保存案例表的完整业务关系。结构化案例、文档元数据和索引状态仍以 SQLite 为主。检索时必须同时传入 tenant ID 与授权知识库集合；授权集合为空则直接返回空结果，不能退化为无范围检索。\n📷 [图片 token=C1JZbiNcUohTEix9A4Gcxc74nFg（未能下载，见飞书原文）]\n案例复用也不是自动“照抄历史根因”。Planner 只是把检索结果作为 SOP/历史证据的一部分，Executor 仍需调用真实工具验证当前事故。历史案例可以提供排查方向，但不能证明当前告警具有相同根因，更不能把过去的处置结果写成当前已经执行成功。\n📷 [图片 token=OWzIb2Ay5oQrHBxIjCFcNLkQnrh（未能下载，见飞书原文）]\n阅读顺序与小结 先从 AiopsDiagnosticService._report 找到成功门槛和自动触发点。\n完整阅读 apps/backend/src/super_ai/aiops/cases.py，理解结构化字段、正文和幂等规则。\n沿 SQLiteDiagnosticMemoryRepository.create_case 检查关联资源和 owner 校验。\n再进入文档索引与检索工具，确认案例怎样变成可追溯 citation。\n最后比较自动案例与手动保存接口，避免把两条路径混为一谈。\n案例沉淀的价值不在于数量，而在于它能否保留“哪次任务、哪份报告、哪些证据、哪篇文档、哪次索引”的完整来源。OncallAgent 通过结构化案例和普通知识索引把诊断结果接回 RAG，但后续诊断仍要重新取证；历史经验提供方向，真实工具决定当前事实。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/13.%20%E8%AF%8A%E6%96%AD%E6%A1%88%E4%BE%8B%E8%87%AA%E5%8A%A8%E6%B2%89%E6%B7%80%E4%B8%8E%E7%9F%A5%E8%AF%86%E5%A4%8D%E7%94%A8/","summary":"一次诊断如果只停留在任务历史里，它对下一次相似故障的帮助很有限。OncallAgent 会在有最终报告且任务成功时，把诊断结果转换成当前用户拥有的结构化案例、Markdown 知识文档和普通文档索引任务。索引完成后，案例 chunk 与 S","title":"13. 诊断案例自动沉淀与知识复用"},{"content":"[项目源码下载](/oncall/智能 OnCall Agent 项目/项目源码下载(完整)/)\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E9%A1%B9%E7%9B%AE%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD/","summary":"\u0026lt;mention-doc token=\u0026ldquo;UfYIwTsNKi6wopkYErTcfb8mnTc\u0026rdquo; type=\u0026ldquo;wiki\u0026rdquo; 项目源码下载\u0026lt;/mention-doc","title":"项目源码下载"},{"content":"AI Native 应用要想持续改进，至少需要回答两类不同问题：系统实际调用了什么工具、调用是否成功；用户认为哪一段结果有帮助、哪里不正确。OncallAgent 分别用工具调用审计和结构化用户反馈处理这两类问题。前者记录可观测的执行事实，后者记录用户对回答、引用、诊断步骤或报告的评价。\n这两套数据不能混为一谈。工具审计不会自动判断回答是否正确，用户点踩也不能证明某次 MCP 调用失败。只有把审计中的执行链、引用中的来源、反馈中的具体目标和纠正意见放在一起，才可能定位“工具没调用”“工具返回无结果”“结果被错误解释”或“引用与结论不匹配”等不同问题。\n📷 [图片 token=HyFJbBVHzoMOXMxIKjDcSkKpntF（未能下载，见飞书原文）]\n学习目标 理解聊天与 AIOps 如何共用一套 owner-scoped 工具审计模型。\n掌握 started、completed、failed 三种持久状态与实时 SSE 状态的关系。\n理解四类反馈目标、重复提交更新规则和目标归属校验。\n识别运行日志脱敏、业务审计内容和前端展示之间的安全边界。\n功能入口与完整调用链 聊天工具调用从 LangChain 事件进入 apps/backend/src/super_ai/chat/streaming.py。on_tool_start、on_tool_end 和 on_tool_error 被转换成具有稳定 run ID 的 ChatAgentToolCall，ChatStreamingService 在发送 tool.call SSE 之前调用 _persist_tool_call_audit。started 事件创建记录，completed 或 failed 事件完成同一记录并计算耗时。\n📷 [图片 token=DPS4b0m25onHbBxfBxTceDghnac（未能下载，见飞书原文）]\nAIOps 侧由 AiopsDiagnosticService 显式控制生命周期。Planner 的知识检索和 Executor 的每个工具调用都先调用 _create_audit，随后根据真实结果调用 _finalize_audit。所有诊断审计绑定 diagnostic_task_id；聊天审计绑定 chat_session_id。数据库约束要求二者只能有一个父资源。\n📷 [图片 token=NHUxbowrfoRXqixaHiqcTYM2nRg（未能下载，见飞书原文）]\n反馈入口分布在聊天回答、单条 citation、诊断 step 和最终 report 旁边。UserFeedbackControl.vue 提供点赞、点踩、问题类型、说明和建议纠正；useUserFeedbackStore 通过 GET、POST 和 DELETE 方法访问 /feedback。后端 UserFeedbackService 先验证目标真实存在且属于当前用户，再执行 owner-scoped upsert 或 delete。\n📷 [图片 token=DeY8byimso3DVexOvzMc1NuAnph（未能下载，见飞书原文）]\n工具执行事实 → LangChain tool event 或 AIOps Executor → tool.call SSE 供当前页面实时展示 → SQLiteToolCallAuditRepository → started / completed / failed + duration → 会话审计接口或 AIOps evidence-chain 回读 用户质量判断 → 回答 / citation / diagnostic step / report 旁的反馈控件 → UpsertFeedbackRequest → UserFeedbackService._require_owned_target → SQLiteUserFeedbackRepository.upsert → 重新打开目标时恢复评价 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/chat/streaming.py _persist_tool_call_audit 将聊天工具 started/completed/failed 事件映射为持久审计生命周期。 apps/backend/src/super_ai/aiops/diagnostics.py _create_audit、_finalize_audit 为知识检索与诊断工具调用创建并完成同一审计。 apps/backend/src/super_ai/memory/sqlite.py SQLiteToolCallAuditRepository 验证父资源归属，保存状态、参数、摘要、错误和派生耗时。 apps/backend/src/super_ai/memory/models.py AgentToolCallAuditModel 定义审计表和“聊天会话或诊断任务二选一”的父资源约束。 packages/api-contracts/src/chat.ts ToolCallAudit 定义审计在前后端之间的共享字段和状态枚举。 apps/frontend/src/stores/chat.ts updateLiveToolCall、loadSession 流式期间合并实时工具状态，流结束后回读会话与服务器持久审计。 apps/backend/src/super_ai/feedback.py UserFeedbackService 校验反馈类型、评分、长度和目标归属，组织 upsert/list/delete。 apps/backend/src/super_ai/memory/extended_sqlite.py SQLiteUserFeedbackRepository 按 owner、target、subject 唯一键更新或创建反馈。 packages/api-contracts/src/feedback.ts FeedbackTargetType、UpsertFeedbackRequest 定义四类目标、正负评价和可选纠正字段。 apps/frontend/src/components/UserFeedbackControl.vue rate、remove、current、pending 提供紧凑评价、渐进表单、提交状态和删除操作。 代码调用流程图 审计回答“工具实际做了什么”，反馈回答“用户如何评价结果”。两条链路共享 owner scope，但数据目的和写入时机不同。\n关键实现拆解 同一个工具调用 ID 贯穿实时事件与持久记录 LangChain adapter 使用事件的 run_id 作为工具调用 ID；AIOps 则在调用前生成 tool_... ID。started 事件携带工具名与输入，创建审计记录；completed 事件保存有限结果摘要；failed 事件保存安全错误摘要。SQLiteToolCallAuditRepository.finalize 根据完成时间减开始时间计算非负 duration_ms。\n📷 [图片 token=OO3Cb4EGnoCXWrxPhv7c35yKnTz（未能下载，见飞书原文）]\n看什么：聊天流收到工具生命周期事件后，使用事件自身的 ID 创建或完成审计；started 保存结构化参数，completed 只保存有限结果摘要。\ntry: if event.status == \u0026#34;started\u0026#34;: # 1. started 用事件 ID 创建 owner 与会话关联的审计。 await repository.create_for_chat_session( owner_user_id=owner_user_id, audit_id=event.id, chat_session_id=session_id, tool_name=event.name, arguments=_json_dict_or_empty(event.input), ) return if event.status == \u0026#34;completed\u0026#34;: # 2. completed 用同一个 ID finalize，而不是新建第二条记录。 audit = await repository.finalize( owner_user_id=owner_user_id, audit_id=event.id, status=\u0026#34;completed\u0026#34;, result_summary=_audit_summary(event.output), ) if audit is None: await self._create_and_finalize_missing_audit( owner_user_id=owner_user_id, session_id=session_id, event=event, result_summary=_audit_summary(event.output), ) 这段代码证明实时事件 ID 是 SSE 与 SQLite 记录之间的稳定关联键。异常边界是整个方法外层采用 best-effort：Repository 写入抛错会被捕获并返回，聊天内容流仍可继续，因此一次成功回答不保证审计一定存在。\n如果聊天流先收到 completed/failed 而没有对应 started 记录，_create_and_finalize_missing_audit 会补建后再完成，避免历史中完全丢失一次调用。反过来，只有 started 而没有终止事件时，记录会保留 started、空 completed time 和空 duration，不会虚构完成结果。这对进程中断或上游事件缺失尤其重要。\n看什么：补偿函数只处理“收到终止事件但找不到 started 记录”的情况；它先以同一 ID 建立 started，再立刻根据终止事件完成。\nrepository = self._repositories.tool_call_audits if repository is None: return # 1. 先补建同 owner、会话和事件 ID 的 started 记录。 await repository.create_for_chat_session( owner_user_id=owner_user_id, audit_id=event.id, chat_session_id=session_id, tool_name=event.name, arguments=_json_dict_or_empty(event.input), ) # 2. 再按终止事件内容完成，不反推不存在的输出。 await repository.finalize( owner_user_id=owner_user_id, audit_id=event.id, status=\u0026#34;failed\u0026#34; if error_message is not None else \u0026#34;completed\u0026#34;, result_summary=result_summary, error_message=error_message, ) 它证明缺 started 的终止事件不会让整次调用从历史中消失，但补偿开始时间只能是补建时刻，因此持续时间不代表真实工具端执行全程。只有 started 没有终止事件时不会触发反向补偿，记录会如实停留在 started。\n📷 [图片 token=VjRwbYpaKo1oFnxM7e8c1txRnFc（未能下载，见飞书原文）]\n聊天页面在流式期间由 updateLiveToolCall 合并 tool.call 事件，因此用户不必等回答结束才看到工具开始。收到完整流后，store 调用 loadSession；该函数并行读取会话详情和 /chat/sessions/{session_id}/tool-call-audits，用服务器记录替换临时状态。AIOps 页面则从 evidence-chain 中读取同一 ToolCallAudit 结构。\n📷 [图片 token=KaT5bsLfzoGYAGxKwBfckQGPn7e（未能下载，见飞书原文）]\n看什么：前端回载会话时并行读取消息和持久审计，随后清空流式临时调用；刷新后的事实来源是服务器记录。\nasync function loadSession(sessionId: string): Promise\u0026lt;void\u0026gt; { const [detail, audits] = await Promise.all([ client.getSession(sessionId), client.listToolCallAudits(sessionId) ]); // 1. 完整回载后以持久审计替换临时调用。 activeSessionId.value = detail.session.id; messages.value = detail.messages; toolAudits.value = audits.items; liveToolCalls.value = []; setReferencesFromMessages(detail.messages); upsertSession(detail.session); } // … 省略其他 store 方法 // 2. 流式阶段按同一 ID 合并 started/completed/failed。 const existing = target.value.find((item) =\u0026gt; item.id === next.id); const merged: LiveToolCall = { ...existing, ...next, ...(next.input === undefined ? {} : { input: next.input }), ...(next.output === undefined ? {} : { output: next.output }) }; 这段代码证明页面有“流中临时状态”和“完成后持久状态”两层，而不是只依赖 SSE 缓存。若审计 best-effort 写入失败，回载后临时工具调用可能消失；这不是前端伪造完成状态，而是服务器持久事实缺失的可见结果。\n看什么：状态图展示完整、缺 started、缺终止事件三种实际路径，重点是系统只补偿已有事实，不推测未收到的终态。\n这张图证明 started 是可持久观察的合法状态，而不是必须由清理任务强行改成 failed。审计历史忠实保留事件缺口；业务方应结合聊天完成事件或诊断任务终态解释它。\n审计记录、结构化日志和 SSE 各有边界 SSE 面向当前交互，允许前端展示输入或输出；SQLite 审计用于历史追踪，保存 JSON 参数、最多约束长度的结果摘要或错误；结构化运行日志用于运维，只记录工具名、参数键、状态、耗时和错误类别。apps/backend/src/super_ai/mcp_client.py 不把参数值或工具输出写进运行日志，apps/backend/tests/test_mcp_observability.py 对此有明确断言。\n📷 [图片 token=L1jCbHSHtoXSvmxos7EcFMoCnSh（未能下载，见飞书原文）]\n看什么：SQLite finalize 只在 owner 与 audit ID 同时匹配时更新，完成时间与开始时间计算为非负毫秒；找不到记录返回 None，让上层决定是否补偿。\ntimestamp = completed_at or utc_now() async with self._session_factory() as session: row = ( await session.scalars( select(AgentToolCallAuditModel).where( AgentToolCallAuditModel.id == audit_id, # 1. finalize 不能跨 owner 命中同 ID 记录。 AgentToolCallAuditModel.owner_user_id == owner_user_id, ) ) ).one_or_none() if row is None: return None row.status = status row.result_summary = result_summary row.error_message = error_message row.completed_at = timestamp # 2. 时钟异常时也不写负 duration。 row.duration_ms = max( 0, round((timestamp - _ensure_utc(row.started_at)).total_seconds() * 1000), ) await session.commit() 它证明 owner scope 同时约束创建、完成与读取，不是 API 返回前才过滤。边界是 Repository 保存调用参数和摘要本身，并不等同于运行日志的最小字段策略；数据库访问必须继续受认证与父资源权限保护。\n📷 [图片 token=W2FfbGOeOoxdGtxxc1IcieqGnJb（未能下载，见飞书原文）]\n需要注意的是，业务审计不是一个通用秘密扫描器。聊天的 _audit_summary 将 JSON 序列化结果截断到 2000 字符，错误摘要只清理特定 API key 形式；started 审计会保存结构化参数。它们只通过 owner-scoped API 暴露，但调用工具时仍不应把凭据放进普通参数或结果正文。运行日志脱敏不能被误解为所有数据库审计字段都已自动删除敏感信息。\n看什么：业务审计摘要和错误摘要的安全策略非常具体——先 JSON 化并截断，再只对错误文本替换当前正则识别的 key 形态。\ndef _audit_summary(value: object | None) -\u0026gt; str: encoded = json.dumps(_jsonable(value), ensure_ascii=True, separators=(\u0026#34;,\u0026#34;, \u0026#34;:\u0026#34;), default=str) # 1. 结果摘要是长度边界，不是字段级秘密清洗。 return encoded[:2000] def _audit_error_summary(value: object | None) -\u0026gt; str: # 2. 当前错误清洗只覆盖特定 sk 与 AKID 形式。 return re.sub(r\u0026#34;(?:sk-[A-Za-z0-9_-]+|AKID[A-Za-z0-9]+)\u0026#34;, \u0026#34;[redacted]\u0026#34;, _audit_summary(value)) 这段代码证明数据库审计不是“绝不含输入输出”，而是受 owner 保护的有界业务记录。未命中正则的凭据仍可能进入审计，因此正确边界是调用端不传秘密、API 做权限校验、运行日志保持最小字段，三者缺一不可。\n📷 [图片 token=IyfWb3Wdoof5lGxeGHucJqBEnie（未能下载，见飞书原文）]\n聊天侧的审计持久化使用 best-effort 策略：_persist_tool_call_audit 捕获异常，避免审计存储故障中断回答流。这意味着一次聊天回答成功但审计缺失在异常场景下仍可能发生。AIOps 的审计写入属于诊断节点执行链，存储异常可能使节点失败。阅读系统状态时，应以实际审计记录和任务状态为准，不要根据设计意图补齐不存在的数据。\n📷 [图片 token=OWyXbQ8zzoaC9Wxo0mLcf7nInId（未能下载，见飞书原文）]\n看什么：三数据面图把同一工具事实分流到即时 SSE、owner-scoped SQLite 和最小化结构化日志；每条边的保留内容与失败影响不同。\n这张图证明“都在记录工具调用”不代表三者数据相同或一致性相同。排障时应先确定观察的是即时 UI、持久历史还是运维日志，再解释缺失；任何一面都不能凭空补成另一面的事实。\n反馈首先验证“你能否评价这个对象” UserFeedbackService 支持 chat_message、citation、diagnostic_step 和 diagnostic_report。聊天目标必须是当前用户拥有的 assistant message；citation 还要求 subject_id 出现在该消息 metadata 的 citations 中；诊断 step 和 report 通过 owner-scoped repository 查询。目标不可访问时统一返回 AUTH_FORBIDDEN，不会泄露其他用户对象是否存在。\n📷 [图片 token=ZWs4bEH4Rof9oGxd7VBcYuE2nif（未能下载，见飞书原文）]\n看什么：写入前先校验目标类型、评分和所有自由文本长度，再调用目标归属检查；Repository upsert 只有在这些条件全部通过后才发生。\n# 1. 目标类型和评分使用后端白名单。 if target_type not in SUPPORTED_FEEDBACK_TARGETS: raise FeedbackError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;Unsupported feedback target.\u0026#34;) if rating not in SUPPORTED_FEEDBACK_RATINGS: raise FeedbackError(\u0026#34;VALIDATION_INVALID_ARGUMENT\u0026#34;, \u0026#34;Unsupported feedback rating.\u0026#34;) normalized_subject = _optional_text(subject_id, 160) normalized_reason = _optional_text(reason, 80) normalized_comment = _optional_text(comment, 2000) normalized_correction = _optional_text(correction, 4000) # 2. targetId 可由调用者提交，但归属必须再次查库确认。 await self._require_owned_target( owner_user_id=owner_user_id, target_type=target_type, target_id=target_id, subject_id=normalized_subject, ) repository = self._repositories.feedback if repository is None: raise FeedbackError(\u0026#34;SYSTEM_UNAVAILABLE\u0026#34;, \u0026#34;Feedback storage is unavailable.\u0026#34;) 这段代码证明前端的输入控件不是权限边界，伪造 target ID 仍会在服务层被查库拒绝。reason 当前只受长度约束，并没有后端枚举白名单；前端给出的若干原因选项不能被文档描述成强制协议。\n评分只允许 positive 或 negative。subject 最长 160 字符，reason 最长 80，comment 最长 2000，correction 最长 4000；空白会规范化为 null。前端提供 incorrect、incomplete、citation、unsafe 和 other 等问题类型选项，但后端当前只做长度校验，并未把 reason 限定为固定枚举。\n📷 [图片 token=MkH8bPVdjoNj8yxmCDacbzv0nbc（未能下载，见飞书原文）]\n看什么：归属检查按目标类型分支；citation 不仅要求 assistant message 属于 owner，还要求 subject ID 真正在该消息的 citations metadata 中。\nif target_type in {\u0026#34;chat_message\u0026#34;, \u0026#34;citation\u0026#34;}: message = await self._repositories.chat.get_message( owner_user_id=owner_user_id, message_id=target_id, ) # 1. 只允许评价当前 owner 的 assistant message。 if message is None or message.role != \u0026#34;assistant\u0026#34;: raise FeedbackError(\u0026#34;AUTH_FORBIDDEN\u0026#34;, \u0026#34;Feedback target is not accessible.\u0026#34;) if target_type == \u0026#34;citation\u0026#34; and not allow_citation_collection: # 2. citation subject 必须出现在该消息持久 metadata 中。 if subject_id is None or not _message_has_citation(message.metadata, subject_id): raise FeedbackError(\u0026#34;AUTH_FORBIDDEN\u0026#34;, \u0026#34;Feedback target is not accessible.\u0026#34;) return if target_type == \u0026#34;diagnostic_step\u0026#34;: target = await self._repositories.diagnostics.get_step( owner_user_id=owner_user_id, step_id=target_id, ) else: target = await self._repositories.diagnostics.get_report( owner_user_id=owner_user_id, report_id=target_id, ) if target is None: raise FeedbackError(\u0026#34;AUTH_FORBIDDEN\u0026#34;, \u0026#34;Feedback target is not accessible.\u0026#34;) 它证明 citation 评价不能借用同一回答中不存在的 subject，诊断反馈也不能跨 owner 引用 step 或 report。统一 AUTH_FORBIDDEN 有意模糊“对象不存在”和“对象属于别人”，减少枚举泄露。\n看什么：权限流程图展示输入合法性、owner 归属和 citation 成员关系三个连续门槛；任一失败都不会进入反馈 Repository。\n这张图证明反馈权限依赖被评价对象的真实父资源，而不是反馈记录自身携带的 owner 字段。即使请求 schema 已限制类型，服务层仍重复校验，保障非 HTTP 调用路径也遵守同一边界。\n重复提交是更新，不是重复计数 UserFeedbackModel 对 owner_user_id + target_type + target_id + subject_key 建立唯一约束。SQLiteUserFeedbackRepository.upsert 先按这个组合查询；没有记录就创建，有记录就更新 rating、reason、comment、correction 和 updated time。一个用户反复修改同一回答或同一 citation 的评价，只保留当前版本。\n📷 [图片 token=T2xWbDwVioameQxtoIpcZCM6nac（未能下载，见飞书原文）]\n看什么：Repository 把空 subject 规范为 \u0026quot;\u0026quot;，用 owner、目标类型、目标 ID 和 subject key 查找现有行；更新分支保留原反馈 ID 和 created time。\nsubject_key = subject_id or \u0026#34;\u0026#34; now = utc_now() stmt = select(UserFeedbackModel).where( UserFeedbackModel.owner_user_id == owner_user_id, UserFeedbackModel.target_type == target_type, UserFeedbackModel.target_id == target_id, # 1. 总评价与每条 citation 由 subject_key 区分。 UserFeedbackModel.subject_key == subject_key, ) async with self._session_factory() as session, session.begin(): row = (await session.scalars(stmt)).one_or_none() if row is None: row = UserFeedbackModel( id=feedback_id, owner_user_id=owner_user_id, target_type=target_type, target_id=target_id, subject_key=subject_key, rating=rating, # … 省略其余可选字段与时间字段赋值 ) session.add(row) else: # 2. 重复组合只更新当前版本，不累加一行。 row.rating = rating row.reason = reason row.comment = comment row.correction = correction row.updated_at = now 这段代码证明重复反馈是可修改状态而非事件计数器；统计“用户改了几次”无法从当前表直接得出。数据库唯一约束为并发写提供最后防线，但当前 upsert 是先查后写，极端并发首次提交仍可能由唯一约束抛冲突而不是自动重试。\nsubject_key 让同一 assistant message 上的总评价与多个 citation 评价互不覆盖。回答反馈使用空 subject，引用反馈使用 citation ID。列表接口按 target 返回当前用户记录，前端 store 再以 target 与 subject 组合键合并；删除时同样带 owner 条件。\n📷 [图片 token=MEQ6bODSno6exOxXmhrc8wCSnZb（未能下载，见飞书原文）]\n看什么：最后用状态图理解“同一组合键更新、不同 subject 并存”的持久行为，而不是把每次 POST 都画成新反馈。\n这张图证明总评价和多条 citation 评价可以并存，而同一 citation 的反复提交会覆盖旧值。删除也是 owner-scoped 当前状态变更，不会留下可由该表直接查询的历史版本。\n数据、契约与状态 ToolCallAudit 包含 ID、owner、二选一父资源、工具名、状态、arguments、result summary、error message、开始与完成时间、duration 和创建时间。SSE 的 tool.call 还允许 delta，但持久契约只保存 started、completed、failed；前端遇到 delta 时不会创建新的持久态模拟记录。\nUserFeedback 保存目标类型、目标 ID、可选 subject、rating、reason、comment、correction 和时间戳。API 提供列表、upsert 与删除，没有把反馈自动转化为模型训练、Prompt 修改或处置动作。反馈是可查询的质量信号，不是自动学习已经发生的证明。\n两者的关联目前是间接的。例如 assistant message metadata 保存 citation 与 tool call IDs，工具审计绑定 chat session，反馈绑定 message 或 citation；AIOps evidence-chain 同时返回 steps、tool calls、evidence 和 reports，反馈则绑定 step/report。当前没有一张“反馈直接指向某条工具审计”的关系表，分析时需要通过父资源和页面上下文关联。\n📷 [图片 token=DFYdbFG6voQrGBxIyqJcDh02nNd（未能下载，见飞书原文）]\n权限、安全与失败边界 审计 repository 在创建时验证 chat session 或 diagnostic task 属于 owner，查询时再次验证父资源，finalize 还要求审计 ID 和 owner 同时匹配。数据库 CheckConstraint 保证一条审计不能同时属于聊天和诊断，也不能两者都为空。跨 tenant 写入会被拒绝。\n反馈 service 不相信前端传入的 target type 和 ID，而是重新查询真实目标。citation 不能只凭任意 subject ID 提交；它必须出现在当前用户 assistant message 的 metadata 中。删除其他用户反馈返回 false，API 再映射为统一权限错误。\n工具审计只证明调用生命周期和保存的摘要，不证明结果语义正确；用户正向反馈也不证明来源可靠。若工具没有终止事件，应保留 started；若目标不存在，反馈不能保存；若审计或反馈存储不可用，系统必须显示真实失败或保留缺口，不能生成一条看似完整的记录。\n📷 [图片 token=T9aLbTpTQoLEnmxPcLPcnd54nfd（未能下载，见飞书原文）]\n阅读顺序与小结 先读 packages/api-contracts/src/chat.ts 和 feedback.ts，建立两类数据的区别。\n沿聊天或 AIOps 工具事件阅读 audit 的创建、完成和回读。\n进入 SQLite model/repository，核对父资源约束、owner 条件和时间计算。\n再沿 UserFeedbackControl.vue、store、API 和 service 看目标校验与 upsert。\n最后沿 owner 条件、目标校验和日志脱敏规则，检查跨 tenant、缺失事件与重复提交边界。\n可靠的 AI 工程反馈环不是一组点赞按钮，而是把“执行了什么”和“用户如何评价”分别记录，并保留它们各自的证据强度。OncallAgent 已经具备这两条基础链路，但它不会自动把反馈训练进模型，也不会把审计等同于正确性；后续改进仍需要开发者基于真实记录做判断。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/14.%20%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E5%AE%A1%E8%AE%A1%E4%B8%8E%E7%BB%93%E6%9E%84%E5%8C%96%E7%94%A8%E6%88%B7%E5%8F%8D%E9%A6%88/","summary":"AI Native 应用要想持续改进，至少需要回答两类不同问题：系统实际调用了什么工具、调用是否成功；用户认为哪一段结果有帮助、哪里不正确。OncallAgent 分别用工具调用审计和结构化用户反馈处理这两类问题。前者记录可观测的执行事实，","title":"14. 工具调用审计与结构化用户反馈"},{"content":"Oncall Agent源码下载 python- harness最新版本 📎 agent_py-release-2026-07-25.zip（源码附件：飞书原文中可下载）\n[两套资料怎么学：定位区别、学习路线与面试准备](/oncall/AI Native 工程化实战（智能OnCall Agent）/两套资料怎么学：定位区别、学习路线与面试准备/)\nGo语言项目源码 📎 SuperBizAgent-release-2026-05-19.zip（源码附件：飞书原文中可下载）\nJava语言项目源码 📎 SuperBizAgent-release-2026-05-17.zip（源码附件：飞书原文中可下载）\nPython语言项目源码 📎 super_biz_agent_py-release-2026-05-17.zip（源码附件：飞书原文中可下载）\njava小试牛刀 使用spring boot实现一个http接口\n📎 springboot_demo_http.zip（源码附件：飞书原文中可下载）\n使用spring ai alibaba实现一个ai对话接口\n📎 springboot_demo_chat.zip（源码附件：飞书原文中可下载）\nPython小试牛刀 使用fastapi实现一个http接口\n📎 py_demo_http.zip（源码附件：飞书原文中可下载）\n使用langchain实现一个ai对话接口\n📎 py_demo_chat.zip（源码附件：飞书原文中可下载）\n","permalink":"https://rsc-blog.pages.dev/oncall/%E6%99%BA%E8%83%BD%20OnCall%20Agent%20%E9%A1%B9%E7%9B%AE/%E9%A1%B9%E7%9B%AE%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD%28%E5%AE%8C%E6%95%B4%29/","summary":"Oncall Agent源码下载  python- harness最新版本 \u0026lt;!\u0026ndash; 不支持的块类型: View (type=33) \u0026ndash; \u0026lt;file token=\u0026ldquo;EwbwbCL3aoD97fx4aQMcEcV9nbe\u0026rdquo; name=\u0026ldquo;ag","title":"项目源码下载(完整)"},{"content":"本地优先并不意味着“只要进程能启动就算正常”。OncallAgent 同时依赖 SQLite、Milvus、模型服务、MCP Server 和浏览器前端，其中任何一项都可能单独失败。仓库因此把存活检查、依赖就绪检查、配置诊断、请求指标和结构化日志拆成不同入口，再用宿主机启动器与基础设施 Compose 明确运行边界。\n这篇文章的重点不是记住四个 URL，而是理解每个信号能够证明什么。/health 只证明 FastAPI 还能响应；/ready 才会探测运行依赖；/config/check 同时检查配置能否解析和依赖是否可用；/metrics 只提供进程内请求聚合。把这些信号混用，很容易把“端口打开”误判成“诊断链路可用”。\n📷 [图片 token=KTuQbdyWLorQeKx05dzcGCDUnNg（未能下载，见飞书原文）]\n学习目标 区分 liveness、readiness、configuration diagnostics 和本地 metrics。\n理解 request ID、结构化事件和敏感字段脱敏的实现方式。\n掌握 Compose 基础设施与宿主机应用进程的本地运行拓扑。\n能够根据真实检查结果定位 SQLite、Milvus、模型或 MCP 的具体故障层。\n功能入口与完整调用链 apps/backend/src/super_ai/api/app.py 的 create_app 在应用创建时启用结构化日志，组装 SQLite repositories、Milvus、后台任务和请求指标，并保存可注入的模型 provider；默认模型 provider 会在首次请求或 readiness 路径中懒加载。lifespan 启动 durable job runtime，在应用关闭时停止 worker 并释放自建数据库 engine。外部网络连接没有在模块导入时发生，而是在请求、readiness 或显式运行路径中创建。\n📷 [图片 token=Rnxtb0PaXoNQA7xQW4Ec20E2nHK（未能下载，见飞书原文）]\n每个 HTTP 请求先经过 observe_request middleware。它尊重调用方传入的 X-Request-ID，没有时生成新 ID，将其放入 context variable，并在响应头中返回。请求完成后记录 method、path、status 和 latency，同时更新 RequestMetrics；异常处理器另行记录规范化错误码或异常类别。\n📷 [图片 token=CIW2bkM2toKwWtxeAI7cPdkmnwd（未能下载，见飞书原文）]\n本地启动时，infra/compose.yaml 只运行 etcd、MinIO、Milvus、Attu 和 Alertmanager。scripts/start-local.sh 或 scripts/start-local.bat 在宿主机准备依赖、执行 Alembic migration，并启动官方 CLS MCP Server、FastAPI 与 Vite 前端。应用配置仍来自本地 JSON；启动器只为外部 CLS MCP 进程把合并后的必要字段导出为该进程需要的环境变量。\n📷 [图片 token=SJelbCjQYoGgmKxVvLqcivxFnEw（未能下载，见飞书原文）]\n浏览器 / 运维检查 → /health：仅 FastAPI 存活 → /ready：并行检查 SQLite、Milvus、LLM、MCP → /config/check：解析配置 + 复用依赖检查 → /metrics：进程内请求数、5xx 数、平均延迟 本地运行拓扑 → Docker Compose：etcd + MinIO + Milvus + Attu + Alertmanager → 宿主机进程：CLS MCP Server + FastAPI + Vue/Vite → SQLite：宿主机本地业务持久化 → config/project.json + config/user.project.json：递归合并，用户配置覆盖基础配置 核心源码地图 源码位置 关键符号 职责 apps/backend/src/super_ai/api/app.py health、ready、config_check、metrics 提供分层运行状态入口，并统一返回安全响应。 apps/backend/src/super_ai/api/app.py observe_request、_runtime_dependency_payload 关联请求、汇总指标，并并行检查 SQLite、Milvus、LLM 与 MCP。 apps/backend/src/super_ai/observability.py emit_event、_redact 输出紧凑 JSON 事件，并按字段名递归清理敏感值。 apps/backend/src/super_ai/api/observability.py RequestMetrics 线程安全地聚合请求数、5xx 数和总延迟。 apps/backend/src/super_ai/project_config.py load_project_config、_deep_merge 读取基础 JSON 和用户覆盖文件，按嵌套对象递归合并。 apps/frontend/src/runtimeHealth.ts createRuntimeHealthClient 调用轻量 /health，供工作台标题栏显示连接状态。 apps/frontend/src/layouts/WorkspaceLayout.vue isConnected 把轻量 health 成功或失败渲染为可感知状态，不阻塞受保护页面加载。 infra/compose.yaml 五个基础设施 services 定义本地容器基础设施、端口、依赖、healthcheck 和数据卷。 scripts/start-local.sh require_command、port_is_open 检查前置命令、合并配置、启动基础设施、迁移和宿主机进程。 openspec/specs/runtime-readiness-checks/spec.md Readiness requirements 规定安全依赖检查、轻量 health 和配置诊断边界。 代码调用流程图 本地运行不是一个单进程黑盒：Compose 管理基础设施，宿主机运行应用；同一 FastAPI middleware 再把请求关联、结构化事件和进程内指标串起来。\n关键实现拆解 四个状态入口回答四个问题 GET /health 返回 service、status 和 version，不访问 SQLite、Milvus、模型或 MCP。它适合回答“后端进程是否还能处理 HTTP”，也适合前端标题栏的轻量连通性显示。apps/backend/tests/test_readiness_api.py 明确使用一个一旦调用就抛错的 vector store，验证 health 不会触发依赖探测。\n看什么：/health 只读取进程内 foundation 信息；/metrics 只读取当前进程内累计快照并计算平均耗时，两者都不调用四项依赖探针。\n@app.get(\u0026#34;/health\u0026#34;) async def health(request: Request) -\u0026gt; object: # 1. liveness 不触碰 SQLite、Milvus、LLM 或 MCP。 foundation = get_foundation_info() return success_response( request, { \u0026#34;service\u0026#34;: foundation.service, \u0026#34;status\u0026#34;: foundation.status, \u0026#34;version\u0026#34;: foundation.version, }, ) @app.get(\u0026#34;/metrics\u0026#34;) async def metrics(request: Request) -\u0026gt; object: snapshot = request.app.state.request_metrics.snapshot() # 2. 平均值来自当前进程内累计总耗时。 average = ( snapshot.total_latency_ms / snapshot.request_count if snapshot.request_count else 0.0 ) 这段代码证明 health 成功只表示 HTTP 进程可响应，不能推出数据库或外部服务可用；metrics 也不是探针。进程重启会清空计数，且这些数据没有跨实例聚合或长期保留。\nGET /ready 使用 asyncio.gather 并行检查四个组件。SQLite 执行 SELECT 1；Milvus 调用 vector store 的 health_check；LLM 调用 provider 的 check_readiness；MCP 调用 LocalMcpClient.readiness 并统计真实发现的工具。所有组件都成功时返回 200 和 ready，任一失败返回 503 和 degraded，同时保留其他组件的结果。\n📷 [图片 token=AFLkbaUiMoDnUnxMdoUcfAWpnlg（未能下载，见飞书原文）]\n看什么：四个探针用 asyncio.gather 并行执行，再以固定键返回；各探针内部把异常转换为安全的 ok=False 结果，所以单项失败不会抹掉其他项。\nasync def _runtime_dependency_payload(request: Request) -\u0026gt; dict[str, dict[str, object]]: # 1. 四项真实检查并行等待，避免串行叠加延迟。 sqlite_result, milvus_result, llm_result, mcp_result = await asyncio.gather( _sqlite_readiness_payload(request), _milvus_readiness_payload(request), _llm_readiness_payload(request), _mcp_readiness_payload(request), ) # 2. 某一项降级时仍保留另外三项结果。 return { \u0026#34;sqlite\u0026#34;: sqlite_result, \u0026#34;milvus\u0026#34;: milvus_result, \u0026#34;llm\u0026#34;: llm_result, \u0026#34;mcp\u0026#34;: mcp_result, } 它证明 readiness 是聚合结果而不是“第一个失败就提前返回”。一致性边界是四个探针不是同一时刻的分布式快照：并行能缩小时间差，但组件状态仍可能在响应生成后立即变化。\n看什么：路由层只做最终归约——所有组件 ok 才返回 200；否则仍使用统一成功 envelope 承载 degraded 数据，但 HTTP 状态为 503。\n@app.get(\u0026#34;/ready\u0026#34;) async def ready(request: Request) -\u0026gt; object: dependencies = await _runtime_dependency_payload(request) # 1. 任一组件 ok 为假，整体 readiness 即降级。 is_ready = all(bool(component[\u0026#34;ok\u0026#34;]) for component in dependencies.values()) # 2. 响应保留全部组件安全结果，便于定位单点故障。 return success_response( request, {\u0026#34;status\u0026#34;: \u0026#34;ready\u0026#34; if is_ready else \u0026#34;degraded\u0026#34;, \u0026#34;dependencies\u0026#34;: dependencies}, status_code=200 if is_ready else 503, ) 这段代码证明客户端必须同时读取 HTTP 状态和响应数据：503 并不意味着响应体不可解析，而是明确的 readiness 失败。它也没有自动重启或修复依赖，只负责报告当前探测事实。\n📷 [图片 token=O6ZFbnFBjooGEnxKteEcDaPAn5g（未能下载，见飞书原文）]\nGET /config/check 先分别解析 SQLite、LLM、Milvus 和 MCP 配置，再复用运行依赖检查。配置解析和连接状态是两套结果：字段合法但服务没启动时，configuration 可以 valid，而 dependency 仍然不 ready；字段缺失时则在 configuration 标记 invalid。响应不返回显式 API key 或 CLS secret 字段，但会按配置原样返回 provider、model、base URL、Milvus URI、collection 和 MCP endpoint；因此连接 URL 与 endpoint 中不得嵌入 userinfo、token 或其他凭据。\n📷 [图片 token=HqaLbZiheo6TP2xBV8pcQnOVnx9（未能下载，见飞书原文）]\nGET /metrics 从进程内 RequestMetrics 返回 request count、failure count 和 average latency。failure 只统计 HTTP 状态大于等于 500 的请求；数据随进程重启清零，也不是 Prometheus 格式的完整监控系统。它适合本地快速观察，不能被描述为长期指标存储或分布式 tracing。\n📷 [图片 token=SKuObt57YoABamx4zY1c20CinvC（未能下载，见飞书原文）]\n看什么：入口关系图把 liveness、readiness、配置诊断与本地指标分开，避免把“状态页面”误写成一个含义。\n这张图证明 /config/check 比 /ready 多回答“配置能否解析”，但不会因此替代真实连接检查；/health 与 /metrics 则刻意保持轻量。选择错误入口会得到语义正确却不适合问题的答案。\n请求关联与结构化事件 observe_request 把 request ID 放入 ContextVar，因此同一异步请求内调用 emit_event 时可以自动附带关联 ID。正常完成事件记录 method、path、status 和 latency；API 错误处理器只记录规范化 error code；未捕获异常只记录异常类型，不记录原始异常文本、header 或 body。\n看什么：middleware 优先复用请求头中的 ID，否则生成新 ID；无论成功或异常，finally 都更新本地指标并发出 completion 事件，最后恢复 ContextVar。\n# 1. 同一请求复用或生成稳定关联 ID。 request_id = request.headers.get(\u0026#34;x-request-id\u0026#34;) or f\u0026#34;req_{uuid4().hex}\u0026#34; request.state.request_id = request_id correlation_token = set_request_id(request_id) started_at = monotonic() response: Response | None = None try: response = await call_next(request) response.headers[\u0026#34;X-Request-ID\u0026#34;] = request_id return response except Exception as exc: # 2. 未捕获异常只记录类型，不复制原异常文本。 emit_event(logger, \u0026#34;request.error\u0026#34;, errorCategory=exc.__class__.__name__) raise finally: status_code = response.status_code if response is not None else 500 latency_ms = elapsed_ms(started_at) app.state.request_metrics.record(latency_ms=latency_ms, status_code=status_code) emit_event( logger, \u0026#34;request.complete\u0026#34;, method=request.method, path=request.url.path, status=status_code, latencyMs=latency_ms, ) reset_request_id(correlation_token) 这段代码证明关联 ID 同时进入响应头与请求范围日志。未捕获异常没有可用 Response，因此不能从这段代码推断异常响应一定带 X-Request-ID；但错误与 completion 事件在 ContextVar 重置前仍能共享该 ID。\n📷 [图片 token=HU1gb2XHXokeGex2cIOcdJgXn2b（未能下载，见飞书原文）]\nemit_event 在 JSON 编码前递归调用 _redact。字段名被规范化后，只要命中 authorization、password、secret、token、API key 等精确集合，或以 _key、_password、_secret、_token 结尾，值就替换为 [redacted]。像 privatekey 或 mySecret 这类不满足当前规则的名字不会自动命中。这是一道防守线，但调用方仍应遵守“不要把用户消息、文档正文、工具参数值和模型输出写进日志”的约束；脱敏不能替代正确的日志字段设计。\n📷 [图片 token=TABrbTw9mol3r2xzqBvcRDQAnxg（未能下载，见飞书原文）]\n看什么：脱敏根据父字段名递归决定是否替换值；字典、列表和元组都会继续遍历，其他对象最终转为字符串。\ndef _redact(value: object, *, parent_key: str | None = None) -\u0026gt; object: # 1. 命中敏感字段名时整值替换，不检查值的内容。 if parent_key is not None and _is_sensitive_key(parent_key): return \u0026#34;[redacted]\u0026#34; if isinstance(value, Mapping): mapping = cast(Mapping[object, object], value) return {str(key): _redact(item, parent_key=str(key)) for key, item in mapping.items()} if isinstance(value, list): return [_redact(item, parent_key=parent_key) for item in cast(list[object], value)] if isinstance(value, tuple): return [_redact(item, parent_key=parent_key) for item in cast(tuple[object, ...], value)] if isinstance(value, (str, int, float, bool)) or value is None: return value return str(value) def _is_sensitive_key(key: str) -\u0026gt; bool: normalized = key.replace(\u0026#34;-\u0026#34;, \u0026#34;_\u0026#34;).lower() # 2. 只覆盖精确集合和四类下划线后缀。 return normalized in _SENSITIVE_KEYS or normalized.endswith( (\u0026#34;_key\u0026#34;, \u0026#34;_password\u0026#34;, \u0026#34;_secret\u0026#34;, \u0026#34;_token\u0026#34;) ) 它证明脱敏是字段名驱动而不是通用内容扫描；未命名为规则可识别字段的秘密不会自动清除。因此调用方必须只传最小化运维字段，不能把整个请求、模型输出或工具 payload 交给 emit_event 后期待它自动安全。\n📷 [图片 token=YUfQbPYj7o32apxOH9OcPVZBnKb（未能下载，见飞书原文）]\n关键工作还有独立生命周期事件：文档索引记录任务和文档 ID，聊天/AIOps 记录会话或诊断 ID，MCP 记录工具名和参数键。它们使用 elapsed_ms 计算耗时，不把 embedding、完整工具输出或用户输入塞进日志。工具业务审计保存在 SQLite，和运行日志是两种不同的数据面。\n看什么：时序图展示 request ID 在一次异步请求内如何自动关联 API 错误、关键工作事件与最终 completion，并在结束后恢复上下文。\n这张图证明请求关联是进程内异步上下文传播，不是完整分布式 tracing。跨进程、队列或外部 MCP 的关联需要显式传递；当前图不能被扩展解释为已经存在全链路 trace backend。\n项目配置只从本地 JSON 合并 load_project_config 默认读取 config/project.json，如果 config/user.project.json 存在，则用 _deep_merge 递归覆盖相同路径。对象会继续合并，非对象值由用户配置替换。project_config_section 和 required_str、required_int 等函数在真正使用时校验字段类型。\n看什么：加载器先读基础 JSON，再解析用户配置路径；只有用户文件存在时才递归合并，代码中没有读取环境变量的补值分支。\npath = Path(config_path) if config_path is not None else DEFAULT_PROJECT_CONFIG_PATH config = _read_json_object(path) override_path = _default_user_config_path(path, user_config_path) if override_path.exists(): override = _read_json_object(override_path) # 1. 用户 JSON 仅在文件存在时覆盖基础 JSON。 return _deep_merge(config, override) return config # … 省略 JSON 读取与路径解析 def _deep_merge(base: Mapping[str, Any], override: Mapping[str, Any]) -\u0026gt; Mapping[str, Any]: merged: dict[str, Any] = dict(base) for key, value in override.items(): current = merged.get(key) # 2. 两侧都是对象才递归，否则用户值整体替换。 if isinstance(current, dict) and isinstance(value, dict): merged[key] = _deep_merge( cast(Mapping[str, Any], current), cast(Mapping[str, Any], value), ) else: merged[key] = value return merged 这段代码证明数组、标量和布尔值不会逐项合并，而是由用户值整体替换；缺失用户文件则完整保留基础配置。JSON 缺失、格式错误或顶层非对象会抛配置错误，不会悄悄从 .env 或进程环境恢复。\n后端业务配置不会从 .env 或宿主机环境变量补值。启动脚本中的环境变量导出只服务于单独运行的官方 CLS MCP Server：脚本先读取并合并两份 JSON，再设置该外部进程要求的 transport、port、腾讯云凭据和时区。不能由此推断 FastAPI 自身改成了环境变量配置。\n📷 [图片 token=WLsSbm1qwoed3MxhZBIcu72Un7d（未能下载，见飞书原文）]\n看什么：配置数据流图区分 FastAPI 的 JSON 读取和启动脚本为外部 CLS MCP 进程导出环境变量，两条消费者不能互相替代。\n这张图证明环境变量出现在启动拓扑中不等于后端配置来源发生变化。安全边界是两份运行时 JSON 都被 Git 忽略且可能含真实凭据；模板、日志和诊断响应不得复制这些值。\nCompose 与宿主机进程刻意分离 infra/compose.yaml 中的 etcd 和 MinIO 是 Milvus standalone 的依赖，Milvus 暴露 19530 与 9091，Attu 暴露本地管理界面，Alertmanager 暴露 9093。Compose 不包含 backend、frontend 或 CLS MCP Server，也不读取项目 .env。这使容器层只承担有状态基础设施，应用代码仍保留本机调试和文件访问体验。\n📷 [图片 token=A57LbOLhSostUcx6j2gc1M6Ynud（未能下载，见飞书原文）]\n看什么：Compose 中 Milvus 只依赖 etcd 与 MinIO 的健康状态，并暴露服务端口；这一段没有 backend、frontend 或 MCP 应用容器。\nmilvus: image: milvusdb/milvus:v3.0-beta command: [\u0026#34;milvus\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;standalone\u0026#34;] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin # 1. Milvus 的服务端与健康端口由基础设施栈暴露。 ports: - \u0026#34;19530:19530\u0026#34; - \u0026#34;9091:9091\u0026#34; volumes: - milvus-data:/var/lib/milvus depends_on: etcd: condition: service_healthy minio: condition: service_healthy # 2. 这里只定义基础设施健康检查，不启动应用进程。 healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9091/healthz\u0026#34;] 这段代码证明容器编排边界由文件本身落实，而不是文档约定。Compose 的健康只代表基础设施容器状态；FastAPI、Vue 和 CLS MCP 仍可能未启动，因此不能把 docker compose up 成功等同于完整工作台 ready。\nmacOS/Linux 启动器会检查 docker、npm、python3、uv 和 cls-mcp-server，启动 Compose，执行 uv sync 与 Alembic migration，并在对应端口未占用时启动 MCP、后端和前端，日志写到 apps/backend/var/。Windows 启动器完成相同角色的依赖检查、Compose、迁移和宿主机进程启动。启动器会安装依赖、写运行日志并启动服务，因此它不是无副作用的验证命令。\n📷 [图片 token=SBClbC4JtoT35WxYmEZcSvdynTd（未能下载，见飞书原文）]\n看什么：正式本机启动器先启动五项基础设施，再在后端目录安装依赖和升级迁移，最后按端口判断是否启动三个宿主机进程。\ndocker compose -f infra/compose.yaml up -d etcd minio milvus attu alertmanager ( cd \u0026#34;$BACKEND_DIR\u0026#34; # 1. 启动器会安装依赖并执行数据库迁移。 uv sync uv run alembic upgrade head ) if ! port_is_open \u0026#34;$PORT\u0026#34;; then ( cd \u0026#34;$BACKEND_DIR\u0026#34; nohup cls-mcp-server \u0026lt;/dev/null \u0026gt; \u0026#34;$RUNTIME_DIR/cls-mcp-server-local.log\u0026#34; 2\u0026gt;\u0026amp;1 \u0026amp; ) fi if ! port_is_open 8000; then ( cd \u0026#34;$BACKEND_DIR\u0026#34; # 2. 后端直接运行在宿主机，而不是 Compose 服务。 nohup uv run uvicorn super_ai.api.app:create_app --factory --host 127.0.0.1 --port 8000 \\ \u0026lt;/dev/null \u0026gt; \u0026#34;$RUNTIME_DIR/backend-local.log\u0026#34; 2\u0026gt;\u0026amp;1 \u0026amp; ) fi 它证明启动器是有副作用的运行入口：会拉起容器、同步依赖、迁移数据库、创建日志并启动后台进程。端口已占用时脚本跳过对应进程，但不会验证占用者就是本项目服务；因此启动后的最终事实仍应由 /health、/ready 与实际日志确认。\n📷 [图片 token=CcbSblEbio8GQfxworocw8Q9nnf（未能下载，见飞书原文）]\n看什么：拓扑图用进程归属而非调用顺序组织组件，清楚标出 Compose 与宿主机之间的连接边界。\n这张图证明 Compose 只负责有状态基础设施，应用与官方 MCP Server 保留在宿主机。失败边界也分层：容器 healthy 不能证明宿主机进程正常，反之 health 成功也不能证明 Milvus、LLM 或 MCP ready。\n数据、契约与状态 readiness 返回每个 dependency 的 ok、连接元数据、latency 或 tool count 以及规范化 error。configuration check 返回每个配置块的 valid 和部分配置上下文。共享 OpenAPI 定义位于 packages/api-contracts/src/openapi.ts，前端 health 客户端通过通用 createApiClient 解开统一响应 envelope。由于 endpoint 与 URI 可能原样出现在响应中，安全性依赖配置本身不把凭据写入 URL。\nWorkspaceLayout.vue 只用 /health 设置 isConnected。请求失败时标题栏显示降级，但不会把整个工作台路由替换成阻塞页。这是合理的 liveness 用法：用户仍可以查看已经加载到前端内存状态中的内容或继续排查，而更严格的依赖信息应到 /ready 和 /config/check 查看。\n请求 metrics 位于当前 FastAPI 进程内，使用锁保护并发更新。它没有 tenant 维度、路径标签或历史落盘。结构化日志则输出到本地进程 handler，启动脚本把 stdout/stderr 写入运行目录。二者共同提供本地排障基线，但不等同于集中式监控平台。\n📷 [图片 token=XRhDbphpTozSmFxCOoyclS3dnK0（未能下载，见飞书原文）]\n权限、安全与失败边界 /health、/ready、/config/check 和 /metrics 当前都不要求用户认证，因此响应必须坚持最小安全信息原则。代码不会返回显式 API key、Authorization 字段、CLS secret 字段或原始基础设施异常，外部服务异常会规范化为 “SQLite/Milvus/LLM/MCP unavailable” 等信息；但 base URL、Milvus URI 和 MCP endpoint 会按配置值返回，配置文件不得把凭据嵌进这些字符串。\nreadiness 的成功只证明探针执行时组件响应，并不证明完整业务链路。MCP readiness 能发现工具，不代表特定参数调用一定成功；LLM readiness 不证明长对话或 rerank 都成功；Milvus health 不证明每个 tenant 都有可检索 chunk。真实验收需要继续执行相应的垂直业务路径。\n📷 [图片 token=ZLSlbq1HzolHbfx7NF8coIqDnff（未能下载，见飞书原文）]\n本地启动也不会自动上传 CLS 日志、发布告警、创建用户、写入 SOP 或执行诊断。这些属于显式演示/运维动作。将它们放进普通启动会污染数据并制造“已经完成真实取证”的假象，因此当前脚本只负责基础设施、依赖、迁移和进程。\n阅读顺序与小结 先读 apps/backend/src/super_ai/api/app.py 的四个端点和请求 middleware，建立信号层级。\n再读两份 observability 模块，理解 metrics、request ID 和脱敏。\n进入 project_config.py，确认 JSON 合并和字段校验方式。\n对照 infra/compose.yaml 与两个启动脚本，画出本地进程拓扑。\n最后把 readiness、observability、配置加载和 infra 拓扑连起来，核对每项检查实际证明了什么。\n本地优先系统的可靠性来自清晰边界：存活不是就绪，配置合法不是依赖可用，依赖可用也不是业务闭环成功；容器负责基础设施，宿主机负责应用进程；日志记录安全信号，SQLite 保存业务审计。把这些层次分清，OncallAgent 的启动、排障和验收才不会依赖猜测。\n","permalink":"https://rsc-blog.pages.dev/oncall/AI%20Native%20%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E6%88%98%EF%BC%88%E6%99%BA%E8%83%BDOnCall%20Agent%EF%BC%89/05%EF%BD%9COncallAgent%20%E5%8A%9F%E8%83%BD%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/15.%20Readiness%E3%80%81%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7%E4%B8%8E%E6%9C%AC%E5%9C%B0%E8%BF%90%E8%A1%8C/","summary":"本地优先并不意味着“只要进程能启动就算正常”。OncallAgent 同时依赖 SQLite、Milvus、模型服务、MCP Server 和浏览器前端，其中任何一项都可能单独失败。仓库因此把存活检查、依赖就绪检查、配置诊断、请求指标和结构","title":"15. Readiness、可观测性与本地运行"},{"content":"AI Coding的经验和SOP 启动位置 启动方式 操作 IDEA 集成终端 在 IDEA 内置终端 cd 到项目根目录后执行 mc --code 其他 IDE 比如VS Code、Catpaw 的集成终端也可以 独立终端 手动 cd 至目标项目目录后执行 mc --code 建议：在项目根目录启动，因为Claude Code是根据启动目录名进行项目级记忆。\nClaude Code 启动后会自动读取项目根目录的 CLAUDE.md、.claude/settings.json、.claude/agents/ 等配置，同时也按照启动目录进行记忆。\n项目不强制是 git 仓库，建议 git 管理。如果多仓库的话每个子目录作为 git 仓库也是可以的。\n项目目录结构全景 一个完整的 Claude Code 项目配置结构如下：\n// 代码块 your-project/ ├── CLAUDE.md # 📋 项目级指令（团队共享，提交 Git） ├── CLAUDE.local.md # 👤 个人项目偏好（gitignore） ├── .claude/ │ ├── settings.json # ⚙️ 项目设置：权限、Hook、模型 │ ├── settings.local.json# 👤 个人项目设置 │ ├── CLAUDE.md # 📋 等效于项目根目录 CLAUDE.md │ ├── rules/ # 📏 模块化规则文件（按域拆分） │ │ ├── code-style.md │ │ ├── testing.md │ │ └── security.md │ ├── agents/ # 🤖 自定义子代理 │ │ ├── code-reviewer.md │ │ └── debugger.md │ └── skills/ # ⚡ 自定义技能 └── .mcp.json # 🔌 项目级 MCP 服务器配置 新手提示：起步只需 CLAUDE.md + .claude/settings.json 两个文件，其他配置随需求逐步添加\nCLAUDE.md 放哪 路径：项目根目录 CLAUDE.md（始终加载）。 三级配置： 全部 Claude Code 遵循： ~/.claude/CLAUDE.md 会话启动目录生效：CLAUDE.md CLAUDE.local.md 子目录读取该目录内容才生效： ./xxx/.../CLAUDE.md（适合模块级约定） 三、开始编程 编程思路：探索 → 计划 → 编码 → 评测 1. 探索（Explore）： 先让 AI 理解现状再动手。直接说需求，AI 会自动派内置的 Explore 子 Agent（只读、快速）去搜索代码、梳理调用关系，把结论摘要回主对话。\n2. 计划（Plan）： 进入 Plan 模式，让 AI 输出修改方案（改哪些文件、怎么改、风险点），人工确认后再执行。\n3. 编码（Code）： 按确认的方案分阶段执行，每完成一个可验证单元就跑测试验证。\n4. 评测（review/test）\n具体开发顺序看自己的开发习惯和使用的 plugin/skill 以上顺序比较偏 SPEC coding，也可以使用 TDD （测试驱动开发）等开发思想 简单示例 // 代码块 你：我要给交易服务加一个\u0026#34;大额订单风控拦截\u0026#34;功能，金额超过 10 万的订单 需要调风控接口校验。帮我理清现有下单流程，产物文档化。 AI（自动派 Explore 子 Agent 搜索）： 下单主流程在 TradeService.createOrder()，入口 TradeController。 当前校验链：参数校验 → 重复单校验 → 余额校验 → 落库。 建议在\u0026#34;余额校验\u0026#34;后、\u0026#34;落库\u0026#34;前插入风控校验。 你：/plan 按技术方案链接出方案 AI（Plan 模式，只规划不动手）： 方案： 1. 新增 RiskClient 调用风控接口 2. TradeService.createOrder() 中余额校验后加 checkRisk() 3. 失败抛 BizException(ErrorCode.RISK_REJECTED) 4. 新增 RiskClient 单测 涉及文件：4 个新增/修改 风险：风控接口超时需设 500ms 短超时 + 降级 你：（确认方案）创建SPEC.md，划分任务阶段和每个阶段的子任务，包括开发、评估、测试。 AI（创建 SPEC.md, 开发任务.md） 你：开始实现 AI（开始编码）... 1.项目初始化 /init — 项目初始化 **生成 CLAUDE.md **\n怎么用：进入项目根目录，启动后输入 /init。\n发生什么：Claude Code 扫描项目结构（技术栈、目录、构建命令），生成 CLAUDE.md。\n已存在会覆盖吗：不会直接覆盖，会合并/更新，建议先 review 再采纳。\n何时用：首次接入新项目，或项目结构大改后刷新。\n不使用 /init 创建：\n其他编程工具的 AGENT.md 可以直接复制 也可手动创建CLAUDE.md后用 /memory 追加 可以直接手动编辑 CLAUDE.md 写什么 核心原则： 简洁为佳。\n控制在 200 行以内\n模块级约定可拆到子目录 CLAUDE.md，读取该目录文件时才加载。\n一个简单示例：\n// 代码块 ## 技术栈 - Java 17 + Spring Boot 3 + MyBatis - MySQL 8 / Redis / RocketMQ ## 编码规范 - 价格字段一律用 BigDecimal - 异常抛 BizException(ErrorCode.XXX)，禁止 RuntimeException ## 流程规范 - 所有阶段成果文档化到 xxx 目录 - 重要决策记录日志 ## 不要做的事 - 不要新建 util 包，通用方法放 com.xxx.common - 不要 git push ## （选填）从网上看到的开发八荣耻 - 以瞎猜接口为耻，以认真查询为荣。 - 以模糊执行为耻，以寻求确认为荣。 - 以臆想业务为耻，以人类确认为荣。 - 以创造接口为耻，以复用现有为荣。 - 以跳过验证为耻，以主动测试为荣。 - 以破坏架构为耻，以遵循规范为荣。 - 以假装理解为耻，以诚实无知为荣。 - 以盲目修改为耻，以谨慎重构为荣。 /effort — 调节思考深度 参数：/effort low|medium|high|xhigh|max|ultracode 各档场景： 档位 场景 耗时 low 改个变量名、加注释 最快 medium 简单修改 适中 high 日常开发（默认） 较慢 xhigh/max 架构设计、复杂 Bug 定位 最慢 ultracode 任务托管，自动划分开发阶段、派遣 subagent 推进任务 较慢 默认 high，可以通过配置文件修改默认思考等级\n/Plan — 先方案设计后编码 怎么进入：输入 /plan（带需求描述），或按 Shift+Tab 在普通/Plan 模式间切换。 进去后做什么：直接描述需求，AI 只输出方案（改哪些文件、步骤、风险），不实际改代码。 怎么确认：方案满意后回复\u0026quot;开始实现\u0026quot;或\u0026quot;按方案执行\u0026quot;，AI 才动手编码。 怎么退出：Shift+Tab 切回普通模式。 何时用：多文件改动、重构、方案不确定时。简单改一个函数不必进 Plan。 进阶插件/skill： grill-me（Matt Pocock）：极简 skill，写代码前\u0026quot;往死里盘问\u0026quot;需求，一次只问一个分叉，能在代码库找到答案就不打扰你。适合需求模糊时先把意图聊透。 superpowers（Jesse Vincent）：20+ skill 的完整开发流水线（brainstorm → writing-plans → 子 Agent 实现 → TDD + 代码审查），相当于给 Claude Code 请了一位流程严格的工程经理。适合大型功能开发。包装较重，按需启用。 两者非替代关系：grill-me 是单点工具（拷问需求），superpowers 是完整方法论。可组合：grill-me 聊透需求 → superpowers 走流程实现。 Plan 模式背后是内置的 Plan 子 Agent（只读收集信息），它不能再派子 Agent，防止无限嵌套。\n2.子 Agent 使用 适用场景 上下文隔离：比如开发了一个 Skill，让 Claude 派一个 Subagent 测试效果，因为在开发会话测试会有开发过程上下文记忆。 并行化提速：比如开发涉及多个模块，分多个 Subagent 在不同模块目录下完成开发任务 多角色定制：比如对抗性方案讨论，两个 subagent 分别找方案 A 的合理性证据 / 不合理性证据，主会话总结给你确定方案。 多思考等级定制：比如想找某个确认点，主会话做既慢且占用上下文，可以扇出 effort 为 low/medium 的 subagent 去做。 省上下文：比如想找某个确认点，但代码仓库很大，链路很长，可以扇出 subagent 去做。 使用方式 直接自然语言说明即可，主会话会自动写 Subagent 的提示词。使用示例：\n// 代码块 派一个 effort 为 medium 的 Subagent 测试边界/功能点 用多个高思考等级的子代理帮我并行化开发/测试这几个模块 3.创建自定义子 Agent 适用场景 有已经开发好的 Agent 描述文件 自己总结了一个可复用的好 Agent 描述文件 通用子代理不满足业务要求 创建方式 手写 Markdown 文件或让AI创建到目录.claude/agents/，需重启 Claude Code 才加载。\n文件格式（YAML frontmatter + Markdown 正文）：\n// 代码块 --- name: code-reviewer description: 审查代码质量与安全问题。在代码修改后主动使用。 tools: Read, Grep, Glob, Bash model: sonnet --- 你是高级代码审查员。收到代码后： 1. 检查明显 bug（空指针、边界、未处理异常） 2. 检查安全问题（SQL 注入、硬编码密钥） 3. 检查性能问题（N+1 查询、循环内大对象） 输出格式：[严重程度] 文件:行号 问题 + 修复方案 怎么调用 自动调用：AI 根据 description 匹配任务自动派发。 显式调用：用 code-reviewer 子代理审查 src/service/ 下最近的修改。 4.Dynamic Workflow 使用 Workflow 是把多个子 Agent 按流程自动编排起来的上层模式——你只需描述复杂目标，Claude Code 自动拆解任务、调度多个子 Agent 并行执行、汇总结果。全程自动编排，无需手动串联。\n什么时候用 Workflow 任务可清晰拆解为多个独立子任务，且需要并行执行提速。 单会话上下文装不下整个任务，需要分片处理后汇总。 跨仓大项目（如前端 + 后端 + 测试同时开工）。 怎么触发 直接用自然语言描述复杂目标，Claude Code 会自动判断是否拆解为 Workflow；也可显式提及关键词 \u0026ldquo;用 dynamic workflow\u0026rdquo; 或者 \u0026ldquo;动态工作流\u0026rdquo;、\u0026ldquo;ultracode\u0026rdquo;。\n/effort ultracode 也会自动划分开发阶段、派遣 subagent 推进。\n示例（自动编排） // 代码块 你： 把 src/controller 下 10 个接口统一补参数校验和异常处理，自动拆分执行 ​AI（自动编排）： 拆为 10 个子任务 → 派多个 general-purpose 子 Agent 并行处理， 每个负责一个接口（加 @Valid + 全局异常处理），各自跑编译。 全部完成后汇总改动清单返回主对话。 ​你： （收到汇总清单，review 后提交） 四、开发过程管控 开发过程管控按\u0026quot;方向管控 → 上下文管控 → 记忆管控 → 成果归档\u0026ldquo;的逻辑顺序组织。\n方向管控确保不走偏；上下文管控确保窗口不爆；记忆管控把经验固化；成果归档收尾交付。\n4.1 方向管控：会话分叉与回滚 开发方向错了要能快速回退或并行验证，避免在错误路径上越陷越深。\n4.1.1 /branch \u0026lt;name\u0026gt; — 会话分叉 name 是什么：会话标签（不是 git 分支名），用于在分支列表里识别和切换。与 git branch 完全无关。 分叉后：当前对话状态复制到新分支，主干保持不动，两分支独立演进。 怎么切换：输入 /branch 列出所有分支选择切换；或 /resume 从会话列表选。 没有合并操作：/branch 分叉的是对话上下文，主会话看不到 /branch 之后的内容，你可以选择其中一个分支作为后续开发的主会话，或者 commit 之后，回到主会话让他读修改的代码。 何时用：想试另一个方案又不想丢当前进度；多思路并行验证。 4.1.2 /rewind — 回滚到检查点 怎么用：输入 /rewind，或按两次 Esc。弹出历史节点列表，选择回滚到哪个节点。 回滚范围：可同时回滚对话 + 代码，也可只回滚其一。 何时用：编码方向错误、需要撤销时；连续失败 2 次即应回滚，别在错误方向硬改。 检查点跨会话持久化：关闭终端后下次仍可回退。但检查点只追踪 Claude 的修改，不能替代 Git。 4.2 上下文管控：提问、压缩、恢复 上下文窗口是稀缺资源，要主动管理而非等系统自动处理。\n4.2.1 /btw \u0026lt;问题\u0026gt; — 顺带提问不打断 怎么用：主任务执行中产生关联疑问时，输入 /btw 问题内容。 特点：不打断主任务，答案在主对话以浮层显示，不进入会话历史、不污染上下文。 何时用：Claude 正在跑长任务，你想确认之前讨论过的某个细节——不用打断它，直接 /btw 问。 4.2.2 /compact [重点] — 主动压缩上下文 怎么用：/compact 自动压缩；或 /compact 重点保留 API 设计决策 指定保留内容。 何时用：上下文接近上限、完成阶段性任务后。建议主动用，别等系统自动压缩（自动时机不可控，可能正好压掉关键细节）。每完成一个阶段性任务（修完一个 Bug、开发完一个功能）就主动 /compact 重点保留 xxx。 压缩后：对话可正常继续，但细节可能丢失，所以要用\u0026quot;重点保留\u0026quot;指定关键信息。 4.2.3 /resume 与 /clear — 会话恢复与清空 /resume：恢复历史会话。列出历史会话可选，恢复后上下文还在。跨天/中断后继续时用。 /clear：清空当前会话。切换全新任务时用。清空后仍可通过 /resume 找回。 核心原则：干净会话 + 好提示词，几乎总是优于长会话 + 反复修正。任务切换时果断 /clear。\n4.3 记忆管控：规则沉淀 把开发中积累的经验固化到项目记忆，让 AI 下次自动遵循。\n4.3.1 /memory — 查看/编辑记忆 怎么用：输入 /memory，在编辑器中查看/编辑系统记忆与项目记忆全貌。编辑后立即生效。 调试技巧：当 AI 行为不符合预期时，先 /memory 查看加载了哪些记忆——问题可能出在过时的自动记忆覆盖了 CLAUDE.md 规则。 4.3.2 主动记忆声明 怎么提示（话术示例）：\n\u0026ldquo;记住这条规则：价格字段一律 long 单位分，写入 CLAUDE.md\u0026rdquo; \u0026ldquo;把这个架构决策记到项目记忆里\u0026rdquo; \u0026ldquo;这个坑以后别再踩，加到规则里\u0026rdquo; 写入机制：AI 通过 /memory 或直接编辑 CLAUDE.md 写入，不会覆盖已有内容，采用追加/合并。\n**# xxx**** 快速追加语法**：在对话输入框中以 # 开头打一行内容（如 # 价格字段一律 long 单位分），回车后 Claude Code 会将其作为规则追加到 CLAUDE.md，无需手动开文件。区别：# 是单条快速追加；/memory 打开完整记忆管理界面。\n踩坑规则示例条目：\n[踩坑] RiskClient 调用必须设 500ms 超时，否则会拖垮整个下单链路（事故 2026-08-05） 4.4 成果归档 怎么触发：阶段任务完成后，直接说\u0026quot;总结本轮改动\u0026quot;或\u0026quot;生成本次变更摘要\u0026rdquo;。也可用 /recap 输出会话回顾。\n应包含：\n改动文件清单 关键变更摘要（做了什么、为什么） 未完成项与后续待办 写到哪：对话内显示，或要求写入 CHANGELOG.md / PR 描述。\n4.5 Hook 配置 Hook 是事件驱动的自动化脚本，在生命周期节点自动触发，不靠提示词、不靠 AI 记忆，每次都跑。配置在 .claude/settings.json（项目级）或 ~/.claude/settings.json（用户级）。\n常用事件： PreToolUse（工具执行前，可 exit 2 阻断）、PostToolUse（工具执行后）、Stop（AI 完成一轮回复）、SubagentStop（子 Agent 完成）。\n平时使用直接让 Cluade Code 帮你配置即可。\n权限规则（deny/ask/allow）： 与 Hook 配合，在 permissions 字段配置，优先级 deny \u0026gt; ask \u0026gt; allow：\n{ \u0026ldquo;permissions\u0026rdquo;: { \u0026ldquo;allow\u0026rdquo;: [\u0026ldquo;Bash(npm run )\u0026quot;, \u0026ldquo;Bash(git diff )\u0026quot;, \u0026ldquo;Edit()\u0026quot;, \u0026ldquo;Write()\u0026quot;], \u0026ldquo;ask\u0026rdquo;: [\u0026ldquo;Bash(git push *)\u0026quot;, \u0026ldquo;Bash(rm *)\u0026quot;], \u0026ldquo;deny\u0026rdquo;: [\u0026ldquo;Read(.env)\u0026quot;, \u0026ldquo;Read(./secrets/**)\u0026quot;, \u0026ldquo;Bash(git push \u0026ndash;force *)\u0026quot;] } }\nallow 自动放行；ask 弹确认框；deny 直接禁止。 配置层级（高→低）：企业策略 \u0026gt; 命令行参数 \u0026gt; .claude/settings.local.json \u0026gt; .claude/settings.json \u0026gt; ~/.claude/settings.json。 建议： 起步只配一条 PreToolUse 拦截 rm -rf / git push --force，性价比最高\n五、反模式 以下行为会显著降低效率与质量，严格规避。每条附\u0026quot;症状\u0026quot;帮助识别：\n反模式 症状（怎么发现正在踩坑） 正确做法 静默修改文件 编译突然报\u0026quot;找不到符号\u0026rdquo;，排查发现 AI 改了包名/移了文件却没说 要求 AI 所有结构性变更（移动/重命名/删除文件）必须先列出并确认 多工具交叉修改 IDE 和 AI 同时改同一文件，保存时互相覆盖，改动莫名丢失 单一时段由单一主体持有编辑权；切回 IDE 前先让 AI 停手并提交 反复纠错不 Rewind AI 在错误方向上越改越乱，上下文堆积大量失败尝试，开始\u0026quot;遗忘\u0026quot;早期指令 连续失败 2 次即 /rewind 回到正确节点重选方向，别继续修补 不设权限卡控 AI 自信地执行了 git push --force / rm -rf，造成不可逆损失 用 Hook + 权限规则对敏感操作设 ask/deny（见第六章） 六、SOP 示例：全流程开发任务 注意：此例是为梳理以上提到的内容，这个任务实际开发过程中直接开 /goal 或者 dynamic workflow 基本就够用了\n本章以一个开发任务为例，把前文讲过的命令和能力按开发顺序串起来演示一遍。目标是让大家看清每一步用什么、怎么用、为什么用。\n任务背景：给交易服务加一个订单导出功能，支持按日期范围筛选，导出 CSV。 涉及能力：初始化、规则配置、Plan、子 Agent、Workflow、/goal、/branch、/rewind、/compact、/btw、/memory、Hook、归档。\n6.1 第一步：接入项目 进入项目根目录启动，这一步对应第二章的安装配置和项目目录结构。\n// 代码块 mc --code 首次接入跑 /init 生成 CLAUDE.md（对应 3.2.1）：\n// 代码块 你：/init AI：已扫描项目，生成 CLAUDE.md，含技术栈、构建命令、目录结构。 review 后用追加几条红线（对应 4.3.2）：\n// 代码块 你：/memory 不要 git push，价格字段用 BigDecimal AI：两条规则已追加到 CLAUDE.md。 最后配一条 Hook 拦截危险命令（对应 4.5）\n// 代码块 帮我加一个 hook，拦截所有 git push 和 rm -rf 命令 6.2 第二步：探索 + 计划 用 /plan 进入计划模式，让 AI 先理解现状再出方案（对应 3.1 编程思路、3.2.3 Plan 模式）。Plan 模式下 AI 会自动派 Explore 子 Agent 搜索代码（对应 3.3）。\n// 代码块 你：/plan 给交易服务加订单导出功能，支持按日期范围筛选，导出 CSV。先理清现有订单查询逻辑。形成 md 文档。 AI：md 文档 方案确认后，让 AI 创建 SPEC 文档划分任务阶段（对应 3.1 的 SPEC coding 思路）：\n// 代码块 你：创建 SPEC.md，划分任务阶段和每个阶段的子任务，包括开发、评估、测试。 AI：已创建 SPEC.md、开发任务.md。 6.3 第三步：编码 + 阶段验证 退出 Plan 模式开始编码，每完成一个可验证单元就跑测试（对应 3.1 的\u0026quot;编码\u0026quot;阶段）。过程中如果任务复杂，可以调高 effort（对应 3.2.2）。\n// 代码块 你：/effort xhigh 你：开始实现，先做 OrderExportService AI：修改 OrderExportService，编译通过。 你：跑 OrderExportService 的单测 AI：1 个边界用例失败：空日期范围未处理。 这里如果 AI 连续改不对，就 /rewind 回到改之前的状态重来，别硬改（对应 4.1.2）：\n// 代码块 你：/rewind AI：已回滚到测试失败前的检查点。 你：修复空日期范围边界，重跑测试 AI：已修复，单测全部通过。 6.4 第四步：评测 编码完成后进入评测阶段（对应 3.1 的\u0026quot;评测\u0026quot;阶段）。先用自定义子 Agent 做代码审查（对应 3.4）：\n// 代码块 你：用 code-reviewer 子代理审查 OrderExportService AI：审查结果： [WARNING] OrderExportService:45 大数据量未分批，建议加分页 [INFO] OrderExportService:30 日期格式硬编码，建议提取常量 采纳 WARNING，让 AI 修复分批逻辑。但分批有两种方案拿不准，用 /branch 分叉会话并行验证（对应 4.1.1）。\n主干会话先保留当前进度：\n// 代码块 你：/branch 同步分页方案 AI：已从当前会话分叉，进入分支「同步分页方案」。 分支：同步分页方案\n// 代码块 你：实现同步分页导出，跑压测记录 10 万行耗时 AI：同步分页实现完成。压测：10 万行耗时 8s，期间接口阻塞。 你：/branch 异步任务方案 AI：已从主干分叉，进入分支「异步任务方案」。 分支：异步任务方案\n// 代码块 你：实现异步任务导出，跑压测记录 10 万行耗时 AI：异步任务实现完成。压测：立即返回任务 ID，后台 6s 跑完。 方案A 同步阻塞 8s，方案B 异步立即返回。选方案B。\n之后就把分支：异步任务方案，作为主会话即可\n6.5 第五步：目标驱动收尾 进入收尾阶段，用 /goal 驱动 AI 自主迭代到全部测试通过（对应 3.6）。/goal 执行过程中，AI 会根据需要自动派子 Agent 或开启 Workflow 完成各阶段子任务（对应 3.5）。\n// 代码块 你：/goal 完成订单导出功能，要求： 1) 支持按日期范围筛选 2) 导出 CSV 含订单号/金额/状态 3) 单测覆盖筛选逻辑和大数据量分批 4) mvn test 全部通过 AI：目标已设定，开始推进……（自主派子 Agent 修剩余测试） AI：目标达成，mvn test 全部通过。 如果任务更大、可拆成多个独立子任务，也可以直接用 Dynamic Workflow 自动编排（对应 3.5.2）：\n// 代码块 你：用 dynamic workflow 把订单导出、用户导出、商品导出三个模块并行实现 AI：已拆为 3 个子任务，派 3 个子 Agent 并行处理，全部完成后汇总改动清单。 6.6 第六步：上下文管控 整个开发过程上下文用得多了，主动压缩（对应 4.2.2）。别等系统自动压缩，自动时机不可控可能压掉关键细节。\n// 代码块 你：/compact 重点保留导出功能的接口契约和分批逻辑 AI：已压缩，保留指定重点，上下文已释放。 过程中如果想确认之前讨论过的某个细节，又不想打断主任务，用 /btw（对应 4.2.1）：\n// 代码块 你：/btw 之前说的 CSV 编码是 UTF-8 还是 GBK？ AI：（浮层显示）之前确认用 GBK，兼容 Excel 直接打开。不影响主任务。 跨天中断后用 /resume 恢复（对应 4.2.3）；切换到完全不相关的任务用 /clear 清空。\n6.7 第七步：归档 + 记忆沉淀 任务完成后归档（对应 4.4）：\n// 代码块 你：总结本轮改动 AI：改动文件：OrderExportService（新增）、OrderController（加导出接口）、OrderExportServiceTest（新增） 关键变更：复用 OrderMapper 查询 + OpenCSV 生成 + 异步分批导出 未完成：导出文件清理策略待定 把这次踩的坑和关键决策沉淀到项目记忆，下次自动遵循（对应 4.3）：\n// 代码块 你：把这个分批导出的决策记到项目记忆 AI：已写入 CLAUDE.md：导出类功能须异步分批，避免 OOM。 6.8 流程小结 步骤 用到的能力 对应章节 接入项目 mc --code、/init、# 追加规则、Hook 二、3.2.1、4.3.2、4.5 探索 + 计划 /plan、Explore 子 Agent、SPEC 3.1、3.2.3、3.3 编码 + 验证 /effort、编译/测试、/rewind 3.2.2、3.1、4.1.2 评测 自定义子 Agent、/branch 多方案 3.4、4.1.1 目标驱动 /goal、Dynamic Workflow 3.6、3.5 上下文管控 /compact、/btw、/resume 4.2 归档 + 记忆 /recap、/memory 4.4、4.3 一次完整开发，把前文的初始化、Plan、子 Agent、Workflow、/goal、/branch、/rewind、/compact、/btw、/memory、Hook、归档全串起来了。日常开发不必每次都用全，按任务复杂度选用的即可。\n6.9 该任务真实使用提示词示例 // 代码块 /goal 给交易服务加订单导出功能。 开发目标： 1. 支持按日期范围（起止时间）筛选订单 2. 导出 CSV，含字段：订单号、金额（BigDecimal，单位分）、状态 3. 复用现有 OrderService.queryOrders() 查询逻辑，不重复造轮子 4. 大数据量导出须异步分批，避免 OOM 5. 单测覆盖日期筛选边界（含空范围）和分批逻辑 6. mvn test 全部通过 过程要求： - 先理解现状再出方案 - 创建 SPEC 文档 - 编码必须有上下文独立子代理进行review / test - 关键的开发决策记录到 log.md 供我审阅 - 合适的阶段启用 dynamic workflow 工具 - ... 边界要求：... 代码风格要求：... ...要求：... 问题处理： - 不确定的先查 ... - 还不确定找我人工确认 完成后给我一份总文档供我检查。 七、核心命令速查 命令 怎么用 使用时机 关键细节 /goal \u0026lt;目标\u0026gt; 设定持续目标，AI 循环推进直至达成 复杂任务需自主迭代 设定后 AI 反复推进不提前交卷；目标达成自动清除；可用 /goal clear 提前清 /branch \u0026lt;name\u0026gt; 从当前对话分叉，保留主干上下文 尝试替代方案又不想破坏当前进度 分叉后两分支独立；可切换；代码改动作用于真实文件（和 git branch 无关，是会话分叉） /btw \u0026lt;问题\u0026gt; 顺带提问 主任务执行中产生关联疑问 不打断主任务，答案在主对话显示 /rewind 回滚到之前的对话与代码状态 编码方向错误、需撤销 同时回滚对话+代码；可看到历史节点列表选择回滚到哪；快捷键 Esc Esc /compact [重点] 压缩上下文释放窗口 上下文接近上限、完成阶段性任务后 可指定保留重点，如 /compact 重点保留 API 设计决策；压缩后对话可正常继续，但细节可能丢失 /memory 查看/编辑系统记忆与项目记忆 追加规则、更新项目上下文 编辑后立即生效 /resume 恢复历史会话 跨天/中断后继续 列出历史会话可选；恢复后上下文还在 /clear 清空当前会话 切换全新任务 清空后可通过 /resume 找回 /init 生成/更新 CLAUDE.md 首次接入新项目 见第三章 /effort 调思考深度 复杂任务调高 见第三章 /model 切换模型 任务类型变化时 见第二章 建议： 主动用 /compact，别等系统自动压缩（自动时机不可控，可能正好压掉关键细节）。\n八、以上内容和 AI-SDLC 的关系 定位差异 对象 本质 解释 个人 Coding SOP 人工驾驭编程工具开发过程怎么做 主要是在做这些事： 1. 发挥 claude code 编程能力 2. 给编程工具提供编程指引 Pipeline AI Workflow 将人驾驭编程工具的方法论总结成一套标准的执行手册。并提供相应知识、对接工具的标准操作流程 主要是在做这些事： 1. 提供知识 2. 一套完整的工程化执行手册 3. 与开发流程工具的标准接入指引 4. 分阶段管控，人只在某个阶段（PRD、开发、测试）完成后才介入 5. 每个阶段提供标准化评估、打分 对比 Pipeline AI Workflow 优势 可控性强。每一步都在人手里，方向偏了能立刻拉回来，适合需求模糊、需要边探索边定的任务。 轻量灵活。没有重型编排开销，小任务直接上手，大任务按需挑能力组合。 过程透明。每个决策点都看得见、改得了，适合学习和沉淀个人经验。 劣势/可优化点 手动成本高。人发指令多，不像 Pipeline 给个需求就能自动跑完需求分析到部署。 没有结构化交付物。Pipeline 会自动产出学城技术方案、测试用例、提测单、交付报告并保证研发规范合规；依赖人记得提，容易丢。 不强制合规。SOP 是建议性的；Pipeline 是流程编排，天然对齐研发规范。 团队不可规模化。SOP 强依赖个人熟练度，新人上手慢；Pipeline 是标准化流水线，可复制。 缺端到端闭环。没有提测、部署、报告回写等。 缺乏知识提供，依赖人工使用工具进行项目梳理 缺乏标准化评估、打分内容 ","permalink":"https://rsc-blog.pages.dev/posts/2026/use-claude-code/","summary":"Use Claude Code","title":"Use Claude Code"},{"content":"基本信息 mcp 发布时间：2025.11.25 链接：https://github.com/modelcontextprotocol/modelcontextprotocol\nA2A 发布时间：2025.4.9 链接：https://a2a-protocol.org/latest/\n二者要要解决的问题：\n模型与外部世界的割裂：大模型是基于概率计算的新范式，其思维、推理能力类似于人脑。这种新范式虽强大，但仍需要与传统的结构化计算范式相结合，也就是通过工具调用来完成精确任务。 Agent 协作的孤岛效应：AI Agent 的兴起让“做事”的智能体成为可能，但不同框架（如 LangGraph、AutoGen、CrewAI）各自为政，缺乏统一的通信标准，跨平台协作如同“鸡同鸭讲”。 复杂场景的工程化瓶颈：RAG（检索增强生成）与多 Agent 系统需要处理多源数据、多模态交互和长时任务，现有工具链难以提供统一的解决方案。 Agent A ───── A2A协议 ───── Agent B 企业Agent 第三方Agent │ │ MCP MCP │ │ API \u0026amp;资源 API \u0026amp;资源 MCP MCP 就是一个为大模型更方便的利用外部资源（主要是工具）而设计的标准化接口，旨在打破模型与外部数据、工具之间的壁垒。\nRD before MCP 传统上，将 AI 系统连接到外部工具需要集成多个 API。每个 API 集成都意味着单独的代码、文档、身份验证方法、错误处理和维护。\n在引入 MCP 之前，如果我要让一个 Agent 同时具备“网络搜索”“数据库查询”“文本翻译”三种能力，通常要写三套适配器：\n搜索工具：手动拼 HTTP 请求，处理 OAuth 鉴权，解析 HTML 或 JSON，捕获请求超时。 数据库查询：配置 JDBC/ODBC 连接，管理连接池，拼装 SQL，逐行读取 ResultSet。 翻译服务：对接 Google Translate 或腾讯翻译 API，关注签名算法、流量限速、错误码处理。 RD after MCP MCP 则把这些繁琐细节都“藏”到服务器端：\n服务端只需注册好 Search、QueryDB、Translate 三个工具能力；\n然后 Agent 发起同一套 JSON-RPC 调用：\n{ \u0026#34;method\u0026#34;: \u0026#34;callTool\u0026#34;, \u0026#34;params\u0026#34;: { \u0026#34;tool\u0026#34;: \u0026#34;Translate\u0026#34;, \u0026#34;input\u0026#34;: \u0026#34;Hello, world!\u0026#34;, \u0026#34;target\\_lang\u0026#34;: \u0026#34;zh\u0026#34; } } MCP 服务器负责底层的 HTTP 请求、鉴权、连接管理和结果解析，最后把翻译结果一并返回给模型。 这样，开发者只需关心“模型想用哪个工具做什么”，再也不用为每个服务写一大堆样板代码，Agent 的功能扩展也瞬间变得“即插即用”。\n因此，它是一个客户端 - 服务器的架构。 模型负责推理和决策，客户端则动态提供上下文、工具和资源，而这些工具和资源则由外部服务器来提供。\n模型 │ └─ 推理与决策 │ ▼ 客户端 │ ├─ 上下文 │ ├─ 工具 ◄──── 外部服务器提供 │ └─ 资源 ◄──── 外部服务器提供 Function Calling 缺点 传统的 OpenAI Function Calling、LangChain 框架中的 Tools 或 JSON-RPC 相比，MCP 到底有何不同？\n尽管 OpenAI Function Calling、LangChain 的 Tools 让大模型能够实现工具调用，但它们都没能真正砍掉 Agent 或大模型应用开发的“高墙”。\nOpenAI Function Calling 只绑在 GPT 系列上，其调用规范、参数定义和返回格式都深度耦合在 OpenAI 的 API 流程中，无法扩展到其他开源或私有模型。\nLangChain Tools 虽然支持多种模型和工具，但所有能力都要写在 LangChain 的框架里——跨框架、跨语言、跨团队复用时，得重造一套适配逻辑。\nJSON-RPC 作为通用远程调用协议，轻量却太过底层：它只管“发什么请求、收什么响应”，但显然不包含大模型的上下文管理、并发会话、流式返回、能力发现等核心需求。\n它们解决了“怎么调”或“调哪个好”这一维度的问题，却没形成一套“面向大模型 + Agent 协作”的统一、可插拔、开箱即用的接口规范。\nMCP 改进 MCP 则在 JSON-RPC 之上，专门为大模型量身定制了：\n统一工具描述：所有 Search、Translate、QueryDB 等能力用同一份 tool.json 注册，模型只按名字“call”就行。\n会话与权限：自动管理多轮对话状态、工具访问权限与鉴权流程，模型无需关心鉴权细节。\n流式与事件：内建流式数据返回、异步回调和事件订阅，让长任务、实时监控、异步通知都能自然融入推理流程。\n多传输层：既可跑 HTTP/REST，也可挂 WebSocket、RPC 框架或消息队列，灵活适配不同部署场景。\nMCP 真正把“模型想用什么工具干啥”这件事变得极其简单——大模型专注决策，开发者专注业务逻辑，Agent 开发门槛被一举摁平。\n这样，MCP 的统一接口让开发者无需为每个场景定制协议，显著降低了工程化成本。\nA2A 它定义了一套清晰、标准的沟通方式，让所有智能代理可以顺畅地交流彼此的需求、能力、决策和状态。Agent 们通过 A2A 沟通，就像大家都学会了同一种语言，不管来自哪个“国家”，都能顺畅无阻地交流、互相配合完成任务。\n示例 用户向旅行规划 Agent 发送“规划北京到上海 3 天行程”的请求，旅行 Agent 通过 A2A 协议依次发出两次标准化能力调用：\n调用查询天气agent 调用查询酒店agent 旅游规划agent归纳 用户 │ ▼ 旅游规划 Agent │ ┌────────┴────────┐ ▼ ▼ 天气查询 Agent 酒店预订 Agent │ │ 天气信息返回 酒店数据返回 └────────┬────────┘ ▼ 旅行规划结果 A2A 能力 A2A 协议在这个过程中所负责的要点在于：\n统一协议：所有 Agent 均使用同一消息 schema 进行能力发现与调用。 异步协作：支持流式与异步返回，可并发处理多任务。 灵活扩展：新 Agent 即插即用，无需额外适配器。 解耦实现：Agent 专注业务，底层传输、鉴权、错误处理由协议层统一管理。 A2A 在 JSON-RPC、HTTP/SSE 等底层传输之上，定义了能力发现（通过 Agent 卡片以及标准化的能力定义）、会话管理、任务生命周期管理、消息与内容单元（Part）、权限认证、流式与事件等语义，使多智能体系统能够灵活拼接、异步协作，并具备企业级安全与可扩展性。\nA2A 支持长时任务、多模态协作，并强调企业级安全性，如 OpenAPI 授权和角色访问控制。 与 MCP 的资源管理结合，A2A 使 Agent 能够动态协商任务分配，实时共享数据洞察。例如，一个基于 CrewAI 的客服 Agent 可以通过 A2A 与 LlamaIndex 驱动的知识检索 Agent 协作，共同完成客户问题的精准解答。\nMCP vs A2A MCP 提供了统一的上下文管理与工具调用接口，整合了大模型驱动的概率计算与传统工具驱动的结构化计算。A2A 则为多 Agent 协同注入了开放标准。\n表格一：MCP 与 A2A 相同点\n对比项 MCP 与 A2A 相同点 标准化通信协议 两者都是为了解决信息孤岛，提供统一的、标准的通信机制，使得不同服务或 Agent 可以顺畅沟通。 可扩展性与通用性 都可以扩展到多种应用场景，无论是工具调用、资源整合（MCP），还是智能代理之间的协作沟通（A2A）。 客户端-服务器架构 MCP 明确定义为客户端驱动、服务器响应的方式；A2A 则是 Agent 之间类似“客户端-服务器”或“对等（Peer-to-peer）”的信息交换模式。 表格二：MCP 与 A2A 不同点\n对比维度 MCP A2A 使用场景 更专注于提升与工具（外部资源）的交互，解决的是模型工具调用、资源访问的问题，本质上是一种模型外部接口的标准化实现（这是 LLM 调用传统工具、结构化的、确定性的接口）； A2A 专注于不同智能代理（Agent）之间的协作与通信，本质上是一种 Agent 之间的消息、协作交互机制（这是 LLM 之间的、基于概率的、非结构化的接口）。 交互粒度 MCP 粒度更细，强调具体工具调用、数据交互的实现细节，比如请求、参数、返回等。 A2A 粒度偏高层次，更多强调的是任务协作和信息共享，尤其适合需要多轮沟通、结果反馈等协作层面的交互。 协议定义差异 MCP 定义了一套非常细的“原语（Primitives）”，如 Tools、Resources、Prompts、Memory、Transports、针对模型调用外部工具和资源的整个生命周期都有详细设计； A2A 则主要定义了智能代理间的消息格式、会话状态和信息交换机制，主要定义智能代理间的沟通标准、更抽象、更通用，但不涉及具体工具调用的细节。 ","permalink":"https://rsc-blog.pages.dev/posts/2026/mcp-a2a/","summary":"Mcp A2a","title":"Mcp \u0026 A2a"},{"content":"feishu-cli from github feishu-cli: https://github.com/riba2534/feishu-cli config：备用：Windows PowerShell 快速安装/更新脚本\n$dir = \u0026#34;$env:USERPROFILE\\.local\\bin\u0026#34; if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force } $url = \u0026#34;https://github.com/riba2534/feishu-cli/releases/download/v1.38.3/feishu-cli_v1.38.3_windows-amd64.tar.gz\u0026#34; $tmpFile = \u0026#34;$env:TEMP\\feishu-cli.tar.gz\u0026#34; Invoke-WebRequest -Uri $url -OutFile $tmpFile tar -xzf $tmpFile -C $env:TEMP Move-Item -Force \u0026#34;$env:TEMP\\feishu-cli_v1.38.3_windows-amd64\\feishu-cli.exe\u0026#34; \u0026#34;$dir\\feishu-cli.exe\u0026#34; Remove-Item -Recurse -Force $tmpFile, \u0026#34;$env:TEMP\\feishu-cli_v1.38.3_windows-amd64\u0026#34; -ErrorAction SilentlyContinue ","permalink":"https://rsc-blog.pages.dev/posts/2026/feishu/","summary":"Feishu","title":"Feishu"},{"content":"install nvm https://github.com/coreybutler/nvm-windows/releases\nnvm intall lts nvm use lts npm install -g agent-browser agent-browser install # Download Chrome from Chrome for Testing (first time only) agent-browser: https://github.com/vercel-labs/agent-browser 把skill下载到本地\nconfig chrome ### 一、确定二进制程序路径 agent-browser install 默认会将 Chrome for Testing 解压安装到用户根目录下的 browsers 文件夹： • 二进制文件路径： C:\\Users\\rsc\\.agent-browser\\browsers\\chrome-151.0.7922.77\\chrome.exe ────── ### 二、在 agent-browser 中的配置步骤 agent-browser 默认会自动识别并优先使用此路径下的 Chrome for Testing。如果你希望进行全局显式配置或配合 Cookie 记忆/数据盘隔离，步骤如下： #### 1. 编辑全局配置文件 打开或编辑 config.json： { \u0026#34;$schema\u0026#34;: \u0026#34;https://agent-browser.dev/schema.json\u0026#34;, \u0026#34;executablePath\u0026#34;: \u0026#34;C:\\\\Users\\\\rsc\\\\.agent-browser\\\\browsers\\\\chrome-151.0.7922.77\\\\chrome.exe\u0026#34;, \u0026#34;profile\u0026#34;: \u0026#34;E:\\\\.agent-browser\\\\profile\u0026#34;, \u0026#34;restore\u0026#34;: \u0026#34;default\u0026#34; } #### 2. 配置项说明 • executablePath: 强制指定启动 Chrome for Testing 路径。 • profile: 将用户数据、Cookie、缓存指定到 E 盘（避免 C 盘空间不足）。 • restore: default 开启会话与 Cookie 的自动保存和恢复。 ","permalink":"https://rsc-blog.pages.dev/posts/2026/agent-browser/","summary":"Agent Browser","title":"Agent Browser"},{"content":" 三方代充：小林 Coding（贵，但省事）\n1. 准备工具 iPhone、iPad 或 Mac 未注册过 Apple ID 的邮箱（QQ / 163 / Gmail 均可） 中国手机号（仅收验证码，可绑定多个不同区域的 Apple ID） 科学上网工具：使用美国、日本等 ChatGPT 运营地区节点，不要使用中国香港节点 2. 注册免税美区 Apple ID 全程无需翻墙，详细步骤见知乎教程。\n浏览器打开 Apple ID 注册页面。\n国家和地区选择 美国，出生日期填写 18 岁以上，然后用准备好的邮箱和手机号完成验证。\n在 iPhone 的 App Store 登录新账号，进入头像 → 付款与配送 (Payment \u0026amp; Shipping)。\n不添加信用卡，选择添加配送地址，填写俄勒冈州免税地址：\n街道：1200 SW Morrison St 城市：Portland 州：OR - Oregon 邮编：97205 电话：503-222-1234（符合格式即可） 保存即可。否则礼品卡可能被额外收取 8.6% 消费税，余额不足以支付 Plus。\n3. 购买并兑换礼品卡 App Store → 头像 → 退出原账号，登录美区 Apple ID。 支付宝在这个网站 Pockyt Shop，充值 App Store $20。 收到兑换码后，进入 App Store → 头像 → Redeem Gift Card or Code，手动输入并兑换。 确认账户余额显示 $20.00。 4. 订阅 ChatGPT Plus ChatGPT 账号邮箱无需与美区 Apple ID 一致。\n开启代理并连接美国节点。 打开 ChatGPT App，登录要升级的账号。 菜单 → Settings → Upgrade to Plus → Subscribe。 使用面容 / 指纹确认支付，系统会从 Apple 余额中扣款。 注意事项 关闭自动续费：App Store → 头像 → Subscriptions → ChatGPT → Cancel Subscription。取消后当前订阅仍有效。 无法完成购买：新账号首次消费可能触发风控。通过 Apple Support App 联系中文客服，说明“账号有礼品卡余额，但应用内订阅无法完成”，请求解除风控。 ","permalink":"https://rsc-blog.pages.dev/posts/2026/codex-chatgpt-charge/","summary":"美区 Apple ID 注册、礼品卡充值与 ChatGPT Plus 订阅","title":"Codex/ChatGPT 国内安全充值教程"},{"content":" 在大模型应用中，检索增强生成（RAG）在解决知识局限性和减少生成\u0026quot;幻觉\u0026quot;方面表现突出。RAG技术结合了检索和生成的双重优势，通过从外部知识库中检索相关信息并生成回答，显著提升了模型的响应能力。\n一、RAG定义与发展历史 RAG（Retrieval-Augmented Generation，检索增强生成）是一种结合检索和生成技术的方法。2020年，Facebook AI Research(FAIR)团队发表名为《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》的论文，首次提出了RAG概念。在大语言模型(Large Language Models)的领域中，RAG特指一种模式：模型在回答问题或生成文本时，首先从广阔的文档库中寻找相关信息。然后，模型使用这些找到的信息来生成回答或文本，从而提高其预测的准确度。\n使用RAG原因：\n信息滞后，私有数据匮乏：训练数据早且不全。 幻觉：底层原理是基于概率。 上下文限制：需要在有限上下文窗口给高价值的提示词语料。 数据安全：不能放到大模型上训练。 RAG三个阶段：\n原始RAG只具备了最基础的部分：离线索引构造、在线检索以及大模型生成。 高级RAG则是在这3个流程里增加更多细化的工作，例如数据预处理、滑动窗口、文章切片、用户query重写等，重点在检索层面的优化。 而模块化RAG对RAG流程步骤和能力做模块化，供各个业务场景灵活的选择和编排。本文主要讲解高级RAG的原理及其实践应用。 二、RAG架构 如图所示，是一个RAG的基本架构，可以分为离线和在线两部分。\n离线：对知识库文档进行解析、拆分、索引构建和入库。这部分会从用户给定的文档、图片、表格和外部URL等资源中提取内容，然后通过chunking（可以认为是将连续的文本分成一个个小块）进行合理切割，再使用Embedding模型变成向量数据存入向量数据库或Elasticsearch等载体中，同时结合这些非结构化文件所附带的元数据（时间、文件名、作者、副标题、文件类型等）进行索引创建。 在线：当用户输入问题之后，我们会对query进行分析，如关键词提取、意图识别等，然后再根据路由条件进行知识库的多种召回检索或者联网搜索等。如果是做向量数据库的检索，会先将查询内容通过Embedding模型转化为向量数据，接着在向量数据库中进行相似度匹配，比如从百万的数据块中找出匹配度较高的100个，然后再将这100个数据块进行更精准的重排序（如使用交叉熵校验的Rerank算法），将最相关的top k结果找到，最后将用户问题、经过技术处理的top k数据块、还有prompt一起提交给LLM，让它生成最终可靠的答案。 另外，为了进一步提高系统的稳定性，我们也会引入后置处理的环节，如风控检测、结果缓存和RAG相关指标的监控上报等。\n2.1 为什么是向量数据库 有传统的MySQL、MongoDB等数据库存在的情况下，RAG采用向量数据库的原因是什么？\n传统数据库，搜索功能都是通过不同的索引方式（B-Tree、倒排索引等）加上精确匹配和排序算法（BM25、TF-IDF）等实现的，本质还是基于文本的精确匹配，做关键字匹配的词法搜索。这种索引和搜索算法对于关键字的搜索功能非常合适。\n但在AIGC时代，搜索的主要诉求主要是做语义搜索，捕捉用户所输入语句后面的真正意图，并以此来进行搜索，从而更准确地向用户返回最符合其需求的搜索结果。如搜索\u0026quot;孟字去掉子\u0026quot;，用户想要找的并不是含有\u0026quot;孟\u0026quot;、\u0026ldquo;去掉子\u0026rdquo;，而是\u0026quot;皿\u0026quot;。\n深入分析，向量数据库在处理高维数据，深度学习模型生成的向量数据方面具有特殊的优势，表现如下：\n维度：深度学习模型生成的文本、图像或语音向量通常位于高维空间中。传统的关系型数据库并不擅长处理这类高维数据，因为它们主要是为处理结构化数据（如表格数据）而设计的。 速度：向量数据库专门设计用于存储高维向量，并支持快速的相似性搜索。向量数据库通过优化的索引结构和近似最近邻（ANN）搜索算法，能够高效地完成这一任务，显著提高检索速度。 推理：向量数据库支持基于向量相似度的复杂查询，可以根据查询的语义内容相关性而非仅仅是关键字匹配来检索信息，这使得向量数据库具有一定\u0026quot;推理\u0026quot;的能力，而非只是\u0026quot;查询\u0026quot;。 2.2 文档解析 RAG离线流程的第一步是做文档解析。文档解析不仅包括文本内容的识别，还涉及到图像、图表和表格等非文本元素的解析。很多时候，企业内部数据以各种各样的文件格式存在，如PDF、Word文档、PPT和Excel表格等，如何从大量非结构化数据中提取出内容，需要文档智能解析技术解决。下面我们以图片形式的文档（如对纸质文档进行了扫描、拍照等）为例进行说明。\n首先，我们需要对文档进行版面分析（Layout Analysis，也称布局分析），用于识别和理解文档中的视觉和结构布局。这里会使用到区域检测和区域分类技术。区域检测用于识别文档中的不同区域，如文本块、图像、表格和图表等。而区域分类则将检测到的区域进一步分类，如文本可以进一步被细分为标题、副标题、正文文本等；图像可能被分类为图片、图表或公式等；表格需要识别为包含数据的结构化形式。\n区域检测和分类后，需要对识别出来的具体区域分别进行识别，提取出数据内容。比如，对于表格区域，解析表格的行和列，识别单元格边界，提取结构化数据。对于文本区域，使用OCR引擎将图像中的文字转为机器可读的字符。\n当前随着大语言模型（LLM）和多模态技术的发展，文档理解领域逐渐出现了端到端的多模态模型，长期来看，大模型和文档理解进行结合应该是一个趋势，但目前多模态大模型与传统取得最优效果的模型方案相比，还不具备很好的竞争力，尤其是在处理细粒度文本的场景下。\n文档智能解析技术的应用非常广泛，可用于法律文档的信息提取、财务报表的数据整理、医疗记录的分析等多个领域，是搭建RAG系统不可或缺的部分。下面列举了几个用于文档解析的开源项目，有兴趣可以深入了解和使用。\nRAGFlow：基于深度文档理解构建的开源RAG引擎。最大特色是多样化的文档智能处理，其DeepDoc模块提供了对多种不同格式文档的深度解析。 Unstructured：灵活的Python库，专门用于处理非结构化数据，可以处理各种文档格式，包括PDF、CSV和PPT等。被网易有道的QAnything、Dify等项目使用。 PaddleOCR：百度推出的OCR开源项目，提供全面且高效的文字识别和信息提取功能。 DeepSeek OCR 2.3 文本分块 文本分块（text chunking）或称为文本分割（text splitting），是指将长文本分解为较小的文本块，这些块会被Embedding、索引、存储，用于后续的数据检索。通过将大型文档分解成易于管理的部分（如章节、段落，甚至是句子），可以提高搜索准确性和LLM处理性能，主要体现如下：\n提高搜索准确性：较小的文本块允许更精确的关键词匹配和语义相似性检索。 提升模型性能：LLM在处理过长的文本时可能会遇到性能瓶颈，通过将文本分割成较小的片段，检索出来后输入给大模型，可以使模型更有效地处理和理解每一部分，使得查询返回的信息更准确。 以下介绍一些常用的文本分块策略：\n按大小分块：将文本按固定字符数或单词数进行分割，这是最直接、最经济的分块方法，但也存在明显的问题——语义不连贯。按大小分块通常不考虑文本的语义内容，因此有可能将相关联的信息切割开，导致分出的文本块在内容上缺乏连贯性和完整性。\n特定格式分块：针对具有特定结构或语法特征的文本文件进行分块的一种方法，如Markdown、LaTeX、Python代码等。这种分块方式依据各自格式的特定字符或结构标记来实现，以保证分块后的内容在结构上的完整性和逻辑上的连贯性。\n递归分块：以一组分隔符为参数，以递归的方式将文本分成更小的块。如果在第一次分割时无法得到所需长度的块，它将递归地继续尝试。每次递归都会尝试更细粒度的分割符号，直到块的长度满足要求。\n语义分块：首先在句子之间进行分割，句子通常是一个语义单位，它包含关于一个主题的单一想法，再使用Embedding表征句子，最后将相似的句子组合在一起形成块，同时保持句子的顺序。\n命题分块：也是一种语义分块，它的原理是基于LLM来逐步构建块。整体流程先从基于段落的句法分块开始，得到文本段落，接着对于每个段落，使用LLM生成独立的陈述（或者命题），比如我们可以使用简单的提示\u0026quot;这段文字讨论了哪些主题\u0026quot;。下一步再移除LLM生成的冗余重复命题，对剩下的命题做索引并存储。在查询时，从命题语料库中检索，而不是原始文档语料库。\n文本分块并没有固定的最佳策略，选择哪种方式取决于具体的需求和场景，需要根据业务情况进行调整和优化。关键是找到适合当前应用的分块策略，而不是追求单一的完美方案。可以使用ChunkViz工具进行可视化，帮助调整分割参数。\n2.4 数据Embedding和存储 在使用向量数据库情况下，我们需要对上面解析和分块好的数据，基于嵌入模型（Embedding Models）将文本向量化（Embedding），存储到向量数据库中。\nEmbedding的本质是一种将高维稀疏数据转换为计算机易于处理的低维稠密向量的技术，通过这种转换，能够捕捉数据中的语义或特征关系。具体来说，Embedding用一个多维稠密向量来表示事物的多维特征，从而在一个连续的向量空间中刻画事物之间的相似性和差异性。\nEmbedding模型会根据不同的算法生成高维度的向量数据，代表着数据的不同特征。例如，对于文本，这些特征可能包括词汇、语法、语义、情感、情绪、主题、上下文等。对于音频，这些特征可能包括音调、节奏、音高、音色、音量、语音、音乐等。\n目前常用的Embedding模型包括：\n文本：OpenAI的text-embedding-ada-002（1536维） 图像：clip-vit-base-patch32 音频：wav2vec2-base-960h 在实际的业务场景中，往往不需要在整个向量数据库中进行相似性搜索，而是通过部分的业务字段过滤后再进行查询，所以存储在向量数据库的向量往往还需要包含元数据，例如时间、用户ID、文档ID等信息，这样就可以在搜索的时候，根据元数据来过滤搜索结果，提高搜索结果的准确性和相关性。\n2.5 用户Query理解 在线部分第一个重要的步骤是对用户的query进行理解，这一步也是RAG系统中非常关键的一步。query理解指的是对用户提出的问题进行深入分析，提取出关键信息，从而更准确地从知识库中检索出与用户查询最相关的信息，进而生成高质量的回答。query进行理解的原因：\n用户表达的模糊性：相同的词汇在不同的上下文中可能有不同的含义，通过query理解，系统可以分析上下文，判断用户的意图，从而检索到相关的正确信息。比如，用户输入\u0026quot;book\u0026quot;，可能指一本书，也可能指预订一个座位。 query和检索文档不在同一个语义空间：检索系统需要在不同的表达方式、术语使用、上下文信息等方面建立联系。用户的query通常是非结构化的，可能使用非正式或口语化的语言进行自由表达，而文档则可能采用正式的书面表达。 用户的query可能比较复杂：处理复杂query时，RAG系统需要能够识别并分解用户的查询，将其拆分为更小、更具体的子问题。 在RAG系统中，常用query理解技术分为三大类：query改写、query增强和query分解。\n（1）Query改写 - 上下文信息补全 在多轮对话中，用户的当前输入往往包含隐含的指代关系和省略的信息。我们可以通过使用大型语言模型（LLM），对当前的query进行重写，将上下文中隐含的信息纳入到新生成的query中。\n示例：\nUser：最近有什么好看的电视剧？ Bot：最近上映了《庆余年 2》，与范闲再探庙堂江湖的故事 User：我想看第一季 通过采用上下文信息补全，可以把前面的对话信息也纳入其中，生成类似\u0026quot;我想看庆余年第一季\u0026quot;的完整query。\n（2）Query改写 - RAG Fusion RAG Fusion用于提升搜索精度和全面性，它的核心原理是根据用户的原始query生成多个不同角度的query，接着通过使用倒数排名融合（Reciprocal Rank Fusion，RRF）技术，将多个query的检索结果进行融合，生成统一的排名列表。工作流程如下：\n多查询生成：通过使用LLM将原始用户查询扩展成多样化的查询，从而增加搜索的视野和深度。 倒数排名融合（RRF）：将多个系统的排名结果进行加权综合，生成一个统一的排名列表。 生成性输出：将重新排名的文档和查询输入到LLM，生成答案。 （3）Query改写 - Multi Query 跟RAG Fusion类似，Multi Query是一种通过生成多种视角的查询来检索相关文档的方法。它使用LLM从用户输入的查询生成多个不同的查询视角，然后为每个查询检索一组相关文档，并去重合并这些结果以获得更全面的文档集合。跟RAG Fusion不同的是，Multi Query没有使用RRF来融合多个搜索结果列表的排名，而是将多个搜索结果放到context中。\n（4）Query增强 - HyDE RAG向量检索在度量查询（query）和文档（doc）之间的相似性时存在一个挑战：query和doc不在同一个语义空间。为了解决这个问题，一种可行的方法是HyDE（假设性文档嵌入，Hypothetical Document Embeddings）技术。这种技术基于这样一个假设：与query相比，假设性回答，即LLM直接对query生成的答案，与文档共享更相似的语义空间。\nHyDE具体的工作流程：首先，HyDE针对query直接生成一个假设性文档或者回答（hypo_doc），接着对这个假设性回答进行向量化处理，最后使用向量化的假设性回答去检索相似文档。经过这么一顿操作，以前的query-doc检索就变成了query-hypo_doc-doc的检索。\nHyDE的核心优势：\n避免了在同一向量空间中学习两个嵌入函数的复杂性。 利用无监督学习，直接生成和利用假设文档。 在缺乏标注数据的情况下，仍能显著提高检索的准确性和效率。 （5）Query增强 - Step-back Prompting Step-back prompting技术旨在提高LLM进行抽象推理的能力，它引导LLM在回答问题前进行深度思考和抽象处理，将复杂问题分解为更高层次的问题，其包含如下两个主要步骤：\n抽象（Abstraction）：首先促使LLM提出一个更高级别的\u0026quot;回溯问题\u0026quot;（step-back question），涉及更广泛的高级概念或原则，并检索与这些概念或原则相关的相关事实。 推理（Reasoning）：在高级概念或原则的基础上，利用语言模型的内在推理能力，对原始问题进行推理解答，这种方法被称为基于抽象的推理（Abstraction-grounded Reasoning）。 Step-Back Prompting适用于需要复杂推理的领域，如：STEM领域（物理和化学等科学原理的应用问题）、知识问答（需要大量事实性知识的问题回答）、多跳推理（需要通过多个步骤或信息源进行推理的问题）。\n（6）Query分解 - IR-CoT IR-CoT（Interleaving Retrieval with Chain-of-Thought Reasoning，交错检索与思维链推理）是一种用于解决多步骤问题（Multi-Step Questions）的技术。IR-CoT通过交替执行检索（retrieval）和推理（reasoning）步骤来提高大型语言模型处理复杂问题时的表现，其核心思想是将检索步骤与推理步骤相结合，以指导检索过程并反过来使用检索结果来改进推理链（Chain-of-Thought，CoT）。\n（7）Query分解 - Least-to-Most Least-to-Most prompting也可以用于解决多步推理的问题，它将复杂问题分解为一系列更简单的子问题，并按顺序解决这些子问题。包括两个主要阶段：\n分解（Decomposition）：将复杂问题分解为更简单的子问题。 子问题解决（Subproblem Solving）：使用之前解决的子问题的答案来帮助解决当前子问题。 2.6 路由和数据检索 理解用户query后，进入查询路由步骤，通过定义查询路由器以及各个查询数据插件，将用户查询情况传给LLM，通过LLM决策，决定接下来要调用哪个查询插件，然后调用执行路由选择的插件，最后将各个插件预定义格式返回的结果汇总。\n向量数据库的核心在于相似性搜索(Similarity Search)。实际上，只要维度够多，我们就能够将所有的事物区分开来，世间万物都可以用一个多维坐标系来表示，它们都在一个高维的特征空间中对应着一个坐标点。这样就能够通过计算向量之间的距离来判断它们的相似度，这就是相似性搜索。\nANN近似最近邻搜索 最原始的方法是遍历数据集中的每一个向量vi，计算查询向量q与每个向量vi的距离，并最终获得k个真实的最近邻。这种NNS (Nearest Neighbor Search)最近邻搜索，可以找到数据集中与给定查询向量q距离最小的向量。但是，在大规模高维数据集中，由于数据集规模的增长和维度灾难，精确的最近邻搜索可能非常耗时。高效的搜索算法通过两种方式提高搜索效率：\n减少向量大小——通过降维或减少表示向量值的长度。 缩小搜索范围——可以通过聚类或将向量组织成基于树形、图形结构来实现，并限制搜索范围仅在最接近的簇中进行。 实际上，除了暴力搜索能完美搜索出最相邻，所有的搜索算法只能在速度和质量还有内存上做一个权衡，因此这些算法也被称为ANN(Approximate Nearest Neighbor)近似最近邻搜索算法。现有ANN算法主要可以分为四种类型：基于树的算法 (KD-tree、R-tree)；基于哈希的算法（LSH）；基于量化的算法(PQ、IVF-PQ)；以及基于图的算法（基础图搜索算法、HNSW）。\n向量相似度算法 名称 欧几里得距离 余弦相似度 点积相似度 特点 反映向量的绝对距离，适用于需要考虑向量长度的相似性计算 对向量的长度不敏感，只关注向量的方向，适用于高维向量的相似性计算 兼顾长度和方向，简单易懂，计算速度快 在RAG架构主要做语义搜索场景里面，一般选择余弦相似度进行向量计算召回，主要考虑：\n长度不敏感：在文本数据中，文档的长度可能会有很大的差异，这会影响到向量的长度。余弦相似性只关注向量的方向。 方向敏感：在问答系统中，我们通常关心的是文档的主题或者内容是否相似，余弦相似性可以很好地反映出文档的主题或者内容是否相似。 高维数据：向量Embedding模型表征的高维度(768/1024/1536\u0026hellip;)向量，适合余弦相似性处理。 元数据过滤 在实际的业务场景中，由于往往不需要在整个向量数据库中进行相似性搜索，而是通过部分的业务字段进行过滤再进行向量查询，所以存储在数据库的向量往往还需要包含元数据。为此，向量数据库通常维护两个索引：一个是向量索引，另一个是元数据索引。过滤过程可以在向量搜索之前或之后执行：\nPre-filtering：在向量搜索之前进行元数据过滤。虽然这可以帮助减少搜索空间，但也可能导致系统忽略与元数据筛选标准不匹配的相关结果。 Post-filtering：在向量搜索完成后进行元数据过滤。可以确保考虑所有相关结果，在搜索完成后再将不相关的结果进行筛选。 2.7 搜索结果重排 得出搜索结果后，我们需要对搜索结果进行重排，为什么需要重排这个步骤呢？\n一方面是在RAG框架中，将信息编码为向量时不可避免地会遭受信息丢失，同时近似最近邻搜索（ANN）只能提供近似匹配而非精确匹配，导致检索结果中含有一定程度的噪声。\n另一方面则是提高LLM推理速度与准确率。引入重排后，能够去除上下文不相关的污染数据，提供更精准的上下文信息。重排后精准的上下文不仅可减少token的使用量，也可以提高LLM推理速度与准确率。\n在实际业务场景中，我们可以增加top_k的大小，比如从原来的10个增加到30个，然后做筛选后使用更精确的算法来做rerank，常用的筛选和排序策略如下：\n基于相似度分数进行过滤和排序 基于关键词进行过滤，比如限定包含或者不包含某些关键词 让LLM基于返回的相关文档及其相关性得分来重新排序 基于时间进行过滤和排序，比如只筛选最新的相关文档 基于时间对相似度进行加权，然后进行排序和筛选 目前rerank模型里面，较为知名的是Cohere公司的模型（收费），开源的是智源发布的bge-reranker-base和bge-reranker-large。bge-reranker-large的能力基本上接近Cohere公司的模型。\n2.8 大模型生成结果 检索模块基于用户查询检索出相关的文本块，回复生成模块让LLM利用检索出的相关信息来生成对原始查询的回复，但由于可能检索出来多个文本块，有两种不同的回复生成策略：\n策略一：依次结合每个检索出的相关文本块，每次不断修正生成的回复。有多少个独立的相关文本块，就会产生多少次的LLM调用。 策略二：在每次LLM调用时，尽可能多地在Prompt中填充文本块。如果一个Prompt中填充不下，则采用类似的操作构建多个Prompt。 大模型生成步骤真正发挥巨大作用的是LLM，唯一需要我们关注的就是Prompt工程。Prompt的编写技巧主要可以参考OpenAI提到的6条原则：\nWrite clear instructions（写出清晰的指令） Provide reference text（提供参考文本） Split complex tasks into simpler subtasks（将复杂的任务拆分为更简单的子任务） Give the model time to \u0026ldquo;think\u0026rdquo;（给模型时间\u0026quot;思考\u0026quot;） Use external tools（使用外部工具） Test changes systematically（系统地测试变更） 另外，《Lost in the Middle: How Language Models Use Long Contexts》文章指出，当相关信息出现在输入上下文的开头或结尾时，性能往往最高，而当模型必须在长上下文中间获取相关信息时，性能会明显下降。\n2.9 后置处理 这一步主要是提高系统的稳定性，可以引入后置处理的环节，如：\n风控检测：校验是否存在风险词、违禁词、涉黄涉政；医疗领域的开药规则，金融领域的话术约束；回复是否正确或仍存在幻觉等。 结果缓存：提升搜索性能。 监控上报：RAG相关指标的系统指标和业务指标（大模型调用性能，RAG整体执行性能，回复率，搜索召回率等）。 三、RAG的挑战与解决方案 在实际的业务场景中，RAG的落地应用会遇到各种各样的问题，需要我们去逐步迭代解决。\n3.1 数据处理 当选用按字符或token数分割数据的朴素分块方法时，会出现数据分块频繁重复的问题，使得向量数据存储中出现重复向量或Embedding空间中由相同向量表示类似的单词等情况。\n解决策略：\n源数据去重：在生成Embedding之前，确保源数据没有重复，如对于文本数据，重复数据删除方法可以包括大小写规范化、词干提取、词序化以及删除标点和特殊字符等。 向量相似阈值：如果两个或多个向量的余弦相似度高于某个阈值，则将向量视为重复，保留一个并删除其他向量。 其他数据处理问题和解决方法：\n文件中冲突的内容：分门别类存放数据，减少文件中相似的内容，或将其分在不同的知识库中。 减少具有歧义的句子：避免汉语的高级用法，如歧义句，防止相似度模型对比时搜索错误。 结构复杂的数据：先根据大模型以问答对的形式输出。 数据的有效性：存储的数据知识也需要定期维护更新。 3.2 分块策略和Top-k召回 出于实现成本和复杂度考虑，一般数据分块会采用按大小或者按特定格式分块的分块策略。微软对不同分块尺寸下数据检索召回表现的分析显示，数据块越大，数据召回率会降低。但分出的数据块越小，丢失的信息就越多，因此过小的分块可能也不太理想。当前寻找最优块大小就像参数调优一样，需要不断用自身的数据做实验来逐步调整。\n重叠可以帮助将相邻的块链接在一起，并为块提供更好的上下文信息。然而，根据微软的分析，即使是非常激进的25%重叠也只能将准确率提高1.5%。这意味着重叠并不是优化RAG性能的最有效方法。\n确定最佳块大小的方针：\n清洗数据：在确定应用程序的最佳块大小之前，需要首先预处理清洗数据以确保质量。 选择一个范围的块大小测试：考虑内容的性质、将要使用的Embedding模型及其能力。从128或256个tokens到512或1024个tokens的范围进行测试。 评估每个块大小的性能：运行一系列查询来评估质量，并比较不同块大小的性能。 3.3 Embedding模型选择 Embedding在RAG系统中扮演着至关重要的角色，主要有以下作用：\n对query和私域知识进行向量化表示：通过使用预训练的语言模型（如BERT、DPR等），将query和分块文本转换为向量。 动态更新知识库：新文档经过处理之后会被实时转换为向量并添加到向量数据库中。 数据隐私和安全：向量模型通过将私域知识转换为向量表示，实现了数据的匿名化。 以下推荐的Embedding模型：\n模型 特点 OpenAI Embedding text-embedding-ada-002，支持8K tokens输入，需付费 JinaAI Embedding 支持8K tokens的开源模型，中文友好 BAAI/bge 智源开源，bge-m3支持100+语言和8K长度，免费商用授权 Hugging Face推出了MTEB（Massive Text Embedding Benchmark）测试框架，旨在评估文本Embedding模型在多种任务上的性能。可通过MTEB排行榜对比不同模型的表现。但排行榜只能作为参考，具体选择还需结合业务特点进行综合权衡。\n3.4 向量数据库选型 主流的向量数据库主要分为四个类别：\n类型 代表产品 特点 基于传统数据库改造 PG Vector 无缝集成现有PostgreSQL 基于倒排搜索扩展 Elasticsearch 丰富的API和插件支持 基于向量检索库实现 Chroma 轻量级，运行效率高 原生分布式向量数据库 Milvus 从零设计，功能完整，支持分布式 各数据库优缺点对比：\n数据库 PG Vector Elasticsearch (ES) Qdrant Milvus 优点 无缝集成PostgreSQL；开源易用 多种数据存储方式；丰富的API和插件 HNSW算法向量索引；查询性能出色；分布式支持 多种向量索引算法；查询性能出色；分布式支持 缺点 向量索引能力相对较弱；大规模数据性能不如专业向量数据库 需借助第三方插件支持向量索引；大规模下性能不如专业向量数据库 数据持久化能力相对较弱；集成方面有局限性 数据持久化需与其他存储系统结合；项目较新，集成有限制 3.5 检索模块的调优 值得强调的是，检索模块才是RAG模块调优空间最大的部分，而并非大模型本身，尤其是在项目前期。毕竟\u0026quot;查的准\u0026quot;是大模型最终能吐出正确结果的前提条件。以下是一些检索模块的常用调优思路：\n构造意图识别模块：可以是分类模型、词典，甚至是知识库检索时加阈值，对知识库外的内容进行拒识，同时对知识和query进行分类对应，提升检索准确性。 新增字面检索：用户的输入往往是长尾效应，前期覆盖高频说法就能明显提升指标。字面检索还能做得更精细，例如配合实体抽取来做。 增加追问机制：通过Prompt中加入追问策略，依靠大模型能力引导用户逐步明确问题。 query拓展：借助上下文、同义词、大模型拓展、相似度召回等方式。 多路召回和精排：字面、向量召回都做，甚至向量召回用多个不同策略，然后用规则或模型做精排。 Embedding模型调优：早期用预训练模型即可，后期结合业务数据进一步调优。 多层索引：在大型数据库情况下，创建两个索引（摘要+文档块），分两步搜索。 3.6 幻觉检测 让机器识别幻觉内容极具挑战性。目前主要方法包括：\n交叉验证：使用三个不同的大模型对同一个问题进行回答，然后用Embedding模型进行相似度计算。如果结果间相似度非常高（超过阈值），那么通过。如果出现分歧，记录log推给人检查。 验证链(Chain-of-Verification，CoVe)：通过系统地验证和完善回答以尽量减少不准确性。大型语言模型生成的响应可以用来验证自身。 引用或归因生成：在RAG的知识问答场景下，让模型生成的内容能够与参考信息对齐，提供证据来源。具体方法包括模型生成引用和动态计算引用。 四、RAG适用场景 在大语言模型的应用过程中，有Prompt Engineering、RAG、微调三种应用方案，目前考虑业务效果，主要以RAG、微调两种技术方案为主。\n特征比较 RAG 微调(Fine-tuning) 知识更新 直接更新检索知识库，确保信息持续更新，适合动态变化的数据 存储静态数据，需要重新训练用于更新 外部知识 擅长利用外部资源，适合处理文档或数据库 适合将外部知识与大语言模型对齐 数据处理 对数据的处理和操作要求极低 依赖于构建高质量的数据集 模型定制 侧重于信息检索和融合外部知识 允许根据特定风格或术语调整模型行为 可解释性 答案能追溯到具体数据来源，可解释性高 黑盒子，可解释性相对较低 计算资源 需要支持检索策略和数据库的资源 需要准备训练数据集和计算资源 延迟要求 因涉及数据检索，可能带来较高延迟 可以不通过检索直接回应，降低延迟 降低幻觉 每个回答基于检索到的实际证据，更不容易产生幻觉 面对未训练过的输入时仍可能出现幻觉 五、RAG的评估 \u0026ldquo;如果你不衡量，你就无法改进\u0026rdquo;。构建了基本的RAG应用程序后，需要创建一个评估过程。\n5.1 评测指标 论文《RAGAS: Automated Evaluation of Retrieval Augmented Generation》把RAG系统的评估指标拆分为三个维度：\n事实准确度（Faithfulness）：评测生成的回答是否忠实于contexts，对避免幻觉并确保检索到的上下文可以作为生成答案的理由非常重要。 答案关联性（Answer Relevance）：生成的答案应解决所提供的实际问题。 上下文相关性（Context Relevance）：检索的上下文应重点突出，尽可能少地包含无关信息。 5.2 评测方法 RAGAS：一个开源的RAG评估框架，基于gpt-3.5-turbo-16k模型全自动衡量答案的准确性、相关性和上下文相关性。 LlamaIndex Evaluating：提供了衡量生成结果质量和检索质量的关键模块。 总结 RAG技术通过结合检索和生成的双重优势，显著提升了大模型的响应质量。从离线文档解析、文本分块、向量化存储，到在线Query理解、路由检索、结果重排和回复生成，整个RAG pipeline的每个环节都有丰富的优化空间。结合具体的业务场景，灵活选择和编排各RAG模块的能力，才能发挥出RAG技术的最大价值。\n","permalink":"https://rsc-blog.pages.dev/posts/2026/rag-overview/","summary":"全面解析RAG（检索增强生成）技术：从定义、架构、检索、增强到生成的完整流程，涵盖Query理解、向量数据库、Embedding模型、分块策略等核心环节，同时探讨RAG的挑战、适用场景及实践案例。","title":"一文看懂RAG：检索增强生成"},{"content":"常规安装 windows 默认安装到 C 盘，配置在用户目录下的 .hermes 文件夹下\niex (irm https://hermes-agent.nousresearch.com/install.ps1) Windows 非C盘安装 `Hermes默认安装在C盘的用户目录下。或者安装WSL后在Linux子系统中安装Hermes。 这是非常规的安装方式，主要是为了避免C盘空间不足的问题。 powershell 执行：\n\u0026amp; ([scriptblock]::Create((irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1))) ` -HermesHome \u0026#34;D:\\hermes-agent\u0026#34; ` -InstallDir \u0026#34;D:\\hermes-agent\\code\u0026#34; 模型配置 下面两种配置方式中的 BASE_URL 和 API_KEY 需替换为实际获取的。\n方式一：在终端输入以下命令快速配置\n这里的 model.provider 只能设置为 custom，自定义其他名称将不合法，比如 xiaomi-coding。\nhermes config set model.provider custom hermes config set model.base_url BASE_URL hermes config set model.api_key API_KEY hermes config set model.default mimo-v2.5-pro 方式二：手动编辑配置文件\n手动编辑 D:\\hermes-agent\\config.yaml 或者 C:\\Users\\yourname\\.hermes进行配置：\nmodel: provider: custom base_url: BASE_URL api_key: API_KEY default: mimo-v2.5-pro 配置完成后，使用以下命令验证：\nhermes doctor 使用 Hermes Agent 配置完成后，执行以下命令启动：\nhermes 初次启动会让你选择channel连接（微信飞书之类），可以跳过后续使用下面的命令触发。\n渠道连接 hermes gateway setup 然后按指示操作即可，扫码方式会自动配置，自定义配置按指引操作即可，配置完成后发消息测试，首次回应可能会很慢\n","permalink":"https://rsc-blog.pages.dev/posts/2026/hermes/","summary":"Hermes","title":"Hermes"},{"content":"尽管《心智社会》这本书争议颇多，但是其中不少理论都作为当前AI Agent的哲学理论基础。\nPrologue 序言 开篇引用爱因斯坦的话，我觉得选的很好： Everything should be made as simple as possible, but not simpler. – Albert Einstein 凡事应尽可能简单，但不可更简单。\n此书要解释的核心要点是智能如何从非智能中产生?\n作者的观点是你可以用许多本身没有心智的微小部件来构建一个心智。\n乍一听觉得不可能，但稍微一想就觉得事实就是这样。从生物学上来说大脑就是由大量最简单的突触结构组成的，而深度神经网络也可以由最简单的线性神经元组成（添加非线性）。甚至可以极端一些，由无机物分子组成有智慧的动物。\n1 Building blocks 1.1 The agents of the mind 作者认为心智理论应该阐述好这三个尺度：\n慢的，代表我们大脑进化的十亿年； 快的，代表婴儿期和童年期短暂的几周和几个月； 以及介于两者之间的，代表我们思想在历史长河中发展的几个世纪。 作者抛出了几个问题：\n功能：智能体如何运作？ 具身性：它们由什么构成？ 交互：它们如何沟通？ 起源：最初的智能体来自哪里？ 遗传：我们生来就拥有相同的智能体吗？ 学习：我们如何创造新的智能体并改变旧的智能体？ 性格：最重要的智能体类型有哪些？ 权威性：当智能体意见不一致时会发生什么？ 意图：这样的网络如何产生想法或驱动？ 能力：智能体群体如何做到单个智能体无法做到的事情？ 自我性：是什么赋予它们统一性或个性？ 意义：它们如何理解事物？ 感知性：它们如何拥有感觉和情绪？ 意识：它们如何拥有意识或自我意识？ 作者说：将心智视为一个由众多主体组成的系统，每个问题的答案都会为其他问题提供启示。\n1.2 The mind and the brain *诗人伊姆拉克曾说，*人们 从未认为思考是物质固有的，或者说每个粒子都是有思想的存在。然而，如果物质的任何一部分缺乏思想，我们又能假设哪一部分会思考呢？物质之间的区别仅在于形态、密度、体积、运动和运动方向。意识又能依附于这些特征中的哪一项呢？无论这些特征如何变化或组合。圆形或方形、固体或液体、巨大或微小、缓慢或快速地运动，无论朝哪个方向，这些都是物质存在的方式，它们都与思考的本质截然不同。如果物质曾经没有思想，那么它只能通过某种新的改变才能被赋予思想；但它所能接受的所有改变都与思考能力无关。——塞缪尔·约翰逊\n1.3 The society of mind 作者举了一个例子来说明身体内部有非常多个系统在运作：\n为了保持手中茶杯的水平，光是控制手腕、手掌和手的形状，就至少有上百个系统在协同工作。另外还有上千个肌肉系统，负责控制所有骨骼和关节的活动，让你的身体能够行走。为了维持身体的平衡，所有这些系统都必须与其他系统相互协调。如果你不小心绊倒了怎么办？这时，许多其他系统会迅速做出反应，纠正你的姿势。有些系统负责调整你的身体重心和脚的位置。另一些系统则负责处理茶水：你不想烫伤自己的手，也不想烫伤别人。你需要快速做出决定的能力。\n我们总是同时做好几件事，比如计划、走路、说话，这一切看起来如此自然，以至于我们习以为常。但实际上，这些过程涉及的机制远超任何人的理解范围。\n1.4 The world of blocks 成年人都知道如何把积木堆成一排，现在看来，它们似乎只是常识。但幼年时第一次接触到积木玩具，需要花几个星期学习如何玩它们。我们完全记不起是怎么学会的。\n到了成年，我们往往认为这些都是简单的*常识。*然而，这看似简单的两个字背后，却隐藏着无数不同的技能。\n常识并非简单之物。相反，它是一个庞大的、由来已久的实用理念构成的体系——包含无数在生活中习得的规则和例外、性情和倾向、平衡和制衡。\n本书的每个步骤都运用了两种不同的方式来思考主体。如果你从外部观察建造者的工作，完全不了解它的内部运作原理，你会觉得它懂得如何建造高塔。但如果你能从内部观察建造者，你肯定不会发现它拥有任何知识。你看到的不过是几个开关，以各种方式排列，彼此控制着开关。\n","permalink":"https://rsc-blog.pages.dev/posts/2026/society-of-mind/","summary":"Society of Mind","title":"Society of Mind"},{"content":"CLAUDE.md ## 开发环境 Windows ## 开发方式 - 使用git管理目标项目 - 在适当的时候使用subagent、multiagent workflow、dynamic workflow ## 开发风格 - 极简 - 务实 - 专业 ## 开发规范 - 以瞎猜接口为耻，以认真查询为荣。 - 以模糊执行为耻，以寻求确认为荣。 - 以臆想业务为耻，以人类确认为荣。 - 以创造接口为耻，以复用现有为荣。 - 以跳过验证为耻，以主动测试为荣。 - 以破坏架构为耻，以遵循规范为荣。 - 以假装理解为耻，以诚实无知为荣。 - 以盲目修改为耻，以谨慎重构为荣。 settings.json { \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;ak_2g6.......qw4V\u0026#34;, \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://api.longcat.chat/anthropic\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_HAIKU_MODEL\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_OPUS_MODEL\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_OPUS_MODEL_NAME\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_SONNET_MODEL\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_SONNET_MODEL_NAME\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;LongCat-2.0\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\u0026#34;: \u0026#34;1\u0026#34; }, \u0026#34;permissions\u0026#34;: { \u0026#34;defaultMode\u0026#34;: \u0026#34;bypassPermissions\u0026#34; }, \u0026#34;autoUpdatesChannel\u0026#34;: \u0026#34;latest\u0026#34;, \u0026#34;theme\u0026#34;: \u0026#34;dark\u0026#34;, \u0026#34;skipDangerousModePermissionPrompt\u0026#34;: true } ","permalink":"https://rsc-blog.pages.dev/posts/2026/my-claude-code/","summary":"我的个人claude code设置","title":"My Claude Code"},{"content":"侧颈温和牵伸 练习者站直或坐直，双肩下沉、目视前方，先水平向后收下颌保持 5 秒； 随后一手放后腰固定肩膀，另一手轻搭头侧上方，带动头部向对侧轻倾斜保持 10 秒，缓慢复位后左右交替，每侧做 3 次； 注意勿用力掰或扭转颈部。 靠墙开肩动作 练习者背靠墙站立，后脑勺、背部、臀部及屈肘 90 度的双臂贴近墙面，缓慢上举至轻微酸胀处停留 3 秒，重复 8 次，早晚各完成 1 组； 练习时动作需轻柔，以不疼痛为度，出现刺痛、手麻、头晕症状需立即停止。 前屈 后伸 侧屈 侧旋 以下动作都是停留20s然后回到中立位20s\n前屈下颌尽量接触胸骨柄20s，复位20s 后伸鼻唇沟和地面平行20s，复位20s 侧屈耳朵尽量到肩20s，复位20s；然后换另一侧 侧旋眼睛可以看到肩头20s，复位20s；然后换另一侧 ","permalink":"https://rsc-blog.pages.dev/posts/2026/debugging-my-body/","summary":"放松肩颈腰","title":"Debugging My Body 复活肩颈"},{"content":"为什么选这套方案 Hugo：由 Go 编写，构建速度极快。Hexo 依赖 Node.js，Jekyll 依赖 Ruby，版本一换就瘫痪；Hugo 是单一二进制文件，没有这个问题。 GitHub：内容以 Markdown 存储，天然版本控制。 Cloudflare Pages：免费，全球 CDN，自动监听 GitHub 推送并构建，国内可访问。 整个方案运营成本接近于零。\n用 npm 管理 Hugo 版本 Hugo 是独立二进制文件，通常需要手动安装。为了可移植性，使用 hugo-bin 这个 npm 包代替：\nnpm install hugo-bin --save-dev npm install 时会自动下载当前系统对应的 Hugo Extended 二进制文件到 node_modules，Hugo 版本锁定在 package.json，换电脑也不需要重新配置环境：\n{ \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;hugo server\u0026#34;, \u0026#34;build\u0026#34;: \u0026#34;hugo --gc --minify\u0026#34;, \u0026#34;new\u0026#34;: \u0026#34;hugo new\u0026#34; }, \u0026#34;hugo-bin\u0026#34;: { \u0026#34;buildTags\u0026#34;: \u0026#34;extended\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;0.147.0\u0026#34; } } 日常命令：\n命令 作用 npm run dev 启动本地预览（http://localhost:1313，热重载） npm run new -- posts/title.md 新建文章，自动套用模板 npm run build 构建生产环境静态文件到 public/ 主题：PaperMod via Git Submodule 去官网 https://themes.gohugo.io/ 找自己喜欢的主题。\nPaperMod 以 Git Submodule 方式引入，不直接复制主题文件。好处是主题有独立版本历史，升级时只需更新 submodule：\ngit submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod 克隆仓库时需加 --recursive 参数一并拉取主题：\ngit clone --recursive \u0026lt;仓库地址\u0026gt; 关键配置：hugo.toml baseURL baseURL = \u0026#39;https://your-domain.pages.dev/\u0026#39; 决定所有内部链接的前缀。留着默认的 example.org 不改，是最常见的坑——站点能访问，但导航栏所有链接都跳到错误域名。\n搜索：必须开启 JSON 输出 PaperMod 的站内搜索是纯前端实现，依赖构建时生成的 index.json：\n[outputs] home = [\u0026#34;HTML\u0026#34;, \u0026#34;RSS\u0026#34;, \u0026#34;JSON\u0026#34;] 少了 \u0026quot;JSON\u0026quot;，搜索框出现但无结果。\n首页：ProfileMode [params.profileMode] enabled = true title = \u0026#34;Hi, I\u0026#39;m xxx 👋\u0026#34; subtitle = \u0026#34;Writing about tech, tools, and the occasional life update.\u0026#34; enabled = false 则首页直接变为文章列表。\n导航菜单 菜单顺序由 weight 控制，数值越小越靠前：\n[[menu.main]] identifier = \u0026#34;posts\u0026#34; name = \u0026#34;Posts\u0026#34; url = \u0026#34;/posts/\u0026#34; weight = 10 [[menu.main]] identifier = \u0026#34;categories\u0026#34; name = \u0026#34;Categories\u0026#34; url = \u0026#34;/categories/\u0026#34; weight = 20 [[menu.main]] identifier = \u0026#34;tags\u0026#34; name = \u0026#34;Tags\u0026#34; url = \u0026#34;/tags/\u0026#34; weight = 30 [[menu.main]] identifier = \u0026#34;search\u0026#34; name = \u0026#34;Search\u0026#34; url = \u0026#34;/search/\u0026#34; weight = 40 部署：Cloudflare Pages 登录 Cloudflare 控制台 → Workers \u0026amp; Pages → Create application → Pages → Connect to Git 选择博客仓库，Build settings 配置： Build command：hugo --gc --minify Build output directory：public Environment variables 中添加：HUGO_VERSION = 0.147.0 Save and Deploy 必须设置 HUGO_VERSION，否则 Cloudflare 使用极旧的默认版本，PaperMod 样式会编译失败。\n部署完成后获得免费的 *.pages.dev 域名，也可在后台 Custom domains 绑定自己的域名。\n主题定制：侧边 TOC PaperMod 默认 TOC 是文章顶部的可折叠块。通过 Hugo 布局覆盖机制，不修改主题文件，将 TOC 改为页面侧面的固定侧边栏。\n新增两个文件：\nlayouts/single.html：复制自主题，将 TOC 移出 \u0026lt;article\u0026gt;，放到独立 \u0026lt;aside\u0026gt;，使用 Hugo 原生 .TableOfContents assets/css/extended/toc-sidebar.css：用 position: fixed 定位在正文侧面空白处 .toc-sidebar { position: fixed; top: calc(60px + 2.5rem); left: calc(50% + 360px + 40px); /* 正文右边缘 + 间距 */ width: 200px; } 视口宽度 ≥ 1200px 时显示，更窄时自动隐藏。主题升级时对比 themes/PaperMod/layouts/single.html 与自定义版本差异并手动同步。\n渲染 Mermaid 图表 PaperMod 和 Hugo 都没有内置 Mermaid 支持，通过自定义短代码实现，两步完成：\n1. 新建短代码 layouts/shortcodes/mermaid.html：\n\u0026lt;pre class=\u0026#34;mermaid\u0026#34;\u0026gt;{{ .Inner }}\u0026lt;/pre\u0026gt; 2. 在文章页引入 Mermaid CDN，在 layouts/single.html 末尾加入：\n\u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; mermaid.initialize({ startOnLoad: true, theme: \u0026#34;default\u0026#34; }); \u0026lt;/script\u0026gt; 用 {{}} 分别包裹\u0026lt; mermaid \u0026gt; 和 \u0026lt; /mermaid \u0026gt; 即可渲染，此处没有包裹是因为会强制渲染\n\u0026lt; mermaid \u0026gt; graph LR A[开始] --\u0026gt; B{判断} B --\u0026gt;|是| C[结束] B --\u0026gt;|否| A \u0026lt; /mermaid \u0026gt; graph LR A[开始] --\u003e B{判断} B --\u003e|是| C[结束] B --\u003e|否| A 文件目录结构 blog/ ├── content/ │ ├── posts/ # 博客文章（Markdown） │ │ └── 2026/ # 文章多时按年份建子目录（URL 自动含路径） │ └── about.md # 独立页面（如\u0026#34;关于\u0026#34;） ├── static/ │ └── images/ # 图片等静态资源 │ # Markdown 中用绝对路径引用：/images/xxx.png ├── archetypes/ │ └── default.md # 新文章默认模板（含完整 Front Matter 字段） ├── assets/ │ └── css/extended/ # 自定义 CSS（自动加载，覆盖主题样式） │ └── toc-sidebar.css ├── layouts/ │ ├── single.html # 覆盖主题文章页模板（不修改主题文件本身） │ └── shortcodes/ │ └── mermaid.html # Mermaid 图表短代码 ├── themes/ │ └── PaperMod/ # 主题（Git Submodule，不直接修改） └── hugo.toml # 全局配置 分类 (Categories) 与标签 (Tags) 两者都是文章的筛选维度，区别在粒度：\nCategories Tags 定位 文章所属领域（大方向） 文章涉及的具体知识点 粒度 粗，数量少且稳定 细，随内容自由增长 建议数量/篇 1~2 个 3~5 个 在 Front Matter 中定义：\ncategories = [\u0026#34;后端开发\u0026#34;] tags = [\u0026#34;Docker\u0026#34;, \u0026#34;微服务\u0026#34;, \u0026#34;部署\u0026#34;] Hugo 会自动生成 /categories/ 和 /tags/ 聚合页，无需手动创建。\n写作工作流 flowchart LR A[新建文章] --\u003e B[本地预览] B --\u003e C[写作] C --\u003e D[发布] D --\u003e E[推送] E --\u003e F[Cloudflare 自动部署] npm run new -- posts/your-title.md npm run dev，打开 http://localhost:1313 实时预览 写完后将 draft = true 改为 draft = false git add . \u0026amp;\u0026amp; git commit -m \u0026quot;post: 文章标题\u0026quot; \u0026amp;\u0026amp; git push Cloudflare 自动构建，约 1 分钟后线上更新 跨设备 Clone 后初始化 博客仓库在另一台设备上 clone 后，需要两步初始化：\n# 1. 安装 Hugo（hugo-bin） npm install # 2. 拉取 PaperMod 主题（Git Submodule） git submodule update --init --recursive 如果 clone 时用了 git clone --recursive，第 2 步可跳过。但 npm install 仍然必须执行。\n","permalink":"https://rsc-blog.pages.dev/posts/2026/build-blog-website/","summary":"用 Hugo + PaperMod + GitHub + Cloudflare Pages 搭建零成本个人博客的完整记录，含目录管理、分类标签设计与日常写作工作流。","title":"Hugo + GitHub + Cloudflare Pages 搭建博客"}]