最近把本地智能体(Agent)这套东西认真梳理了一遍,最终选定了 OpenAgents 作为框架、智谱 GLM 作为模型服务,成功搭起一套能调用工具、能记住上下文、也能逐步执行多轮任务的本地 Agent。整个过程踩了不少坑,也积累了不少经验,所以这一篇我打算把从设计思路到配置细节、再到排查技巧全部写透,给想在一个可控环境中跑 Agent 的同学一份可以直接照着做的手册。
先说结论,这套组合真正解决的是“模型服务外部化、智能体逻辑本地化”的问题。OpenAgents 负责本地侧的调度、工具注册、记忆管理和执行循环,智谱 GLM 则通过标准接口提供稳定的理解与生成能力。好处很明显:你不需要本地跑一个巨大的模型,也能拥有一套完全由自己掌控逻辑的智能体;同时因为工具和流程都在本地,后续加新工具、改提示词、接内部系统都很方便。适合正在做自动化实验、想研究 Agent 内部机制,或者打算把 Agent 集成进自己服务里的开发者参考。
1. 为什么要在本地搭一套 GLM 智能体
1.1 先把“本地智能体”这个概念理清楚
很多人一听“本地智能体”,第一反应就是要把大模型也装到本地,动不动十几 GB 甚至几十 GB 的权重文件,还得考虑显卡显存、量化、推理速度。实际上“本地”这个词在 Agent 场景里往往是指“智能体工程逻辑在本地”,而不是“推理也必须在本地”。
我做的这套,模型接口指向智谱 GLM 的开放性 API,但 OpenAgents 框架、工具函数、记忆存储、执行调度全部在自己机器上。这样有几个非常直接的好处:第一,机器配置要求大幅降低,一台普通开发机能跑 Python 就行;第二,迭代速度快,改 Agent 逻辑不用重新部署模型;第三,私密性更好,自己的数据只在自己机器上流转,需要发送给模型的内容由代码严格控制。
如果你确实想连推理也本地化,也可以把 OpenAgents 的接口层做替换,接一套本地推理服务,但那是后话。先把框架跑通,比什么都重要。
1.2 这套组合真正解决的几个问题
用 OpenAgents 接智谱 GLM,最直观的价值体现在四个方面。
其一,工具编排能力。直接调 API 充其量只能做“单轮问答”,但是 Agent 的价值是会“动手”。我在本地给智能体注册了计算器、时间查询、日程写入等工具,它能在对话中自主决定调用哪个工具,执行完再把结果带回去推理。这个过程如果全部自己写 ReAct 循环,很繁琐;OpenAgents 提供了一个合理的框架,我只需要注册工具并描述清楚,剩下的调度交给 Agent。
其二,成本可控。本地框架配合云端模型,可以按需选择不同规格的模型。简单任务用轻量模型,复杂任务再切高级模型,这些策略在本地代码里都很容易实现,比单纯在某个模型平台里配置要灵活得多。
其三,提示词和系统逻辑完全私有。很多场景下提示词本身就是一种资产,或者包含内部业务规则。把提示词放在本地代码里,意味着版本管理、灰度调整、按用户区分策略都能做,不用在公网产品页上反复编辑。
其四,扩展性强。本地 Agent 可以接本地数据库、文件系统、定时任务、企业内部服务。我后来加的一个功能就是从本地 JSON 文件读取待办清单,再由 Agent 做优先级排序,整个过程很顺畅,API 层面完全无感知。
1.3 适用人群与选型判断
我建议三类朋友优先考虑这套方案。一类是做技术验证的开发者,想搞清楚 Agent 的闭环是怎么一回事;另一类是有自动化需求但不想全上云端平台的工程师,希望在自建系统里跑通“对话 + 工具 + 记忆”的能力;还有一类是做课程或分享的同事,需要在可控环境里做一个能演示的智能体 Demo。
选型判断上,OpenAgents 这类开源框架的优势在于透明可控、没有绑定关系,你随时可以替换模型服务、改工具实现。智谱 GLM 的优势在于接口形态贴近主流协议,模型名、参数、返回格式都比较规整,接入成本低。两者组合起来,既能快速见效,又有足够的改造空间。我当时也考虑过其他方案,但综合来看这套最省心。
2. OpenAgents 核心机制拆解
2.1 Agent 内核的循环真相
Agent 之所以区别于普通聊天机器人,关键在于它内部有一个“感知、决策、行动、观察、再决策”的循环。OpenAgents 把这条循环封装成了一个运行时,用户只需要关注三个层面:定义好系统提示词、提供工具列表、在模型返回中解析动作。
简单来说,一次任务执行是这样的:用户输入先进来,Agent 把它组装成消息序列,发送给 GLM;模型返回一段内容,可能是一个普通的回复,也可能是一个“要调用某个工具”的结构化指令;框架解析这个指令,在本地找到对应的工具函数,执行得到结果;结果再作为消息拼回上下文,发给模型,让它继续思考。这个过程一直持续到模型认为任务完成,或者步数达到上限。
我一开始犯的错是把它想得太神秘,老觉得需要一个复杂的状态机。实际上跑通以后就会发现核心就是“while 循环 + 消息拼接 + 函数分发”,OpenAgents 只是把这些做成了优雅的抽象,并额外处理了历史记录、错误重试等边角逻辑。
2.2 工具调用是 Agent 的“手”
工具调用是本套方案的灵魂。OpenAgents 会要求每个工具提供名字、描述、参数说明,这些信息会被编入提示词,让模型“知道”有这些工具可用。关键在于描述要写得足够清楚,模型不是靠函数名来理解工具的,而是靠描述文本。
我给工具写描述时的体会是:要说清“工具能做什么”“什么情况下调用它”“参数分别表示什么”。比如“查询当前时间,不接收参数,当用户询问现在几点时使用”,就比“获取时间”要好得多。模型对自然语言描述的理解力远比我们想象中强,但前提是描述准确、边界清晰。
工具返回值也会被拼进上下文,所以返回值要尽可能结构化、简洁。冗长的返回值不仅浪费 token,还可能让模型抓不住重点。我建议工具函数返回字符串时,按“关键信息 + 精简结论”的格式组织,比如日程查询返回的就是“你最近的日程/事项/时间”三行,信息密度高,模型一眼就能读取。
2.3 记忆与上下文管理的门道
Agent 比普通 API 调用复杂的地方还在于“记忆”。对话历史、工具调用记录、之前的结果都被保存在上下文中,模型才能保持连贯。OpenAgents 在这个层面提供的是基础的会话管理能力,真正的记忆策略则需要使用者自己设计。
短期记忆就是当前轮次内的消息序列,保存所有上下文;长期记忆则是把重要信息落盘,比如 SQLite、JSON 文件或者向量数据库,下次会话再加载。我自己的实践是先做短期记忆,把多轮对话完整保留,跑通以后再考虑长期记忆,避免一开始就把复杂度堆高。
上下文长度也是一道硬约束。GLM 的上下文窗口虽然不小,但对话一旦变长,历史消息会迅速占满。这种情况要么压缩历史,要么让 Agent 在适当的时候“忘掉”旧信息。我后面会把这个问题单独拉出来讲,因为它在实际运行中几乎一定会遇到。
3. 环境准备与基础配置
3.1 Python 环境与依赖安装
本地搭建的第一步是准备环境。我使用的是 Python 3.10 的虚拟环境,这是稳妥的选择,太老的版本可能缺失部分类型语法支持,太新的版本在个别依赖上又可能遇到兼容问题。
做法很常规:创建虚拟环境、克隆 OpenAgents 源码、安装依赖。之所以直接拉源码而不是用包安装,是因为我要改动工具注册逻辑和调试内部循环,源码方式最灵活。依赖文件里主要包含 requests、pydantic、pyyaml 这一类常规库,安装过程基本不会出问题。
装好以后建议先跑一下项目自带的快速示例,确认框架本身的运行没问题,再开始接 GLM。很多时候后面的异常其实并不是配置错误,而是基础环境没就绪,先跑通自带的示例能排除一大半问题。
3.2 申请智谱 GLM API Key 的注意点
接入 GLM 需要一个 API Key,获取和配置本身不复杂,但有一些细节值得说。
首先是模型名的选择。智谱开放平台上有不同规格的模型,名称各不相同。环境准备阶段我建议先选定一个默认模型,比如 GLM-4 系列中的标准对话模型,它在多数任务上表现均衡,适合作为初跑版本。后续再根据任务复杂度调整。
其次是接口地址,这是一个很容易踩坑的点。GLM 开放平台提供兼容主流协议的接口,base_url 需要写到版本路径那一层,不能只填域名。我当时第一次使用少写了一层路径,结果一直报 404,排查了很久才发现是 URL 拼接问题。
最后是 Key 的保存方式。我从不把 Key 直接写在代码里,而是放到环境变量文件中,运行前加载。这样既安全,也方便在多台机器间同步配置,不会因为换了环境就改代码。
3.3 配置文件的组织方式
为了不把参数散落在代码里,我建议使用一个 YAML 或 JSON 配置文件集中管理模型相关参数。里面至少包含模型名称、接口地址、最大 token 数、温度等关键信息。
温度这个参数值得多说一句。Agent 场景和闲聊场景不一样,工具调用要求精确、稳定,所以温度我调得比较低,大概在 0.2 到 0.4 之间。调高了容易出现模型输出结构不稳定的情况,偶尔会编造不存在的工具名,或者返回无法解析的格式。做对话场景可以适当调高,但做 Agent 优先保证结构稳定。
配置好之后,应该写一个简单脚本读取配置并调用一次 GLM,验证连通性和参数正确。这一步我强烈建议单独做,不要等 Agent 跑挂了再回头排查网络层问题。
4. 本地智能体搭建实操
4.1 初始化项目结构与基础模块
实操阶段我从一个最小可用的项目结构说起。项目目录里主要分三块:配置文件、工具模块、Agent 核心模块。工具模块专门放自定义函数,Agent 核心模块负责组装请求、解析响应、调用工具。
我把一个最小的目录结构列出来供参考:
agent-demo/ ├── config.yaml ├── tools.py ├── agent.py └── memory.dbconfig.yaml 里保存模型配置,tools.py 里定义工具函数,agent.py 里写主循环,memory.db 是后面做记忆持久化时自动生成的。结构不复杂,但职责清晰,后续加新工具只需要在 tools.py 里加函数并更新注册表。
4.2 编写一个最简 Agent 核心循环
OpenAgents 提供了一些封装,但为了真正理解,我建议至少自己手写一次核心循环。下面这个示例是我当时用来做原型验证的简化版,只有三十多行,能清楚展示 Agent 的本质:用请求拼消息、从响应里解析动作、执行工具并把结果追加回上下文。
import json import os import requests GLM_URL = os.environ["GLM_URL"] GLM_API_KEY = os.environ["GLM_API_KEY"] MODEL = os.environ["GLM_MODEL"] TOOLS = {} def call_glm(messages): body = { "model": MODEL, "messages": messages, "temperature": 0.2, "max_tokens": 4096 } resp = requests.post( GLM_URL, headers={"Authorization": f"Bearer {GLM_API_KEY}"}, json=body, timeout=60 ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] def run(task, max_steps=5): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task} ] for _ in range(max_steps): result = call_glm(messages) action = parse_action(result) if not action: return result tool_result = execute_action(action) messages.append({"role": "assistant", "content": result}) messages.append({"role": "user", "content": f"工具返回结果:{tool_result}\n请继续"}) return result这段代码把 Agent 主循环的四个要素都涵盖了:发起请求、解析动作、执行工具、回填结果。parse_action 函数会把模型返回的文本尝试解析成 JSON 格式的动作描述;execute_action 负责从 TOOLS 字典中查找并执行对应函数。
这里有个设计细节值得注意:我把“工具返回结果”放在 user 角色的消息里,并提示模型“请继续”。这种写法虽然朴素,但在很多模型上都很稳定,比起非要按某种专有格式返回要通用得多。OpenAgents 内部的机制比我这个示例更完备,但核心思想完全一致。
4.3 注册工具:让 Agent 拥有动手能力
工具函数是 Agent 能力的来源。我最初写的一个工具是获取当前时间,它的代码很简单,但让我摸清了工具注册的格式。之后我加了一个计算器工具,再之后加了一个写日程的工具,逐步让 Agent 从“只会说”变成“真的会干活”。
先看一个计算器工具的实现。这里有安全考虑,不能直接把表达式丢给 eval,因为模型生成的字符串不可控,可能注入危险操作。我会先把表达式过滤一遍,只允许数字、四则运算符、括号和小数点,再做计算。
import re def calculate(expression: str) -> str: safe_pattern = re.compile(r"^[0-9+\-*/().\s]+$") if not safe_pattern.match(expression): return "表达式包含不支持的字符" try: result = eval(expression, {"__builtins__": {}}, {}) return f"计算结果为 {result}" except Exception as exc: return f"计算失败:{exc}"注册时,我会为它配上描述和参数说明,再放进 TOOLS 字典。描述会直接影响模型能否正确触发工具调用。对于计算器,我写的描述类似于“执行四则运算表达式,当用户需要计算数字时调用,参数 expression 为要计算的数学表达式”。短短一句话,模型就能在合适场景主动调用。
再举一个日程写入工具的例子。它把待办内容追加到本地 JSON 文件,可以让 Agent 做到“帮我把明天上午十点开会记入日程”。这种工具实现的是真实系统交互,参数又多了一个时间字段。注册时要把参数含义写清楚,否则模型经常不知道该传什么。
import json from datetime import datetime def add_schedule(task: str, when: str) -> str: filename = "schedules.json" try: with open(filename, "r", encoding="utf-8") as f: records = json.load(f) except FileNotFoundError: records = [] records.append({"task": task, "when": when, "created_at": datetime.now().isoformat()}) with open(filename, "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2) return f"已添加日程:{when} 的 {task}"工具的设计还有一个容易被忽略的点:返回值必须是字符串。模型带入上下文的是文本,而不是 Python 对象,所以工具函数最好统一返回字符串。复杂的结构化数据可以序列化成 JSON 字符串返回,但不要让模型去猜对象结构。
4.4 系统提示词与工具描述的编写技巧
系统提示词在 Agent 场景中承担的任务比聊天场景更重要,它不仅要设定人设,还要告诉模型“这是一个工具调度型 Agent,按 JSON 格式输出动作”。这种结构化约束是保证 Agent 稳定运行的关键。
我的系统提示词大致包含四块内容。第一块是角色定位,说明自己是本地智能体,能使用工具。第二块是输出格式说明,要求当调用工具时输出 JSON 格式的动作对象。第三块是执行准则,比如不要编造工具执行结果、一次性只调用一个工具、获得结果后继续推理。第四块是工具清单,把当前可用的工具名、功能和参数示例列出来。
工具描述要具体到“什么时候用”。我做过对比,描述模糊时模型会把不相干的问题也尝试调用工具,浪费步骤;描述清晰后,触发准确率明显提升。写好工具描述,比调模型参数带来的提升更明显,这是投入产出比最高的一件事。
4.5 执行与验证:一个完整任务跑通的过程
所有代码就位后,我先用一个简单任务验证整套链路:“现在几点了?帮我安排明天上午十点开会。”
这个任务设计得很有技巧,它一半是查询、一半是写入,能同时验证两个工具的调用。第一次运行时我全程打日志,每拿到一次模型返回就打印出来,亲眼看到模型先输出动作对象、然后工具执行、结果回填、模型最终生成确认回答,整条链路一气呵成。
这里要特别强调日志的重要性。我之前排查问题最痛苦的就是“模型到底返回了什么”,OpenAgents 或自定义 Agent 在请求失败时,如果不打日志,你根本不知道是网络问题还是内容解析问题。我后来养成一个习惯,每个关键节点都打印一行日志,尤其是模型原始返回和解析后的动作对象。第一次跑通以后,我反而建议手动改坏一个工具,故意让 Agent 报错一次,观察错误日志长什么样,这样真出问题时你才知道从哪看起。
5. 进阶:多工具协同与记忆持久化
5.1 多工具协同的基本原则
基础版本跑通后,我很快遇到了“工具多了反而变笨”的现象。当我把六七个工具一起注册进去,模型开始频繁选错工具,浪费步数。问题出在工具描述和边界没有梳理清楚,而不是模型能力不够。
我的解决方法是分而治之。第一,把工具按领域分组,比如“时间日期类”“计算类”“日程类”;第二,每个工具描述里明确“不要在其他领域使用本工具”;第三,如果有功能相近的工具,就合并成一个带模式参数的函数,而不是让模型在多个相似工具之间做选择。经过这一轮整理,工具调用的准确性提升非常明显。
多工具协同还有一个顺序问题。有些任务需要先查询、再计算、再写入,模型需要决定调用工具的先后顺序。Agent 循环本身就支持这种多步执行,只要每一步的结果都能正确回填到上下文,模型就能根据中间结果继续决策。这背后依赖的是良好的上下文组织,以及足够宽的历史窗口。
5.2 用 SQLite 做简单的记忆持久化
多轮会话以后,如果不做持久化,一旦进程重启,Agent 就完全失忆。对很多实际场景来说,这是不可接受的。我给 Agent 加了最简单的持久化:用 SQLite 保存每一轮的对话消息,下次启动时把最近 N 条消息重新加载进上下文。
这种做法的价值不在于“聪明”,而在于实用。Agent 重启后能立刻恢复最近的对话状态,用户感知不到中断。等以后消息量变大,可以再把向量检索引进来,但现阶段 SQLite 足够稳,也足够简单。
import sqlite3 def init_db(): conn = sqlite3.connect("memory.db") conn.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.commit() return conn def save_message(conn, role, content): conn.execute("INSERT INTO messages (role, content) VALUES (?, ?)", (role, content)) conn.commit()加载历史时,我用一个简单的思路:按创建时间倒序取出最近二十条消息,再正序拼回 messages 列表。这个数量可以根据模型窗口灵活调整。历史加载的代码并不复杂,但它让 Agent 从一个无状态的函数变成了一个有记忆的服务。
5.3 流式输出与交互体验优化
Agent 的执行过程可能耗时较长,工具调用多的时候,用户会在界面上看到一个长达十几秒的空白等待。为了改善体验,我参考 GLM 的流式接口,把模型的返回改成了逐字输出的模式,测试时能明显感觉到响应“更快了”,实际是首批 token 到达的时间变短,而不是总时间变短。
流式模式下,Agent 循环多了一个注意点:工具动作的解析不能依赖完整文本。因为流式返回是增量数据,你不能等全部写完再解析,否则就失去了流式的意义。我的做法是先缓存一个本地缓冲区,当检测到缓冲区里的 JSON 已经完整时,就直接中断本轮生成,执行对应工具。这个检测不需要很复杂,判断 JSON 是否可解析即可。
def call_glm_stream(messages, on_token): body = { "model": MODEL, "messages": messages, "stream": True } resp = requests.post(GLM_URL, headers=..., json=body, stream=True, timeout=60) for line in resp.iter_lines(): if not line: continue decoded = line.decode("utf-8") if decoded.startswith("data: [DONE]"): break if decoded.startswith("data: "): payload = json.loads(decoded[6:]) delta = payload["choices"][0]["delta"].get("content") if delta: on_token(delta)流式过程一定要处理中断和异常。模型生成中途断开连接、用户主动取消、超时,这些情况都要能安全退出循环,否则内存里会留下一个半完成的请求。我加了 try/finally,确保线程能正常回收。
6. 常见问题与排查技巧记录
6.1 请求超时与网络波动
实际运行中遇到最多的就是请求超时。Agent 的一次任务往往包含多轮模型调用,每一轮都可能是几秒到十几秒,如果再加上工具执行时间,总耗时很容易超过常规 HTTP 客户端的默认超时设置。请求一超时,循环就断,之前已经执行的中间步骤全部作废。
我的处理方案是给 requests 设置一个较长的超时,比如 60 秒,同时加一个简单的重试机制。重试要注意退避策略,第一次失败等一秒,第二次等两秒,最多重试三次。重试期间既要缓解瞬时波动,也不能让无意义的重复请求消耗太多额度。
另一个隐蔽问题是工具执行超时。如果某个工具函数访问外部服务,它自身的超时时间也要显式设置,否则会出现模型一直等工具返回的情况。我建议所有工具内部网络请求都带上 timeout 参数,这是很多教程里没提的坑。
6.2 模型名称与参数不匹配
GLM 的接口虽然兼容主流格式,但不同模型对参数的支持程度不一样。我踩过的一个坑是:某个模型不支持某些高级参数,传了以后接口直接报错。排查方法是先把请求体精简,逐步加参数,找出是哪一项触发了异常。
模型名称大小写、别名、版本后缀也可能是问题来源。代码里配置的模型名必须和接口文档严格一致,多一个空格或版本标记都会导致 404 或 400。我建议把模型名放在配置文件里,不要在代码中硬编码,方便切换。
还有一点,不同模型对“工具调用”的支持格式有差异。如果你的 Agent 一直返回普通文本而不是动作对象,很可能是模型本身不擅长按你的 JSON 格式输出,而不是代码问题。这时可以考虑换一个规格更高的模型,或者把你的输出格式示范写得更加明确。
6.3 工具调用 JSON 格式不稳定
这是做 Agent 时最让人头疼的问题之一。模型偶尔会把动作对象包装成 Markdown 代码块,偶尔会在 JSON 前后多输出解释性文字,导致解析失败。
我的对策是容忍与修复并行。解析函数先尝试直接 json.loads,失败后用正则找到第一个花括号到最后一个花括号的区间再做解析,再不行就尝试去掉代码块标记。这套“三级容错”把解析成功率提升到了接近百分之百。
当然,最好的办法是让模型别产生这种输出。我在系统提示词里明确加了“直接输出 JSON,不要使用 Markdown 代码块,不要添加解释文字”,并在示例中给出一段标准格式。示例的作用比抽象说明大得多,尤其在模型遵循指令不够稳定时。
6.4 上下文长度与成本控制
Agent 循环每执行一步,就会往上下文里追加好几条消息。任务稍微复杂一点,几千 token 就出去了。上下文窗口虽然能容纳,但成本是真实的,而且过长上下文也可能分散模型注意力。
我有两个手段。第一是历史压缩,当消息数量超过阈值时,让模型对旧消息做一次摘要,用摘要替换原始记录。第二是边界控制,工具返回的结果只保留核心字段,丢弃无关信息。这两个手段配合起来,能把 token 消耗降下来三分之一到一半。
控制成本还有一个简单但有效的思路:区分任务难度。简单任务走轻量模型,复杂任务才切高级模型,甚至同一个 Agent 里可以让“理解意图”和“最终生成”使用不同模型。这种策略在本地框架里实现起来很容易,只是需要在配置上多花点心思。
6.5 常见问题速查表
我把踩过的典型问题整理成了一个速查表,方便直接对照。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 请求返回 404 | 接口地址路径不完整 | 检查 base_url 是否包含版本路径 |
| 请求返回 401 | API Key 错误或未加载 | 检查环境变量及 Key 前缀 |
| 模型一直输出普通文本 | 系统提示词缺少输出格式约束 | 在提示词中加入动作 JSON 示例 |
| 动作 JSON 解析失败 | 模型在 JSON 外添加了说明 | 加正则容错,并强化输出约束 |
| Agent 频繁选错工具 | 工具描述模糊或功能重叠 | 分组、合并相似工具、明确边界 |
| 上下文很快占满 | 历史与工具结果未压缩 | 使用摘要压缩和精简工具返回值 |
| 任务执行中断 | 单轮请求超时 | 加长时间;并设置重试退避策略 |
这些问题的共同根源多半不是单一原因,而是多个条件叠加。排查时不要只盯着表面报错,把日志打开看完整链路,往往比猜测根因更高效。
7. 一点个人体会
这套本地智能体从零跑通以后,我最深的感受是:Agent 工程真正的难点不在模型,而在如何把“工具”“记忆”“流程”三大块搭得足够稳。GLM 提供了很扎实的模型底座,但让它稳定地调用工具、按预期完成任务,靠的是本地的提示词策略、工具设计和上下文管理。
如果让我给后入坑的朋友一个建议,我会说:别急着堆高深功能,先把一个带计算器和日程写入的极简 Agent 跑通,彻底理解循环的每一环,再谈记忆、多 Agent、流式这些进阶话题。这个过程的每一步都能肉眼可见地积累经验,是最稳妥的路径。
最后再分享一个小技巧:日志永远是你最可靠的老师。我刚跑通时经常遇到“这次好了下次坏了”的间歇性问题,后来把所有关键节点全部打日志,排查效率一下子高了很多。工具返回了什么、模型在哪个节点抽风、解析在哪一步失败,日志里写得清清楚楚。很多看起来诡异的问题,其实都只是缺了一条日志而已。