hermes-agent实战拆解:大模型智能体框架的工具调用与ReAct设计 📅 发布时间:2026/9/9 4:47:23 👁 浏览次数: 1. 项目概述与核心定位1.1 从标题到项目的定位推理先说结论hermes-agent这个项目名一眼就能看出它属于当下最热的大模型应用层赛道——AI Agent。Hermes 是希腊神话里的信使神负责传递信息、引导灵魂、穿梭于神人之间这个名字放在 Agent 项目上特别贴切智能体本身就是连接用户与大模型能力、连接自然语言意图与具体工具执行之间的“信使”。我在看到这个标题的第一反应是这应该是一个基于大语言模型LLM的自主智能体框架目标是把“用户说一句话”变成“Agent 自主规划并完成一系列任务”。在这个方向上早期有 AutoGPT、BabyAGI后来有 LangChain 生态里的各种 Agent 实现再后来是更工程化的框架如 MetaGPT、AutoGen——hermes-agent基本都是同一类问题的不同解法但每个项目在架构取舍、使用场景、扩展方式上都有自己的侧重。从项目命名习惯推测这类项目通常聚焦几个核心诉求让大模型不再只是“聊天机器”而是能调用工具、能记住上下文、能拆解复杂任务并一步步执行。它解决的痛点是纯 Prompt 对话解决不了的“多步骤完成任务”问题。举个例子你跟普通聊天机器人说“帮我调研一下开源社区里排名前十的 Python Web 框架整理成表格发到我邮箱”它大概率只能给你一段文字建议而 Agent 类的项目则有可能真正做到先联网搜索、再整理数据、再调用邮件服务发送。hermes-agent这类项目的目标用户画像也比较清晰有一定 Python 基础的后端工程师、爬虫工程师、自动化运维人员以及对 LLM 应用开发感兴趣的独立开发者。1.2 项目适合谁来学习和参考我自己在实际把玩这类 Agent 框架时最大的感受是如果你只想调 API 聊天完全没必要上 Agent但如果你手里有一堆零散的脚本、API、内部工具想用自然语言把它们“粘”起来Agent 框架就是那个胶水层。所以hermes-agent这类项目最适合三类人做内部效率工具开发的工程师公司里总有各种内部系统、数据库查询、定时任务用 Agent 统一包装成自然语言入口能大幅降低使用门槛。个人开发者、独立黑客把自己常用的服务天气查询、股票盯盘、笔记整理、邮件发送通过 Agent 串成个人助理。研究 LLM 应用架构的学生或研究员Agent 框架代码量通常不大但麻雀虽小五脏俱全是学习 ReAct 模式、工具调用、记忆管理等概念的绝佳教材。当然如果你刚接触 Python 或者刚听说 OpenAI API我建议先跑通几个基础调用再来看 Agent 框架否则很容易被抽象层绕晕。接下来我从设计思路、核心模块、实操部署、问题排查四个维度把我实际把玩这类项目的经验完整拆一遍。2. 整体设计与架构思路拆解2.1 为什么需要 Agent 层而不是直接写死逻辑在动手写代码之前我们先想清楚一个核心问题为什么不能直接用 if-else 把工具调用写死比如用户说“查天气”就调用天气 API说“算数学题”就调用计算器——这样不是更简单吗答案很现实自然语言的表达组合是近乎无限的。用户可能会说“今天出门要带伞吗”“北京这会儿冷吗”“帮我看看明天上海的体感温度”这些话的意图都是“查天气”但字面差异巨大。用规则匹配你得维护几百条正则用意图分类模型你得标注数据、持续迭代。而 LLM 的优势恰恰在于理解语义、抽取意图Agent 模式做的事情就是让 LLM 充当“决策大脑”把理解到的用户意图翻译成结构化的工具调用指令再执行并反馈结果。这就是 Agent 模式和传统软件架构最本质的区别控制流不再由程序员预先写死而是由模型在运行时动态决定。程序员负责提供“工具箱”工具集和“行为边界”Prompt 约束模型负责“临场发挥”。这种模式的好处是灵活、泛化能力强代价是结果有一定随机性、需要设计兜底机制。hermes-agent这类项目所有架构上的巧思基本都是在解决“如何让模型的临场发挥更可控”这一个问题。2.2 核心模块的划分与职责一个标准的大模型 Agent 框架无论项目怎么命名核心模块都逃不开这几块。我对照hermes-agent的命名习惯和主流实现把架构拆成五层感知层输入解析负责接收用户输入做基础清洗比如去除多余空格、识别多轮对话中的指代“它”“那个”“刚才说的”把上下文历史拼装成模型可消费的消息序列。很多初学者忽略这一层直接把用户原话丢给模型结果多轮对话里充满了“帮我”“那个”这类指代不清的表述Agent 根本没法正确理解。规划层决策中枢这是 Agent 的灵魂。它通过 Prompt 引导 LLM 决定“下一步做什么”常见模式是 ReActReasoning Acting即让模型先输出思考过程再输出要调用的工具和参数。这一层往往需要设计输出格式约束比如要求模型输出 JSON或者用 Function Calling 的结构化能力。规划层的设计直接影响 Agent 的智商上限和稳定性。执行层工具调用根据规划层产出的指令实际调用对应的函数、API、脚本。这一层要考虑参数校验、异常捕获、超时控制。工具执行的结果需要以文本形式“回填”给模型让模型看到执行结果并决定下一步动作这就是 Agent 闭环里最关键的一环——反馈回路。记忆层上下文管理负责短期对话历史、长期事实记忆、向量数据库检索。Agent 和普通聊天最大的区别之一就是它需要在一个任务里做多轮“思考-行动-观察”如果不管理好上下文很快会超出模型的上下文窗口。记忆层是工程复杂度最容易被低估的部分。工具注册与发现层维护一个工具清单把每个工具的名称、描述、参数 Schema 告诉模型。模型根据描述选择工具所以工具描述写得清不清楚直接影响调用准确率。这一层在hermes-agent这类项目里通常是一个装饰器加注册表的设计。2.3 技术选型为什么用 Python为什么选这些依赖老实说做 Agent 框架Python 基本是唯一合理的选择没有之一。原因很简单大模型生态的 SDK 几乎全是 Python 优先LangChain、LlamaIndex 这些基础设施也都在 Python 生态里。如果你用 Java 或 Go光是封装各家模型 API 就能写到你怀疑人生。在模型接入上hermes-agent这类项目通常会设计成 Provider 模式——抽象出一个统一的LLMProvider接口再分别实现 OpenAI、Anthropic、本地模型如 Ollama 跑的 Llama等具体 Provider。这样做的好处是上层规划逻辑完全不用感知底层模型差异。实际接多个模型之后你才会发现不同模型的 Function Calling 输出格式、上下文长度、中文能力差异巨大Provider 这层抽象能帮你把差异隔离在最小范围。工具调用方面主流做法有两种一是让模型输出固定格式的 JSON再用json.loads解析后反射调用二是走各家厂商的 Function Calling 原生能力。前者兼容所有模型后者效果更稳定。我个人的经验是能走 Function Calling 就走 Function Calling解析自由文本输出太容易翻车了模型经常会多输出一段解释文字把你的 JSON 解析器搞崩。后面我会给出一个兼容两种方式的封装思路。3. 核心模块的实操细节与实现要点3.1 规划层ReAct 模式与 Prompt 设计实操规划层最经典的落地方式是 ReAct 模式。这个模式的核心思想特别简单模仿人类解决问题的思维过程先“想”再“做”再“看结果”循环往复直到任务完成。落实到代码层面就是构造一个系统 Prompt明确告诉模型你可以使用以下工具工具的说明和参数如下附工具清单。你必须以指定格式输出先写Thought:分析当前情况再写Action:决定要调用哪个工具Action Input:是传给工具的 JSON 参数。每次工具执行后你会收到Observation:开头的执行结果。当你认为任务完成时输出Final Answer:加最终回复。我在实际项目中踩过一个大坑如果 Prompt 里对输出格式的约束写得不够死模型会自作主张地“省略思考过程”或“混合输出”导致解析器经常拿到残缺内容。后来我换了一种更稳妥的约束方式——用 JSON 表单约束整个输出让模型严格输出{thought: ..., action: ..., action_input: {...}}这样的结构宁可牺牲一点可读性也要保证程序能稳定解析。这项改进让我的 Agent 任务成功率从不到 60% 提升到 85% 以上。对于支持原生 Function Calling 的模型如 GPT-4o、Claude 3.5 Sonnet更推荐的方案是直接走原生工具调用能力。你只需要把工具列表以 JSON Schema 的形式传给模型模型返回值里会有一个结构化的tool_calls字段直接解析这个字段就行根本不需要跟模型费口舌约定输出格式。代码长这样response client.chat.completions.create( modelgpt-4o, messagesmessages, toolstool_schemas, # 工具列表的JSON Schema tool_choiceauto, ) # 结果直接从response.choices[0].message.tool_calls里取3.2 执行层工具注册机制与参数校验工具注册是整个框架里代码量不大但设计感最强的部分。我见过很多项目用一个大字典维护工具名到函数的映射但这有两个问题一是工具描述、参数 Schema 和函数本体散落各处加一个新工具要改几个地方二是参数校验全靠手工写容易漏。推荐的做法是用 Python 装饰器加函数签名推导。你可以定义一个tool装饰器自动读取函数的__name__、__doc__和类型注解生成 OpenAI Function Calling 所需的参数 Schemadef tool(func): # 从函数签名生成参数schema sig inspect.signature(func) properties {} required [] for name, param in sig.parameters.items(): properties[name] {type: string, description: param.annotation or } if param.default is inspect.Parameter.empty: required.append(name) tool_def { type: function, function: { name: func.__name__, description: func.__doc__ or , parameters: { type: object, properties: properties, required: required, }, }, } registry[func.__name__] func tool_defs.append(tool_def) return func用装饰器还有一个隐藏好处函数注解本身就是参数说明写工具的时候顺手就把文档写了代码层强迫你保持注释完整。我在工具函数里都会要求至少写清三件事这个工具解决什么问题、每个参数的具体含义和单位、可能的异常行为。执行层的另一个关键点是超时控制。大模型可能会生成一个调用外部 API 的工具请求但那个 API 可能很慢甚至永远不返回如果不设超时整个 Agent 就被卡死了。我通常在工具执行外层包一层functools.partialThreadPoolExecutor的带超时执行器超时直接返回“工具执行超时”作为 Observation让模型决定是重试还是换方式from concurrent.futures import ThreadPoolExecutor, TimeoutError def run_with_timeout(func, timeout10, **kwargs): with ThreadPoolExecutor(max_workers1) as executor: future executor.submit(func, **kwargs) try: return future.result(timeouttimeout) except TimeoutError: return Error: 工具调用超时请检查参数或改换其他工具3.3 记忆层短期上下文、长期记忆与向量检索记忆层是我认为整个 Agent 里最“反直觉”的模块。刚入门时我觉得记忆不就是把聊天记录存个列表吗直到我发现上下文窗口爆了、多轮之前的信息被截断、不同任务的知识互相干扰才明白这里面的门道。短期记忆相对简单维护一个消息列表每次对话把新消息追加进去超过一定长度就做截断或摘要。我常用的策略是滑动窗口保留最近 N 条消息 把更早的消息用 LLM 总结成一段“压缩摘要”塞进系统 Prompt。这样既保留了关键信息又控制住 token 消耗。长期记忆则要处理“Agent 需要跨会话记住用户偏好或事实”的场景。比如用户说过“我常驻上海关注天气偏好湿冷提醒”下个会话还希望 Agent 记得这就得把这类信息抽出来存到外部存储里。主流的做法是用 Embedding 模型把文本向量化存进向量数据库Chroma、Milvus 都行需要时根据当前问题做相似度检索召回相关记忆再注入 Prompt。实操中我建议先想清楚一个问题你的 Agent 真的需要长期记忆吗如果只是做单个任务执行长期记忆反而会引入噪声——陈旧信息可能误导模型。我的原则是任务型 Agent 默认不带长期记忆只有明确要做“个人助理”类产品时才引入向量检索。别为了炫技加复杂度这是很多开源项目翻车的根源。3.4 工具与模型的双向适配Provider 抽象层设计最后一块核心是 Provider 抽象。Agent 框架要做得通用就不能绑定某一家模型。我的设计是这样定义一个LLMProvider基类规定chat(messages, tools)和parse_tool_calls(response)两个核心方法然后分别实现OpenAIProvider、AnthropicProvider、OllamaProvider。上层 Agent 只依赖这个基类完全不感知具体模型差异class LLMProvider(ABC): abstractmethod def chat(self, messages, toolsNone) - LLMResponse: ... abstractmethod def parse_tool_calls(self, response) - list[ToolCall]: ...这个抽象最大的价值在于当你发现某家模型的 Function Calling 效果不好时可以单独为它写一套解析逻辑甚至退化为“让模型输出 JSON 再解析”的模式而不影响其他模型。我在给一个项目接入国产开源模型时就遇到了这种情况——模型官方不支持 Function Calling但是通过精心设计 Prompt 让它稳定输出 JSON 一样能用。Provider 这层的存在让这种“妥协”变成了局部修改而不是整体返工。4. 实操过程从零搭建一个可用的 Hermes Agent4.1 环境准备与依赖安装在开始写代码之前先把环境搭好。我建议用 Python 3.10 以上版本虚拟环境隔离是必须的。核心依赖其实不多openai或对应模型的 SDK、rich终端输出美化、typer命令行接口就够了。如果你要用向量记忆再加chromadb。不需要一上来就装 LangChain 全家桶Agent 框架的核心逻辑自己写一遍远比调包更能让你理解原理。python -m venv .venv source .venv/bin/activate pip install openai rich typer chromadb环境变量方面需要设置对应的 API Key。我一般习惯在项目根目录放一个.env文件用python-dotenv加载。注意把.env加进.gitignore这个坑我踩过——没过滤环境变量文件就直接推到 GitHub十分钟后收到一条陌生人的邮件告诉我你的 API Key 被别人刷爆了别问我是怎么知道的。4.2 核心 Agent 循环的代码实现与讲解Agent 的主循环是整个项目的发动机。我给出一个最精简但功能完整的实现这个版本我实测能在 200 行以内跑通“多工具调用 多轮思考”的完整流程class Agent: def __init__(self, provider: LLMProvider, tools: list[Tool]): self.provider provider self.tool_map {t.name: t for t in tools} self.tool_schemas [t.to_schema() for t in tools] self.messages [{role: system, content: SYSTEM_PROMPT}] def run(self, user_input: str, max_steps: int 10): self.messages.append({role: user, content: user_input}) for step in range(max_steps): response self.provider.chat(self.messages, toolsself.tool_schemas) tool_calls self.provider.parse_tool_calls(response) if not tool_calls: # 没有工具调用说明要给出最终回答 self.messages.append({role: assistant, content: response.content}) return response.content # 先把模型的工具调用意图记录到消息里 self.messages.append({role: assistant, content: response.content, tool_calls: [ {id: c.id, type: function, function: {name: c.name, arguments: json.dumps(c.arguments)}} for c in tool_calls ]}) # 执行每个工具调用并把结果回填 for call in tool_calls: result self.execute_tool(call.name, call.arguments) self.messages.append({ role: tool, tool_call_id: call.id, content: result, }) return 已达到最大步骤数任务可能未完成请尝试更明确地描述需求。 def execute_tool(self, name: str, arguments: dict): if name not in self.tool_map: return fError: 未知工具 {name} try: return str(self.tool_map[name].func(**arguments)) except Exception as e: return fError: 工具执行异常: {e}这个循环的逻辑不复杂关键点在于消息序列的拼接规则模型发出的每条助手消息里附带tool_calls然后每条工具执行结果必须用role: tool的消息紧跟其后且需要用tool_call_id关联到对应的调用。很多初学者在这一步出错要么忘了把tool_calls附到历史消息里要么工具结果的 role 写错了导致模型看到的历史不完整决策质量直线下降。主循环里还藏着一个经验值max_steps默认设 10实际大多数任务在 3-5 步内就能完成。如果模型频繁达到步数上限问题通常不在步数而在工具设计——工具太粗或太细都会导致多轮无效调用。这个后面会细说。4.3 典型工具函数编写示例工具的质量直接决定 Agent 的天花板。我拿两个最常见也最容易出错的例子来说明怎么写好工具函数。第一个是“当前时间工具”。看起来简单但新手容易忽略时区问题。用户说“帮我看看今天的日程”Agent 调用时间工具拿到的是 UTC 时间跟本地时间差了 8 个小时所有后续逻辑全崩。我的写法是明确返回带时区的 ISO 格式并在描述里写清楚tool def get_current_time(timezone: str Asia/Shanghai): 获取指定时区的当前时间用于所有需要时间判断的场景。 Args: timezone: IANA时区名称默认Asia/Shanghai。 from datetime import datetime from zoneinfo import ZoneInfo return datetime.now(ZoneInfo(timezone)).isoformat()第二个是“网页内容抓取工具”。它展示了一个更复杂的情况工具执行结果可能太长直接回填会撑爆上下文窗口。解决思路是在工具内部做总结提炼只把关键信息返回给模型。比如抓取网页后先用 BeautifulSoup 抽取正文再截断前 2000 字符让模型基于摘要做判断tool def fetch_webpage(url: str, max_chars: int 2000): 抓取指定URL的网页正文内容返回截断后的文本。 Args: url: 完整网页地址必须包含协议头。 max_chars: 返回文本的最大字符数。 import requests from bs4 import BeautifulSoup resp requests.get(url, timeout15, headers{User-Agent: Mozilla/5.0}) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) for tag in soup([script, style, nav, footer]): tag.decompose() text .join(soup.stripped_strings) return text[:max_chars]写工具函数的核心原则我把它们总结成一条工具返回的内容不是给人看的而是给模型看的。所以返回结果要尽量结构化、去掉噪声、控制长度。很多时候我会让工具直接返回 JSON 字符串而不是长文本模型理解结构化数据远比理解散乱文本要准。4.4 运行效果与任务示例环境配置好、代码写完最兴奋的就是跑通第一个完整任务了。我在本地跑过的一个典型案例是用户输入“帮我查一下明天的天气如果下雨就提醒我带伞顺便把今天的日期告诉我。”Agent 的执行轨迹如下模型先思考需要获取今天日期和明天天气 → 调用get_current_time获取当前时间。工具返回2025-02-20T14:30:0008:00模型识别出今天是 2 月 20 日明天是 2 月 21 日。模型调用get_weather工具参数为{city: 上海, date: 2025-02-21}城市默认值来自对话历史。工具返回{date: 2025-02-21, city: 上海, weather: 小雨, temperature: 8-12℃}。模型看到下雨根据系统 Prompt 里的提醒规则给出最终回答“明天上海有小雨气温 8-12℃记得出门带伞。今天是 2 月 20 日。”整个流程 3 步搞定全程不需要人工干预。第一次跑通这个任务的时候我是真心觉得 Agent 这玩意儿确实能干活不是玩具。5. 常见问题与排查技巧实录5.1 上下文窗口溢出与截断策略这是 Agent 开发中最容易遇到的硬问题。模型上下文窗口有限即使现在 GPT-4o 有 128K但越长延迟越高、费用越贵而 Agent 每执行一步工具调用就会往消息列表里塞一大段内容。如果是网页抓取、日志分析类的工具几轮下来上下文就爆了。我常用的解决方案有三级。第一级是前置控制工具返回结果时强制截断控制在合理长度。第二级是滑动窗口只保留最近 N 轮完整消息更早的历史用摘要替代。第三级是任务抽屉将整个历史操作记录异步写入文件或数据库当 Agent 认为需要回溯时再通过检索工具主动读取。三级配合使用后我在一个数据采集任务里跑了 20 多步都没再爆过上下文。还有一个容易忽略的细节system消息里的工具列表本身就很占 token。工具描述写得越长可用上下文就越少。我曾经注册了 15 个工具光工具定义就占了两千多 token。后来我把工具描述精简每个工具控制在两句话以内参数说明尽量用简短的字段描述整体占用降了一半。5.2 模型频繁调用错误参数时的兜底策略Agent 区别于普通程序的一个痛点就是模型的输出不稳定工具参数经常给错。明明你的天气工具要求传city模型偏偏传一个location明明城市是字符串模型传了一个数组。这时候如果你直接抛异常Agent 就断在这一步了。我的经验是三层兜底。第一层是在工具执行器里做轻量参数清洗比对函数签名忽略未知参数对缺失的必填参数尝试从对话历史里找默认值。第二层是把参数校验错误包装成“友好错误信息”作为 Observation 返回给模型让模型自己看着办。第三层是在系统 Prompt 里强调“如果工具的输入参数不明确请先用 get_current_time 等基本信息工具确认上下文不要猜测”这种规则性约束。这里有一个我实测很有效的技巧故意在工具描述里写明参数格式示例。比如city: str - 城市名例如北京、上海模型看到示例后犯错率会显著下降。LLM 对具体示例的理解远比对抽象描述的把握要好。5.3 多轮对话中的“幻觉行动”这是 Agent 最隐蔽的坑模型在没有足够依据的情况下编造工具调用结果或者是“自问自答”而不实际调用工具。我遇到过最离谱的一次是模型在没有调用天气工具的情况下直接给出了“上海明天晴24℃”的回答。这个数字完全是它编的。排查这个问题我采取的策略是双管齐下。一方面在 Prompt 里硬性约束“除非你明确调用了工具并获得 Observation 反馈否则严禁在最终回答中包含具体的实时数据如果你没有获取到数据请如实说明无法回答。”另一方面在代码层面检测如果模型给出的最终回答里提及了某个工具的结果字段但历史消息里没有对应的工具执行记录则强制把回答打回重来。代码层面的校验逻辑不复杂但能堵住一大半幻觉def validate_answer(answer: str, messages: list) - bool: # 简单策略如果答案中包含℃、晴等天气关键词 # 但消息列表里没有tool执行记录就判定为可疑 weather_keywords [℃, 晴, 雨, 温度] has_tool_call any(msg.get(role) tool for msg in messages) if any(kw in answer for kw in weather_keywords) and not has_tool_call: return False return True5.4 调试 Agent 的有效方法调试 Agent 和调试普通程序完全是两回事。普通程序有明确的堆栈和执行路径Agent 的决策过程是模型“黑盒”产出的根本没法单步跟踪。我总结了几个经验第一日志里一定要打印完整消息序列。每次工具调用前后把 messages 列表原样输出到文件里。模型说什么、工具返回什么、上下文长什么样一眼就能看出来。很多诡异问题比如模型“忘记了”之前的指令都是看了完整日志才定位到原因的。第二准备一套标准测试用例。我维护了一个test_cases列表里面有简单的单工具调用、复杂的多工具协同、需要拒绝回答的越界请求等十几条用例。每次改完 Prompt 或工具代码先跑一遍回归测试看哪些用例效果变差。这个习惯帮我避免了很多次“改好一个 bug 弄坏一个功能”的窘境。第三善用“最小复现”思路。如果某个任务一直失败就把工具数量减少到只保留必要的一两个Prompt 精简到最短看看能不能复现问题。如果简化后问题消失了说明是上下文太复杂导致的干扰如果问题还在说明是核心逻辑的 bug。靠这个办法我定位过好几个“看起来像是模型问题其实是工具设计问题”的案例。6. 从能用走向好用个人经验与扩展方向项目跑通基本功能后你会很快意识到“能用”和“好用”之间隔着一条巨大的鸿沟。我自己的经历是第一版 Agent 能跑通简单任务时信心满满但一放到稍微复杂、稍微真实的场景里就各种掉链子几乎每走一步都要填坑。我觉得最值得投入的三个方向是工具质量的重构、评估体系的建设、以及任务编排的优化。工具质量决定了 Agent 能力的上限——工具又多又杂反而让模型选择困难应该把高频工具做细、低频工具聚合。评估体系能让你从“感觉变好了”变成“客观知道变好了”——记录每次任务的成功率、步数、token 消耗改版前后对比数据说话。任务编排则是从更宏观的层面优化一个大任务先拆成子任务为每个子任务设计专用的 Prompt 和工具子集而不是让 Agent 在几十个工具里大海捞针。如果你打算把hermes-agent推向生产环境我个人最想提醒的一点是永远给 Agent 留一个“人类确认”的出口。大模型自主执行任务确实酷但涉及发送邮件、删除文件、修改数据库这类不可逆操作时强制加入一个人工审批环节会让整个系统的可用性和安全性上一个台阶。我在execute_tool里加了一个needs_confirmation标记工具定义时声明需要确认的操作执行前会打印详情并等待用户输入 y/n。这个小改动成本极低但实操中救了我好多次。我在实际把玩这一类项目时最深的一个体会是Agent 框架本身的技术难点其实不难攻克最难的是理解模型的能力边界并围绕这个边界设计合理的工具与流程。没有哪个大模型是百分百可靠的Agent 架构的真正价值在于通过工具封装、反馈校验、人工兜底这三道防线把模型不可靠的影响压缩到最小让整个系统整体上变得可用。这个“系统性设计”的思维方式才是玩这类项目最珍贵的收获。