从0实现OnCall基于Python语言框架
Step01
第一步做的事情,先把 Python 版 OnCall 的后端外壳搭起来。
从工程角度看,完成的是一个最小可运行骨架。创建了基础目录,明确了入口文件、路由文件、请求响应结构和 SSE 工具函数这些最核心的位置。
在服务层面,把 FastAPI 服务真正启动起来,并且有了 main.py 作为统一入口。这说明 Python 版 OnCall 已经不再停留在方案阶段,而是有了一个可以启动、可以监听端口、可以对外提供接口的实际运行体。
在接口层面,固定了两个最重要的入口,一个是普通的 /api/chat,一个是流式输出的 /api/chat_stream。
在功能层面,虽然现在还没有接入真实模型,但已经让 /api/chat 能正常接收请求并返回一段 mock 内容了。这说明请求解析、参数校验、路由分发和 JSON 响应这一整条基础链路是通的。
更重要的是, /api/chat_stream 做通了,也就是把 SSE 流式输出先跑起来了。
所以如果把第一步浓缩成一句话来说,那就是:已经从零搭出了 Python 版 OnCall 的最小后端运行底座,并验证了普通 HTTP 和 SSE 两条最基础的通信链路都能正常工作。
Step02
第二步做的事情,核心不是新增更多接口,而是把第一步那个能跑的 demo 外壳,进一步整理成一个有分层的后端结构。
第二步把原来直接写在路由里的 mock 聊天逻辑抽离出来,放进一个单独的 ChatService 里。这样一来,路由层就不再负责具体怎么生成回复,而只负责接收请求、调用服务、返回结果。后面你要接真实模型、接 RAG、接工具调用,其实都应该发生在 service 层,而不是堆在 API 路由函数里。
除了 ChatService,第二步还引入了一个最简单的 SessionStore,也就是会话存储。它的作用是先把同一个 session_id 下的历史消息保存起来,让系统开始具备最基础的多轮对话记忆”能力。目前这个版本只是内存存储,进程一重启就会丢失。
从项目结构上看,第二步的意义在于开始引入真正的分层。原来项目里只有入口、路由、schema 和 SSE 工具,逻辑基本都贴在接口附近;到了第二步,项目开始出现 services/ 和 infra/memory/ 这样的目录。
第二步还带来了一个很重要的效果,就是开始从“接口 mock”过渡到“业务 mock”。第一步的 mock 更像是在证明接口通不通,而第二步的 mock 已经开始模拟真实业务行为了。因为现在回复内容不只是简单返回一段固定文本,而是可以结合当前会话历史数量来构造结果,这说明系统内部已经开始有状态、有上下文,而不是纯静态输出。
第二步其实是在给第三步做准备。因为一旦 ChatService 已经存在,下一步你要做的事情就非常清晰了:把 ChatService 里原本的 mock 拼字符串逻辑,替换成真实的 LLM 调用;再往后,可以在这个 service 里继续插入 RAG 检索、工具调用、提示词构建等能力。
所以如果把第二步浓缩成一句话,它做的事情就是:把第一步跑通的最小聊天接口,进一步整理成“路由层 + 服务层 + 会话存储层”的结构,让 Python 版 OnCall 开始具备最基础的业务分层和多轮会话能力。
Step03
第三步做的事情,核心就是把前面那个“结构已经搭好、但回复还是假的聊天系统”,正式升级成了一个会真实调用大模型的聊天系统。那么第三步就是第一次把“智能能力内核”真正接进来,让OnCall开始根据用户输入,向真实模型发请求并拿回结果。
新增了一个独立的模型适配层。也就是说,模型调用不再直接散落在路由或 service 代码里,而是被封装进了 infra/llm/openai_compatible.py ,专门负责和 OpenAI 兼容接口通信。
补上了配置层,也就是 config.py 和 .env 这部分。这样模型的 base_url、api_key、model 名称这些信息,就不再写死在代码里,而是统一从配置读取。
改造 ChatService。先读取当前 session_id 对应的历史消息,再加上 system prompt,把这些内容组织成模型需要的 messages 格式,然后调用 LLM 适配器获取真实回答,最后再把用户消息和模型回复一起写回会话历史。
第三步还把流式接口也从假流升级成了真流。持续接收模型返回的增量内容,再通过 start、token、done 这些事件发给前端。
从整体结果上看,第三步完成后,系统内部第一次形成了一个真实的 AI 闭环:用户请求进入路由——路由调用 ChatService——ChatService 组织消息并调用模型——模型返回真实内容——再把结果写进 session history——最后由接口返回给用户。
所以如果把第三步浓缩成一句话,它做的事情就是:把 Python 版 OnCall 从一个“有结构的 mock 聊天后端”,升级成了一个“有结构、可配置、支持上下文、支持流式输出的真实大模型聊天后端”。这一阶段做完以后,后面的第四步就顺理成章了,因为你已经有了模型调用能力,接下来只需要在模型调用之前插入检索逻辑,就可以把系统继续升级成带 RAG 的 OnCall。
Step04
第四步做的事情,核心就是把第三步那个“已经能真实调用大模型聊天”的系统,进一步升级成了一个带知识库检索能力的 RAG 系统。如果说第三步解决的是“模型能不能真正回答”,那么第四步解决的就是“模型能不能在回答之前,先去查本地知识,再基于知识回答”。这一步做完之后,Python 版 OnCall 就不再只是一个通用聊天后端,而是开始具备“面向特定知识库做增强问答”的能力了。
这一阶段最重要的新增能力,是文件上传和知识入库链路。也就是说,系统不再只接收用户的聊天请求,还新增了一个上传入口,用来接收本地文档。上传之后,文件不会只是简单保存下来,而是会继续经过一条完整的数据处理流程:先读取文件内容,再对文本做切分,然后调用 embedding 模型把每个文本片段转成向量,最后把这些片段连同向量一起写入 Milvus。这个过程的意义在于,它把原本“人类可读的文档”,转成了“系统可以检索的知识单元”。
为了让这条链路成立,第四步还引入了 embedding 层和向量存储层。embedding 层负责把文本变成向量,也就是把自然语言内容转换成模型可以用于相似度检索的数值表示。向量存储层则负责把这些向量和对应的文本片段保存起来,并在后续查询时,根据用户问题去做相似度搜索。你这里用的是 Milvus,本地开发阶段走的是 Milvus Lite,所以这一步实际上也意味着:你的项目已经从“只有大模型接口”扩展成了“模型 + 向量库”的双基础设施架构。
第四步另一个关键变化,是在聊天链路里插入了检索逻辑。也就是说,ChatService 不再只是把历史消息和当前用户问题直接送给大模型,而是多了一步:当 use_rag=true 时,先用用户问题生成 query embedding,再到 Milvus 里查找最相关的知识片段,把这些片段组织成上下文信息,然后再一起送给模型。这样一来,模型的回答就不再只依赖自身训练时学到的通用知识,而是会优先基于你本地上传的知识内容来回答。
从行为上看,这一步带来的变化非常明显。开启 RAG 时,模型回答会明显引用知识库中的具体信息,并且返回结果里会带上 references 和 trace,表示它命中了哪些知识片段、检索到了多少条结果。关闭 RAG 时,模型就退回到自身的通用能力,只能给出更泛化、更经验性的回答。你实际做的对照测试已经很好地证明了这一点:在开启 RAG 时,回答会直接贴着你上传的 runbook 内容走;关闭 RAG 时,回答就变成了一个泛化的 5xx 排障思路。这说明第四步并不是“看起来像加了知识库”,而是知识检索真的已经在回答流程里生效了。
从工程结构上说,第四步不只是“多加了几个功能”,而是让整个项目开始出现了更完整的 AI 后端形态。原来系统里只有 API、service、session、LLM 适配器;到了第四步,项目里又增加了 loader、splitter、embedding adapter、Milvus store、RAG service、ingestion service 这些模块。也就是说,Python 版 OnCall 已经开始从一个普通聊天服务,演进成一个真正意义上的“知识增强智能体后端”。
如果把第四步浓缩成一句话,它做的事情就是:为 Python 版 OnCall 建立了一条从文件上传、知识入库到检索增强回答的完整 RAG 闭环。
这一步完成之后,你已经具备了两种能力:一种是纯模型问答,另一种是基于本地知识库的增强问答。也正因为这一步已经完成,接下来的第五步才会很自然地变成“接入工具调用”,因为现在系统已经有了聊天能力,也有了知识能力,下一步就该给它加上“主动调用外部工具”的执行能力了。
Step05
第五步做的事情,核心就是在前面已经具备“普通聊天”和“RAG 检索增强问答”能力的基础上,再往系统里加入了一层工具调用能力。如果说前四步的系统本质上还是“用户问,模型答”,那么第五步之后,系统开始具备一种新的行为模式:模型不再只是单纯根据上下文生成答案,而是可以在判断“需要外部实时信息”时,先请求调用某个工具,等工具返回结果后,再基于这个结果组织最终回答。也就是说,系统第一次拥有了“借助外部能力完成回答”的执行型能力。
这一阶段最重要的新增内容,是你建立了一个最小的工具系统。它并不是一开始就接入日志、告警、数据库这些复杂工具,而是先用 get_current_time 这个非常简单的工具,打通了整条工具调用闭环。这样做的重点并不在于“获取时间”这个功能本身,而在于验证整个协议和流程是否成立:模型能不能识别自己需要一个工具,后端能不能执行这个工具,工具执行结果能不能被重新送回模型,以及模型能不能基于工具结果生成自然语言答案。也就是说,第五步的真正价值在于“让系统第一次学会工具调用”,而不是某一个具体工具有多复杂。
为了让这件事成立,第五步还新增了工具注册中心,也就是 ToolRegistry。这一步的意义在于,你不再是零散地写几个 Python 函数,而是开始把工具当成一种正式的系统能力来管理。注册中心一方面负责把工具的名字、描述和参数 schema 转换成模型能读懂的格式,另一方面负责在模型返回工具调用请求后,根据工具名找到对应的工具实现并执行。这样一来,系统里第一次出现了一种“工具协议层”,后面继续扩展日志工具、告警工具、内部文档工具时,都会沿用这个统一框架,而不是各写各的。
与此同时,第五步也真正改造了模型调用层。原来第三步和第四步中的 LLM 适配器,本质上只需要处理“发消息给模型,再收回文本答案”这类单轮行为;到了第五步,LLM 适配器开始支持 tools 和 tool_calls 这些结构化字段。也就是说,模型返回的结果已经不再只是“纯文本”,而可能是一种“我要调用某个工具”的结构化请求。这代表你的系统开始从“文本交互型”演进到“文本 + 结构化动作请求”混合型交互,这一步其实非常关键,因为它是从普通聊天系统走向 Agent 系统的第一个真实转折点。
在业务层面,第五步最核心的变化发生在 ChatService。前面第四步中,ChatService 的职责主要是:拼好历史消息和 RAG 上下文,然后把它们送给模型,再拿回答案。而到了第五步,ChatService 里新增了一层最小的 tool loop,也就是一个小型循环机制:先发起第一轮模型请求;如果模型直接给出最终答案,就结束;如果模型返回 tool_calls,那就进入工具执行逻辑,执行完成后把工具结果作为新的消息继续发给模型,再让模型基于工具结果生成最终自然语言回答。这个循环虽然现在还比较简单,但它已经让系统开始具备“思考是否要借助外部工具”的能力,这本质上已经是一个最小 Agent 行为了。
第五步还对流式接口做了升级。虽然这一版还没有做到“工具调用过程与模型 token 真正完全混合式的实时流”,但它已经能把工具相关的过程通过 SSE 事件流发给前端,比如 tool_call、tool_result,然后再继续输出最终答案的 token。这意味着你的前端以后不只是能显示“模型在回答什么”,还可以显示“模型刚刚调用了哪个工具、拿到了什么结果”。从 OnCall 场景来说,这一点非常重要,因为在一个运维、排障、分析类系统中,用户往往不仅关心最终答案,还关心这个答案是怎么来的,调用了哪些外部能力,这样系统的行为才更可解释、更可信。
从整体效果上说,第五步完成后,系统已经不再只是一个“会聊天、会查知识库”的后端了,而是开始具备最基础的“执行能力”。前四步解决的更多是知识和表达问题:系统能不能基于上下文和知识回答用户;而第五步开始解决的是动作问题:系统能不能在需要外部实时事实时,不靠猜,而是主动请求工具来获取信息。这就是为什么第五步虽然只先接了一个简单的时间工具,但它在系统演进上的意义其实非常大,因为它第一次让 Python 版 OnCall 从“增强问答系统”走向了“最小可执行智能体系统”。
如果把第五步浓缩成一句话,它做的事情就是:在现有聊天与 RAG 系统之上,建立了一个最小可用的工具调用闭环,让模型能够根据需要触发工具、获取外部信息,并基于工具结果生成最终回答。
也正因为这一步已经完成,后面的第六步就会非常自然:你不需要再重新设计工具框架,而只需要沿着这一套机制,继续增加更符合 OnCall 场景的工具,比如日志查询、告警查询、内部文档查询、数据库只读查询等。换句话说,第五步真正搭好的,不只是一个时间工具,而是整个 OnCall 后续工具体系的底座。
Step06
第 6 步做的事情,核心就是把第 5 步那个“只有一个时间工具的最小工具调用系统”,扩展成了一个更贴近 OnCall 场景的多工具系统。如果说第 5 步是在验证“模型会不会调用工具”,那么第 6 步解决的就是“模型能不能在不同的运维排障场景里,选择合适的工具来辅助分析问题”。这一步做完以后,系统已经不只是会聊天、会查知识库、会查时间,而是开始具备了面向真实 OnCall 问题调用多种外部能力的基础。
这一阶段最重要的变化,是新增了三个真正有业务意义的工具:内部知识库查询工具、告警查询工具和日志查询工具。内部知识库工具的作用,是让模型在用户明确要求“查知识库”“查 runbook”“查内部文档”时,主动调用现有的知识检索能力,而不是仅仅依赖主链路里的 use_rag=true 开关。告警工具的作用,是让模型能够针对“当前有哪些 firing 告警”“gateway 最近的 critical 告警有哪些”这类问题,主动获取结构化告警数据。日志工具则是让模型可以针对“最近有没有 timeout”“有没有 5xx 相关 ERROR 日志”这类问题,主动查询日志并基于日志做进一步总结。也就是说,第 6 步让工具系统开始从演示性质,走向真正的运维场景应用。
为了支撑这些工具,第 6 步并没有推翻第 5 步已经做好的工具调用框架,而是在原有基础上把工具注册中心扩展成一个更完整的多工具管理层。工具注册中心现在不再只是登记一个 get_current_time,而是统一管理多个工具,并且为每个工具提供名字、描述、参数 schema 和统一的返回结构。这样一来,系统已经开始形成一个比较稳定的工具协议:模型根据描述选择工具,后端按统一规则执行工具,工具再按统一格式返回结果,最后模型基于这个结果生成自然语言回答。这说明你的系统已经不再是“临时加几个函数”,而是在形成一个真正可以持续扩展的工具体系。
在业务流程上,第 6 步本质上还是沿用了第 5 步的 tool loop,只不过现在这个 loop 开始服务于更复杂的场景了。也就是说,ChatService 的核心逻辑没有推翻重写,而是在原有“模型先决定是否调用工具”的基础上,继续支持更多工具种类,并适当放宽了工具循环轮次限制,以适应一个回答里可能涉及多次工具请求的情况。这个变化的意义在于,你已经不再处在“验证框架能不能跑”的阶段,而是进入了“框架已成立,开始往里面填真正的业务能力”的阶段。
从系统行为上看,第 6 步带来了一个很明显的升级。以前系统主要有三种能力:普通聊天、大模型问答、RAG 检索增强回答;而现在又多了一层“面向 OnCall 的工具型分析能力”。这意味着,用户问问题时,系统不只是被动地根据知识生成答案,而是会开始主动判断:这个问题更适合查知识库,还是查告警,还是查日志。比如在“5xx 告警应该先检查什么”这种问题上,它会倾向于查内部知识库;在“gateway 现在有哪些 firing 告警”这种问题上,它会选择查告警;在“最近有没有 timeout ERROR 日志”这种问题上,它会选择查日志。这种“按问题类型动态选择工具”的行为,就是第 6 步最本质的成果。
这一阶段的另一个重要变化,是系统的可解释性进一步增强了。因为每一次工具调用都会在 trace 里留下记录,包括模型选择了哪个工具、传了什么参数、工具返回了什么结果。流式接口也继续沿用了第 5 步的事件机制,可以把 tool_call、tool_result 和最终的 token 输出逐步发给前端。这使得系统不只是“给答案”,而且能展示答案是如何来的。对于 OnCall 场景来说,这一点非常重要,因为用户往往不只关心结论,也关心系统到底查了哪些信息、依据了哪些数据做判断。
从工程角度来说,第 6 步最大的价值,不在于某一个具体工具,而在于你已经搭好了一个可以持续扩展的 OnCall 工具体系底座。虽然目前 alerts 和 logs 还是 mock 数据,但这一步已经证明:多工具注册、模型选工具、工具执行、结果回填、trace 记录、SSE 输出,这整套机制都已经成立。也就是说,后面你要做的,已经不再是“怎么设计工具调用框架”,而是“把 mock 工具替换成真实数据源”。这会让后续开发轻松很多。
如果把第 6 步浓缩成一句话,它做的事情就是:在现有聊天、RAG 和单工具调用能力之上,建立了一个面向 OnCall 场景的多工具系统,让模型能够根据问题类型主动调用知识库、告警和日志工具来辅助分析。
也正因为这一步已经完成,下一步才会很自然地进入两个方向中的一个。一个方向是把 alerts 和 logs 从 mock 替换成真实后端数据源;另一个方向是进一步做多步工具协同,也就是更接近 ReAct 或 Plan-Execute-Replan 的执行模式。换句话说,第 6 步真正完成的,不只是 3 个工具,而是整个 OnCall 智能体执行能力的第一层业务化落地。
Step07
第 7 步做的事情,核心就是把第 6 步中仍然停留在 mock 阶段的工具系统,开始真正接入真实业务后端。如果说第 6 步解决的是“模型能不能在 OnCall 场景里选择合适的工具”,那么第 7 步解决的就是“这些工具拿到的数据是不是真的来自外部系统,而不是写死在代码里的假数据”。这一点非常关键,因为它标志着系统开始从“可演示的智能助手”迈向“具备真实业务接入能力的智能助手”。
这一阶段最重要的工作,是把 query_alerts 从第 6 步中的 mock 告警数据,替换成了一个真实的 HTTP 后端适配器。也就是说,告警工具不再直接读代码里写好的几条静态告警,而是通过新增的 alerts_client 去访问真实的告警接口,再把接口返回的数据做规范化处理,然后交还给工具层。这个改动的意义非常大,因为它让“工具”这层和“真实后端接口”这层被正式分开了。以后无论你接的是 Prometheus、Alertmanager、Grafana,还是公司内部告警平台,你都不需要推翻工具框架,只需要替换或扩展底层的 backend 适配器。
在实现层面,第 7 步并没有推翻第 5、6 步已经搭好的工具调用架构,而是沿用了原有的 ToolRegistry + ChatService tool loop 机制,只把 query_alerts 的底层数据来源换成了真实 Prometheus 接口。这说明前面几步搭建的架构已经经得住扩展,不需要每次引入真实系统时都重写一遍业务逻辑。换句话说,第 7 步验证的不只是“能不能查真实告警”,也间接证明了你前面搭好的工具分层和执行流程是合理的。
为了完成这一步,你还补上了本地 Prometheus 环境。也就是说,第 7 步不只是写代码层面的工作,还包括真实运行环境的搭建和验证。你本地起了一个最小 Prometheus 实例,配置了最基本的规则文件,并用一条始终成立的测试告警规则,确保 /api/v1/alerts 这个真实接口能返回 firing 告警。这样做的价值在于,你不是“假设自己接上了真实告警系统”,而是真正在本机造出了一个可访问、可验证、可用于联调的真实告警源。这一步把“真实后端接入”从概念变成了一个可以重复验证的本地开发能力。
从系统行为上看,第 7 步完成后,query_alerts 已经不再是一个“模型以为自己在查告警,其实只是代码里拿几条假数据”的工具,而是真的能调用 Prometheus /api/v1/alerts,把返回的告警信息结构化处理后交给模型。模型再根据这些真实告警信息生成自然语言总结。这一点从你后面的测试结果已经很清楚了:模型不仅成功触发了 query_alerts,还从真实返回中读到了 PrometheusAlwaysFiring 这条告警,并给出了带有状态、级别、描述和时间信息的总结。这说明第 7 步的核心目标已经达成——工具系统开始消费真实系统数据,而不是内部 mock 数据。
这一阶段还有一个非常重要的意义,就是它验证了工具系统在“真实接口条件下”依然能稳定工作。第 6 步里,工具调用闭环虽然已经跑通,但那是在 mock 数据条件下完成的,风险比较低;到了第 7 步,工具开始真正面临 HTTP 超时、接口结构不一致、字段归一化、环境依赖这些现实问题。而你通过 alerts_client 的封装,把这些外部复杂性都隔离在工具层下面,没有把它们污染到 ChatService 或上层路由。这说明你的项目架构已经具备了承载真实外部系统接入的能力,而不仅仅是写几个 demo 用的工具。
从项目整体演进上看,第 7 步是一个很关键的转折点。前面第 4 步和第 5 步更多是在打能力基础:RAG、工具调用协议、SSE 事件流;第 6 步是在这个基础上做出多工具系统;而第 7 步第一次让系统真正连接到了外部真实服务。也就是说,从这一步开始,OnCall 不再只是“模拟一个会查知识和查告警的助手”,而是在逐步成为一个“能接入真实监控系统并进行辅助分析的助手”。
如果把第 7 步浓缩成一句话,它做的事情就是:在第 6 步多工具系统的基础上,把 query_alerts 从 mock 数据升级为真实 Prometheus 告警接口接入,并完成了本地真实告警环境的搭建与验证。
也正因为第 7 步已经完成,后面的下一步就变得很自然了。既然真实告警接口已经接通,那么接下来最合理的方向就是继续把 query_logs 也从 mock 替换成真实日志后端。到那时,你的系统就会同时拥有真实知识库、真实告警和真实日志三个核心 OnCall 能力,整体完成度会再上一个台阶。
Step08
基于MCP的日志接入
原始方案是,Python 里的 query_alerts、query_logs 各自对应一个backend client,然后直接去请求某个HTTP API,比如 /alerts、/logs。这种方式的特点是简单、透明、好调试。一眼就能看清哪个工具请求哪个地址、传什么参数、返回什么 JSON、在哪里做字段归一化。对从零重写、先把链路跑通这个阶段来说,这是最稳的路线。它的代价是耦合比较强:Prometheus、Loki、Elastic、内部日志平台,各自接口格式都不一样,要一个个写适配器。
基于MCP的实现是先读取 cls_mcp_url,再用NewSSEMCPClient建一个 MCP 客户端,随后执行 Initialize,再通过 GetTools 从远端 MCP Server 动态拿到一组工具。也就是说,这段代码不是“应用自己查日志”,而是“应用去连一个 MCP 服务,MCP 服务再把日志能力暴露成工具”。
一个是直连具体后端API,一个是先接统一协议层,再有协议层提供工具。
理解MCP:以前接告警、接日志、接数据库,每种能力都要自己写一套工具描述 + 参数 schema + HTTP 调用 + 结果转换;现在如果后端已经有一个 MCP Server,这些工具可以由 MCP Server 统一暴露出来,客户端只需要连接、发现工具、调用工具。
第 8 步做的核心事情,是把原来第 6 步里还停留在 mock 阶段的 query_logs 工具,正式升级成了基于 MCP + 腾讯云 CLS 的真实日志查询能力。如果说第 7 步解决的是“真实告警怎么接进来”,那么第 8 步解决的就是“真实日志怎么接进来”。这一步完成之后,OnCall 就不再只是能查知识库、查告警,还真正具备了查线上日志的能力。
这一阶段一开始并不是继续走我最早设计的 logs_client.py -> /logs 这种普通 HTTP API 路线,而是根据你贴出来的原 Go 项目代码,改成了更贴近原项目结构的 MCP 接入方案。因为 Go 版日志能力本来就不是直接写一个 /logs 接口去查,而是通过 cls_mcp_url 去连接腾讯云 CLS 的 MCP 服务,再从 MCP server 暴露出来的工具里拿日志查询能力。所以第 8 步本质上不是“补一个日志接口”,而是把 Python 项目的日志工具层改造成了一个 MCP client。
为了完成这一点,第 8 步里首先做的是梳理并确认腾讯云 CLS MCP server 暴露出来的真实工具集合。你通过 Go 侧和 Python 侧分别确认了工具名,包括 SearchLog、TextToSearchLogQuery、GetRegionCodeByName、GetTopicInfoByName 等。这一步非常关键,因为它让后面的 Python 代码不再是凭猜测写参数,而是按真实工具名、真实 schema 去接。也就是说,第 8 步不是盲目接 MCP,而是先把 MCP server 的工具生态摸清楚,再往 Python 里落。
接着,第 8 步在工程层面新增了一套 MCP 日志后端。你在 Python 项目里新增了 app/infra/mcp/cls_mcp_client.py,把日志查询从“普通 HTTP backend”改成了“通过 SSE 连接 MCP server,再调用工具”的模式。同时,app/tools/logs.py 也被改成依赖这个 MCP backend,而不是再从 mock 数据里读日志。换句话说,从这一刻开始,query_logs 这个工具虽然名字没变,但底层实现已经彻底换成了真实的 CLS 日志查询。
为了让这套 MCP 日志链路真正能跑起来,第 8 步里还完成了本地自托管 cls-mcp-server 的搭建。中间经历了几轮排错:先是腾讯云托管 SSE 地址缺少有效密钥,后来又改成本地启动 cls-mcp-server;接着又遇到 Node 版本过低,导致 cls-mcp-server@latest 无法启动,最后通过升级 Node 环境解决。也就是说,第 8 步不仅仅是代码改造,还包含了一次真实外部服务运行环境的搭建。你最终让本地机器可以正常跑起腾讯云 CLS 的 MCP server,并让 Python 项目连到本地 http://127.0.0.1:3000/sse 这个自托管地址。
在和腾讯云 CLS 联调的过程中,第 8 步还完成了一系列日志查询链路的拆解和校准。一开始的失败不是来自 Python 主工程,而是来自细节:有时候是地域码不对,有时候是 TopicId 必填但没传,有时候是 GetTopicInfoByName 的参数名大小写不对,有时候是腾讯云 CLS 服务还没开通,或者 Topic、TopicId 还没准备好。通过这些排查,第 8 步最后把一条完整的链路跑顺了:先根据“北京”拿到 ap-beijing,再根据 Topic 名 gateway 拿到真实 TopicId,再调用 SearchLog 去查日志。也就是说,这一步实际上已经把 CLS 日志查询所需的上下文补全逻辑理顺了。
为了验证这条链路不是停留在“能连上工具”,第 8 步还专门做了真实日志写入和真实日志检索。你在 CLS 中创建了 gateway Topic,并通过 Python 脚本把几条测试日志真正写了进去,字段里包括 level、service、service_name、message。这一步很重要,因为它把“日志工具能调用”进一步升级成了“日志工具能查到自己刚写进去的真实日志”。而且你还对 Topic 的索引配置做了确认,确保日志不是仅仅写进去,而是可检索的。
在调通检索的过程中,第 8 步还顺手解决了一个很实际的问题:如何找到真正能命中的 CQL 查询语句。一开始完全依赖 TextToSearchLogQuery 生成的 Query,但它生成的查询语句和 CLS 当前 Topic 的索引能力并不总是完全匹配。于是你通过单独的调试脚本,一条一条试不同的 Query,包括 *、"timeout"、"gateway"、service_name:"gateway"、service:"gateway"、service_name:"gateway" AND "timeout" 等,最后确认这些写死的查询都能够命中真实日志。这一步的价值在于,它把日志查询从“理论上应该能查到”变成了“已经用实际查询验证过能命中”。
在此基础上,第 8 步最后做的是把验证通过的查询逻辑回收到主工程。也就是说,不再只是 scripts/test_cls_mcp_call.py 这种单独调试脚本能查到日志,而是把成功的 Region -> TopicId -> SearchLog 逻辑收回 app/infra/mcp/cls_mcp_client.py,并最终通过 /api/chat 的主链路验证成功。你最后那条 curl 返回里,模型成功调用了 query_logs,而 tool_result 返回的正是从 CLS 查回来的两条 timeout ERROR 日志,这说明第 8 步已经从“联调成功”走到了“主业务接口成功”。
所以如果把第 8 步整体概括一下,它做的不是单一一件事,而是完整完成了以下闭环:基于原 Go 项目的接入思路,把 Python 版的 query_logs 从 mock 改造成 MCP 日志工具;搭建并启动本地自托管 CLS MCP server;打通 Python 到 MCP,再到腾讯云 CLS 的真实日志查询链路;创建 Topic、写入测试日志、验证索引;最后在 /api/chat 主接口里成功查回真实日志。
如果用一句话来总结第 8 步,就是:
第 8 步完成了 Python OnCall 项目中真实日志能力的落地:将 query_logs 从 mock 升级为基于 MCP + 腾讯云 CLS 的真实日志查询,并完成了从本地自托管 MCP、CLS Topic/索引、日志写入到主链路 /api/chat 的端到端验证。
Step09
你说得对,这次我按纯分段文字来总结第 9 步,不再用分点结构。
第 9 步做的核心事情,是把系统从“能分别调用多个工具”推进到了“能把多个工具按固定顺序串起来,完成一次初步故障分析”。在前面的步骤里,项目已经具备了真实知识库、真实告警和真实日志这三类能力,但它们本质上还是分散的能力点:用户问什么,系统就调某一个工具去处理。第 9 步解决的问题,不是再接一个新工具,而是让系统第一次具备“把多种真实数据源组织成一条分析链”的能力。
这一阶段新增了一个专门用于故障初步分析的接口,也就是 /api/triage。这个接口不是普通聊天接口,而是一个面向 OnCall 场景的专用分析入口。它接收服务名、故障现象、是否查询知识库等参数,然后系统会自动按固定顺序执行分析动作。也就是说,第 9 步第一次把“问答”变成了“流程”,让系统不再只是回答一句话,而是能够围绕一个故障现象,自动去看告警、看日志、看知识库,再把这些结果组织起来。
为了支撑这个新接口,第 9 步新增了 app/schemas/triage.py、app/services/incident_analysis_service.py 和 app/api/routes/triage.py 这几个关键文件。其中最重要的是 IncidentAnalysisService。它不是单个工具的封装,而是一个多步分析服务,负责把多个工具串起来。这个服务会根据用户输入的故障现象自动推断一个默认日志关键字,例如用户提到 timeout、超时、5xx、502、504 之类的现象时,系统会优先联想到 timeout 相关日志,然后再去调用日志工具。它同时还会决定是否查询知识库,并把最后查回来的各类结果整理成一份统一的分析结论。
第 9 步建立起来的分析流程是固定顺序的。它会先查告警,再查日志,再查知识库,最后输出综合结论。告警部分复用了第 7 步接入的真实 Prometheus 能力,日志部分复用了第 8 步接入的真实 CLS MCP 日志能力,知识库部分复用了第 4 步做好的 RAG 检索能力。也就是说,第 9 步并没有重新发明底层能力,而是把前面几步已经做好的真实能力第一次整合到了一个统一流程里。
这一阶段还有一个很重要的产物,就是 triage 的输出不再只是工具原始结果,而是一份已经被整理过的故障分析报告。返回结果里包含 summary、alerts、logs、docs 和 trace。其中 summary 是对告警、日志、知识库结果的综合解释;alerts 是真实告警列表;logs 是真实日志列表;docs 是知识库命中的文档片段;而 trace 则记录了整个分析过程中每一步具体调用了什么工具、传了什么参数、返回了多少条结果。这说明第 9 步不只是把多个工具拼在一起,还保留了一条清晰可追踪的分析过程。
在第 9 步初版跑通之后,你又发现了一个真实问题,那就是如果直接按 service=gateway 去查告警,而 Prometheus 当前没有带这个标签的 firing 告警,那么 triage 会直接得到空结果。为了解决这个问题,第 9 步后来又做了一次优化,加入了告警 fallback 逻辑。现在的策略是先查指定服务的告警,如果没有命中,再回退去查全局 firing alerts,然后再从这些全局告警里筛一遍,看有没有和目标服务相关的内容。为了支持这个优化,IncidentAnalysisService 里补充了服务匹配和二次筛选的逻辑,同时在 trace 中清楚记录了 primary 和 fallback_global 两段查询过程,还在返回里增加了 alert_meta,说明这次分析有没有触发回退。
从实际验证结果来看,第 9 步已经完成了它最重要的目标。现在 /api/triage 已经能够稳定执行多步协同分析,能够自动串联告警、日志和知识库查询,也能够在响应中返回结构化的分析结果和 trace。你已经看到这个接口成功执行了真实的 query_alerts、query_logs 和 query_internal_docs,并且告警 fallback 逻辑也已经真正生效。也就是说,第 9 步当前已经不是“想法”或者“设计图”,而是一个真正能运行的多步分析接口。
当然,这一阶段也暴露出了两个结果质量问题。一个是日志结果容易因为测试数据时间窗口太短而变成空;另一个是知识库里还残留着早期的测试文本,导致 triage 有时会命中无关内容。这两个问题你现在已经决定先不处理,因为它们影响的是分析质量,而不是第 9 步是否成立。换句话说,第 9 步的流程能力已经建立起来了,只是数据质量还有后续优化空间。
如果要用一句完整的话来概括第 9 步,那么可以这样说:第 9 步完成了 Python OnCall 的第一版多步协同分析能力,通过新增 /api/triage 接口,把真实告警、真实日志和知识库检索按固定顺序组织成一次故障初步分析流程,并在此基础上补上了告警查询的 fallback 回退逻辑,使系统从“能分别调用多个工具”进化到了“能自动完成一次结构化故障分析”。
Step10
第 10 步做的核心事情,是把第 9 步已经实现的“固定流程多步分析”进一步升级成了一个会根据当前证据动态决定下一步动作的调查器。如果说第 9 步解决的是“把查告警、查日志、查知识库按固定顺序串起来”,那么第 10 步解决的就是“系统能不能根据前一步查到的结果,自己决定下一步到底该查什么”。所以第 10 步的本质,不是再新增一个工具,而是让系统从固定流程走向轻量 Agent。
这一阶段最重要的变化,是新增了一个新的接口 /api/investigate。这个接口和第 9 步的 /api/triage 不同,/api/triage 更像是一个固定模板分析器,收到请求后总是按既定顺序去查告警、查日志、查知识库。而 /api/investigate 更像一个调查循环,它会先规划下一步动作,再执行工具,再根据执行结果继续规划下一步,直到达到最大步数或者系统判断当前证据已经足够。这意味着第 10 步第一次把系统推进到了“plan / execute / finish”这种更接近 Agent 的工作方式。
为了支撑这套能力,第 10 步新增并完善了一组专门的文件。首先增加了 app/schemas/investigate.py,用来定义调查接口的请求结构,例如 service、symptom、max_steps、use_rag 这些字段。然后新增了 app/api/routes/investigate.py,把调查器正式暴露成 HTTP 接口。更核心的是新增了 app/services/investigation_service.py 和 app/services/planner_service.py。其中 InvestigationService 负责整个调查循环的运行,PlannerService 负责决定下一步动作。除此之外,还新增了 app/prompts/investigation_prompts.py,专门用来约束 Planner 的输出格式和决策边界。
第 10 步一开始并不是直接上完全自由的大模型 Agent,而是先做了一个规则式 Planner 的版本。这个版本的重点,是先把“调查循环”本身搭起来。它会根据当前 evidence 和 trace 去判断下一步是查告警、查日志、查知识库,还是直接 finish。虽然一开始这个 Planner 还是规则驱动的,但它已经实现了第 10 步最关键的结构变化:系统不再只是固定顺序执行,而是进入了一个每一步都要重新决定下一步的循环。
在规则式版本跑通之后,第 10 步又继续做了增强版,也就是把 PlannerService 从纯规则逻辑升级成了LLM Planner 优先、规则兜底的形式。这一步的意义非常大,因为它让“下一步调查什么”不再完全由你预先写死,而是开始由模型根据当前 evidence 来决定。为了避免模型随意发挥,这一版又特别加入了严格的 JSON 输出约束。模型只能输出固定动作:query_alerts、query_logs、query_internal_docs 或 finish,并且必须带上 arguments 和 reason。这样既保留了 LLM 决策的灵活性,又没有失去系统可控性。
第 10 步里,InvestigationService 的作用也比第 9 步更复杂了。它不仅仅是顺序调三个工具,而是会维护一个持续更新的 evidence 和 trace。每次 Planner 产出动作之后,系统都会执行对应工具,把结果放进 evidence,再把这一步的计划和执行情况写进 trace。下一轮 Planner 再根据更新后的 evidence 决定新的动作。这样一来,系统就真正有了“边查边想”的味道,而不是一次性跑完一个固定模板。
从实际验证结果来看,第 10 步已经经历了两个清晰阶段。第一阶段是规则式调查器跑通了,也就是说 /api/investigate 已经能以 plan -> execute -> finish 的形式工作,并且会把告警、日志、知识库的结果按步骤累积到 evidence 中。第二阶段是增强版成功接入了 LLM Planner。增强版的测试结果里,你已经看到 Planner 不再只是按固定模板走,而是会根据前一步没查到 timeout 日志,继续尝试改查 5xx 相关日志,再在日志仍没有结果时转去查知识库,最后主动 finish。这说明第 10 步增强版已经具备了真正“根据当前证据调整策略”的能力。
这一阶段输出结构也更接近真正的调查器了。返回结果中除了 summary 之外,还包含 evidence 和 trace。summary 是最后整理出来的阶段性调查结论,evidence 保存了调查过程中累计到的告警、日志和知识库证据,trace 则详细记录了每一步计划了什么、执行了什么、为什么这么做。这使得第 10 步的系统不只是会给结果,而且会给出一条完整的调查轨迹。
从项目演进角度来看,第 10 步最大的价值,是让系统从“多工具系统”正式迈向了“轻量 Agent 系统”。在第 9 步之前,系统虽然已经能用多个真实工具,但工具之间的协作关系是你提前设定好的;而到了第 10 步,系统已经开始具备根据当前上下文动态决定下一步动作的能力。虽然它还没有完全达到你最开始理解的 Plan-Execute-Replan 那种复杂程度,但已经非常接近真正 Agent 的雏形了。
如果用一句完整的话来总结第 10 步,那么可以这样说:第 10 步完成了 Python OnCall 项目中“轻量调查器”的落地,通过新增 /api/investigate 接口,引入 PlannerService + InvestigationService 的 plan / execute 循环机制,并进一步将 Planner 从规则式升级为 LLM 驱动、规则兜底的结构化决策器,使系统能够根据当前证据动态调整下一步调查动作。
Step11
第 11 步做的核心事情,是把第 10 步已经具备的“多步调查能力”进一步升级成了一个真正意义上的 Plan-Execute-Replan 工作流。如果说第 10 步解决的是“系统能不能根据当前证据决定下一步动作”,那么第 11 步解决的就是“系统能不能先形成一个阶段性计划,执行以后再根据新结果主动调整计划”。所以第 11 步的本质,不再只是一个会循环执行的调查器,而是一个具备计划、执行、重规划三段结构的 AI Ops 调查流程。
这一阶段最重要的新能力,是新增了一个独立的接口 /api/plan_execute。这个接口和前面的 /api/triage、/api/investigate 都不一样。/api/triage 是固定流程分析器,/api/investigate 是按当前证据逐步决定下一步的轻量调查器,而 /api/plan_execute 则是在调查开始之前先形成一个“初始计划”,再按计划执行,并在每一步执行结束后根据新证据决定是继续原计划、替换后续计划,还是直接结束。这意味着第 11 步第一次把系统推进到了真正接近你最开始提到的 Plan-Execute-Replan 形式。
为了实现这条链路,第 11 步新增了 app/schemas/plan_execute.py、app/prompts/plan_execute_prompts.py、app/services/plan_execute_service.py 和 app/api/routes/plan_execute.py 这几个关键文件。其中 plan_execute.py 这个 route 把新的工作流正式暴露成 HTTP 接口;plan_execute.py 对应的 schema 负责规范请求参数,比如服务名、症状、最大轮数、是否使用知识库等;而最核心的实现全部集中在 PlanExecuteService 中。除此之外,这一步还在 prompt 层面新增了专门的计划提示词和重规划提示词,也就是INITIAL_PLAN_SYSTEM_PROMPT 和 REPLAN_SYSTEM_PROMPT。这说明第 11 步不只是新增了一个服务,而是把“初始规划”和“执行后重规划”明确拆成了两个不同阶段。
第 11 步的第一部分能力,是让系统具备了先生成初始计划的能力。系统现在不再是一上来就直接查工具,而是会先根据 service 和 symptom 生成一个 JSON 格式的初始调查计划。这个计划里包含 goal 和 steps。goal 表示这轮调查的目标是什么,steps 则是一组准备执行的动作,每个动作都包含 title、action、arguments 和 reason。而且这些动作并不是随便生成的,自始至终都被严格约束在固定集合里,也就是 query_alerts、query_logs、query_internal_docs 和 finish。这意味着第 11 步虽然开始引入计划能力,但系统仍然保持着很强的可控性。
第 11 步的第二部分能力,是把初始计划真正执行起来。PlanExecuteService 会维护一组 pending_steps,也就是当前待执行的计划步骤。每一轮执行时,系统会从当前剩余计划中取出第一步,记录到 trace 里作为 phase=plan,然后调用对应工具执行,再把执行结果记录成 phase=execute。与此同时,它还会把执行到的告警、日志、知识库结果写入 evidence。所以第 11 步并不是“只会生成计划”,而是已经把“计划”真正接到了“执行”上,形成了一条完整的执行链。
第 11 步最关键、也最区别于第 10 步的部分,是 Replan。也就是在每轮执行之后,系统不会机械地继续跑剩余步骤,而是会把“刚才执行了什么”“刚才查到了什么”“当前还剩哪些步骤”“当前 evidence 长什么样”重新整理出来,再交给一个专门的重规划器判断。这个重规划器会输出三种决策之一:continue、replan 或 finish。如果是 continue,就说明剩余计划还有价值,可以继续执行;如果是 replan,就说明当前计划不够好了,需要替换成新的后续计划;如果是 finish,就说明当前证据已经足够,或者继续查下去价值不大,可以结束本轮调查。正是这一层,让第 11 步真正具备了“重规划”的能力。
从你实际跑出来的结果看,第 11 步已经把这三段结构完整体现出来了。返回中已经有 initial_plan,说明初始计划阶段成立了;trace 里不仅有 phase=plan 和 phase=execute,还有 phase=replan,说明执行后重规划阶段也已经落地了。而且这次的重规划并不只是形式上的记录,而是真的在改变后续路径。初始计划里原本的重点是查 gateway 告警、查 gateway 日志、查知识库、结束调查;但执行到日志为空之后,系统并没有机械地照着原计划往下走,而是主动生成了一个新的调查方向,转去查下游服务日志。这一点非常关键,因为它说明第 11 步不是“先写个计划再死板执行”,而是已经具备了“根据执行结果改计划”的行为。
这一阶段还做了一个非常重要的工程设计,就是 LLM 优先、规则兜底。无论是初始计划还是重规划,都优先尝试通过 LLM 按严格 JSON 输出。如果 LLM 输出格式错误、动作非法、或者根本没法解析,系统就会自动退回到保守规则策略。这一点非常重要,因为它避免了第 11 步一旦引入大模型就变得极不稳定。也就是说,第 11 步虽然开始把 AI 决策引入到更上层的工作流里,但仍然保留了足够强的工程兜底能力,这样接口不会因为模型一次输出歪掉而直接不可用。
第 11 步的返回结果结构也比前面更完整。现在 /api/plan_execute 的返回里不只有 summary,还有 initial_plan、evidence 和 trace。initial_plan 体现的是系统最开始打算怎么调查;evidence 保存的是执行过程中真正收集到的告警、日志和知识库内容;trace 则记录了整个过程里每一轮的计划、执行和重规划决策。这样一来,第 11 步的系统不仅能给用户一个结论,还能完整展示“我是怎么得到这个结论的”。这使得它已经非常接近一个真正可解释的 AI Ops 调查器。
当然,从这次的实际结果里也能看出,第 11 步虽然流程已经成立,但证据质量仍然受当前真实数据环境限制。当前告警为空、日志为空、知识库命中的还是测试文本,所以系统即使已经会计划、会执行、会重规划,最后得到的证据仍然不够强。这说明第 11 步现在已经不是“框架没搭起来”的问题,而是“框架搭起来之后,数据质量如何继续提升”的问题。换句话说,第 11 步的工作流能力已经完成,后续更多是质量优化而不是结构重建。
如果用一句完整的话来总结第 11 步,那么可以这样说:第 11 步完成了 Python OnCall 项目中 Plan-Execute-Replan 工作流的第一版落地,通过新增 /api/plan_execute 接口,让系统具备了先生成初始调查计划、按计划执行工具、再根据执行结果动态重规划后续步骤的能力,并最终输出带初始计划、执行轨迹、重规划记录和证据统计的完整调查结果。
全程按照GPT的提示实现,先复现,之后再来梳理这个过程。
原项目来自小林Coding,但是只有基于Java和Go的实现。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)