从零手写ReAct Agent:深入解析Tool Calling与循环机制

1. 从一次工具调用失败说起:为什么要手写ReAct Agent

去年冬天,我在调试一个自动化任务系统时遇到了一个很典型的问题:模型明明知道该调用哪个工具,参数也传对了,但整个流程就是跑不通。日志里显示工具被调用了,返回结果也拿到了,可模型下一轮却像失忆一样,完全忽略了这个结果,继续重复调用同一个工具。当时我第一反应是框架的bug,翻了两天源码才发现,问题出在我对Tool Calling的理解上——我一直以为它是模型的原生能力,实际上它只是ReAct循环里的一环,而循环的终止条件、观察结果的注入方式、消息历史的组织格式,每一个细节都会影响最终行为。

这件事之后,我决定抛开所有现成的Agent框架,从零手写一个ReAct Agent。不是为了造轮子,而是想搞清楚三件事:Tool Calling到底是怎么被触发的?观察结果是怎么回到模型上下文里的?循环什么时候该停?这三个问题不搞清楚,用再高级的框架也只是在调参碰运气。

这篇文章适合两类人:一类是正在用Agent框架但总觉得“不太可控”的开发者,另一类是好奇ReAct和Tool Calling底层机制的技术爱好者。我会从最核心的循环结构讲起,然后拆解工具调用的触发逻辑,接着讲观察结果的注入方式,最后分享我在手写过程中踩过的几个坑。全程不依赖任何Agent框架,只用最基础的模型调用接口,代码逻辑用伪代码和Python片段说明,你可以直接照着复现。

提示:本文假设你已经了解基本的对话模型调用方式,知道什么是system prompt和user message。如果这些概念还不太清楚,建议先补一下基础再往下看。

2. ReAct循环的骨架:Thought、Action、Observation三段式到底怎么跑

2.1 为什么是这三段,而不是别的结构

ReAct这个名字拆开就是Reasoning和Acting,核心思想是让模型在行动之前先“想一下”。这个“想”不是随便想,而是有明确格式约束的推理过程。我最初以为Thought只是一个可选的装饰,后来发现它承担了一个非常实际的功能:给模型一个显式的推理空间,让它在决定调用哪个工具之前,先把当前状态和下一步目标对齐。

如果没有Thought,模型直接输出Action,会发生什么?我实测过,在工具数量超过5个的场景下,模型选错工具的概率会明显上升。原因很简单:模型需要在一次前向传播中同时完成“理解当前状态”和“选择工具”两个任务,而Thought相当于把第一个任务单独拎出来,让模型先输出一段自然语言推理,再基于这段推理做选择。这就像你让人直接报答案,他可能蒙;但让他先写个解题思路再报答案,准确率就上来了。

Observation是工具执行后的返回结果。这里有个容易忽略的点:Observation不是随便塞回去就行,它的格式和位置会直接影响模型下一轮的推理。我试过把Observation放在assistant消息里,也试过单独作为一条user消息,效果完全不同。后面会详细讲这个坑。

三段式的循环逻辑用伪代码表示大概是这样:

while not done: thought = model.generate(context + "Thought:") action = parse_action(thought) if action.type == "final_answer": done = True break observation = execute_tool(action) context += f"Thought: {thought}\nAction: {action}\nObservation: {observation}\n"

看起来很简单,但每一行都有讲究。parse_action怎么解析?execute_tool的异常怎么处理?context怎么拼接才不会让模型困惑?这些细节才是真正决定Agent能不能跑通的关键。

2.2 循环终止条件:模型说“我完成了”就真的完成了吗

终止条件是我踩的第一个大坑。最初我的设计是:模型输出一个特殊的Action类型叫finish,我就认为任务结束。听起来很合理对吧?但实际跑起来发现,模型有时候会在信息还不完整的时候就输出finish,因为它觉得“差不多够了”。更麻烦的是,有时候模型会陷入死循环,反复调用同一个工具,每次都输出finish但又不给出最终答案。

后来我改成了双重判断:模型必须输出finish并且附带一个非空的最终答案,同时循环次数不能超过预设上限。上限这个事很关键,我一般设8到10轮,超过就强制终止并返回当前最好的结果。为什么是8到10?因为实测下来,大多数任务在5轮以内就能完成,超过8轮的基本都是模型卡住了,再跑下去只是浪费token。

还有一个细节:终止时要把完整的消息历史返回给调用方,而不是只返回最终答案。因为调试的时候你需要看到中间每一步的Thought和Observation,不然出了问题根本不知道是哪一步歪了。

2.3 消息历史的组织方式:为什么不能简单拼接字符串

我一开始图省事,直接把Thought、Action、Observation拼成一个长字符串塞进context。跑简单任务没问题,但任务一复杂就出问题:模型会分不清哪些是它自己说的话,哪些是工具返回的结果。有一次它把Observation里的内容当成了自己的Thought,然后在下一轮里说“根据我之前的推理……”,实际上那段推理是工具返回的JSON。

正确的做法是用结构化的消息列表,每条消息有明确的role。我的做法是:

  • system message:放工具定义和格式要求
  • user message:放原始任务描述
  • assistant message:放Thought和Action
  • user message:放Observation(注意这里用user角色,不是assistant)

为什么Observation要用user角色?因为从模型的角度看,Observation是“外部世界给它的反馈”,而不是它自己生成的内容。用assistant角色会让模型误以为这是它自己的输出,从而在后续推理中产生混淆。这个细节我在三四个不同的模型上都验证过,用user角色明显更稳定。

3. Tool Calling的本质:模型到底是怎么“选择”工具的

3.1 工具定义不是越多越好,关键是描述的质量

很多人以为工具定义就是把函数签名翻译成JSON Schema,参数名、类型、描述填上就行。我一开始也这么想,直到发现模型经常选错工具。后来我把工具描述改了一版,选对率从大概六成提升到了九成以上。改了什么?主要是三点:

第一,描述里要写清楚“什么时候用这个工具”,而不只是“这个工具是什么”。比如一个搜索工具,不要只写“搜索互联网”,要写“当需要获取实时信息、最新数据或你不确定的事实性内容时使用此工具”。这样模型在犹豫要不要搜索的时候,就有了明确的判断依据。

第二,参数描述要给出示例值。比如query参数,不要只写“搜索关键词”,要写“搜索关键词,例如‘2024年新能源汽车销量’”。模型看到具体示例后,生成参数时会更准确,尤其是涉及日期格式、单位这种容易出错的参数。

第三,工具数量控制在7个以内。我试过给模型15个工具,结果它经常在相近的工具之间反复横跳。后来精简到5个核心工具,准确率明显提升。如果确实需要很多工具,可以考虑分组,或者用两级选择:先让模型选工具类别,再选具体工具。

3.2 Action的解析:为什么正则比JSON解析更可靠

模型输出的Action格式,我试过两种方案:一种是要求模型输出JSON,然后用json.loads解析;另一种是要求模型输出特定格式的文本,然后用正则提取。实测下来,正则方案在稳定性上完胜JSON方案。

原因在于,模型生成JSON时经常出现一些微妙的问题:多一个逗号、少一个引号、字符串里有没有转义的反斜杠。这些问题在人类看来很小,但json.loads会直接抛异常。而正则方案对格式的容忍度更高,只要关键字段能提取出来就行。

我的Action格式是这样的:

Action: tool_name Action Input: {"param1": "value1", "param2": "value2"}

解析时先用正则匹配Action:\s*(\w+)提取工具名,再用正则匹配Action Input:\s*(\{.*?\})提取参数JSON。如果JSON解析失败,就尝试用更宽松的方式提取键值对。这样即使模型输出格式有点小瑕疵,也能正常执行。

注意:正则里的.*?要用非贪婪模式,否则遇到多个Action时会匹配到最后一个,导致解析错误。这个坑我踩过,调试了半天才发现是正则贪婪模式的问题。

3.3 工具执行的安全边界:不要让模型直接操作危险接口

手写Agent最大的好处是你完全控制工具执行层。我在工具执行外面包了一层安全检查,主要做三件事:

  • 参数校验:检查必填参数是否存在、类型是否正确、值是否在允许范围内。比如一个删除文件的工具,路径参数必须限定在特定目录下。
  • 超时控制:每个工具调用设置最大执行时间,超过就强制中断并返回超时错误。没有这个的话,一个卡住的工具会让整个Agent挂起。
  • 异常捕获:工具执行抛出的任何异常都要被捕获,转换成模型能理解的错误信息返回。不要让异常直接冒泡到循环外面,否则Agent会直接崩溃。

错误信息的格式也很重要。不要只返回“执行失败”,要返回具体的失败原因,比如“参数date格式错误,期望YYYY-MM-DD,实际收到‘2024/01/01’”。这样模型下一轮就能根据错误信息修正参数,而不是盲目重试。

4. 观察结果的注入:为什么你的Agent总是“忘记”上一步做了什么

4.1 Observation的格式设计:结构化比自然语言更有效

我最初把工具返回的原始结果直接塞回context,比如一个搜索工具返回一大段网页文本。结果模型经常抓不住重点,要么忽略关键信息,要么被无关内容带偏。后来我改成工具返回结构化数据,再由一个格式化层转成简洁的Observation。

举个例子,搜索工具返回的是JSON数组,每条包含标题、摘要、链接。我不会把整个JSON塞回去,而是格式化成:

Observation: 找到3条相关结果: 1. 标题A - 摘要A 2. 标题B - 摘要B 3. 标题C - 摘要C

这样模型一眼就能看到关键信息,不会被JSON的括号和引号干扰。如果结果太长,我会截断到前500个字符,并在末尾加“...(结果已截断)”。截断这个事很关键,不截断的话context会迅速膨胀,不仅浪费token,还会让模型注意力分散。

4.2 多轮观察的累积:什么时候该丢弃历史

ReAct循环跑多了之后,context里会堆积大量的Thought和Observation。我实测发现,超过6轮之后,模型对早期Observation的回忆准确率会明显下降。这不是模型的问题,而是注意力机制本身的限制——context越长,每个token分到的注意力越少。

我的处理策略是:保留最近3轮的完整交互,更早的Observation只保留摘要。摘要怎么生成?简单粗暴的方式是只保留每条Observation的第一句话,复杂一点的方式是让模型自己总结。我一般用第一种,因为够用且不额外消耗token。

还有一种情况需要特殊处理:如果某一轮的工具调用失败了,那条Observation要保留完整的错误信息,不能摘要。因为错误信息是模型修正行为的依据,摘要后可能丢失关键细节。

4.3 观察结果与推理的衔接:如何让模型“接得上”上一轮

这是我最开始没注意到的问题:模型有时候会忽略上一轮的Observation,直接开始新的推理。比如上一轮搜索返回了结果,它下一轮却说“我需要搜索一下”。后来我在system prompt里加了一句话:“在每一轮Thought开始时,先总结上一轮Observation的关键信息,再决定下一步行动。”这句话加进去之后,模型“接不上”的情况减少了大概七成。

另一个技巧是在Observation末尾加一个引导词,比如“基于以上结果,下一步应该:”。这相当于给模型一个软性的格式提示,让它自然地从观察过渡到推理。不要小看这种引导词,在模型行为控制上,格式提示往往比内容提示更有效。

5. 手写过程中踩过的五个坑:每一个都让我重新理解了Agent

5.1 坑一:system prompt里工具定义和格式要求打架

我一开始把工具定义和输出格式要求都放在system prompt里,结果模型经常混淆:要么把工具定义当成输出格式的一部分,要么在Action里输出工具定义的原文。后来我把它们分开:工具定义放在system prompt的开头,用明确的标记隔开,比如<tools>和</tools>;格式要求放在system prompt的末尾,用<format>和</format>包裹。这样模型能清楚地区分两部分内容。

5.2 坑二:模型输出带前缀的Action,正则匹配不到

有些模型喜欢在Action前面加“好的,我将调用”之类的自然语言。我的正则一开始只匹配行首的Action:,结果这些带前缀的输出全部解析失败。后来我把正则改成Action:\s*(\w+),不加行首锚定,同时在解析前先做一次清洗,把常见的自然语言前缀去掉。这个坑让我意识到,永远不要假设模型会严格按照你要求的格式输出,解析层必须有一定的容错能力。

5.3 坑三:工具返回空结果时模型陷入死循环

有一次搜索工具返回了空数组,模型收到Observation: 未找到相关结果之后,下一轮又调用了同一个搜索工具,参数一模一样。然后又是空结果,又调用,循环了五六次才因为达到上限而终止。后来我在Observation里加了引导:“未找到相关结果,建议尝试其他关键词或使用其他工具。”这样模型就知道该换策略了,而不是傻傻重试。

5.4 坑四:多工具并行调用时的顺序依赖

有些任务需要先调用工具A,拿到结果后再调用工具B。但模型有时候会同时输出两个Action,期望并行执行。如果B依赖A的结果,并行就会出错。我的处理方式是:在system prompt里明确说明“每次只能调用一个工具,等待Observation后再决定下一步”。虽然牺牲了并行效率,但换来了稳定性。如果确实需要并行,可以在工具层做依赖检查,发现依赖未满足就返回错误让模型重新规划。

5.5 坑五:最终答案的格式与中间步骤混淆

模型有时候会把最终答案也包装成Action格式,比如Action: final_answer,但内容却是空的。或者反过来,在应该输出Action的时候直接给出了最终答案。我的解决方案是在system prompt里用非常明确的格式说明:

如果你已经可以回答用户的问题,输出: Final Answer: [你的答案] 如果你需要调用工具,输出: Action: [工具名] Action Input: [参数JSON]

同时解析层要能区分这两种情况。我一般先检查是否包含Final Answer:,如果有就直接提取答案并终止循环;否则再尝试解析Action。

6. 从手写Agent到理解框架:为什么自己实现一遍这么重要

手写这个ReAct Agent花了我大概三个周末的时间,代码量不到500行,但收获远超之前用框架调参的几个月。最大的体会是:框架帮你屏蔽了细节,但也屏蔽了你对问题的理解。当你自己实现一遍之后,再回头看那些框架的文档和源码,会有一种“原来如此”的感觉。

比如LangChain的AgentExecutor,之前我一直搞不懂它的max_iterations和early_stopping_method到底怎么配合。自己写完循环之后才明白,max_iterations就是循环上限,early_stopping_method决定达到上限后是返回错误还是返回当前最好的结果。这些概念在手写过程中变得非常具体,不再是抽象的配置项。

另一个收获是对模型行为的理解。手写过程中你会被迫思考:模型为什么会输出这个格式?为什么它会忽略Observation?为什么它会在工具选择上犹豫?这些问题的答案,在框架的使用文档里是找不到的,只有自己一步步调试才能体会。

如果你也在用Agent框架,我强烈建议你抽时间手写一个最小实现。不需要支持很多工具,不需要复杂的错误处理,只要能跑通“推理-行动-观察”的完整循环就行。写完之后,你对Tool Calling和ReAct的理解会上一个台阶,再用框架的时候也会更有掌控感。

最后分享一个我在调试时常用的小技巧:在每一轮循环结束时,把完整的context打印到日志里,包括system prompt、每一条消息的role和content。这样当Agent行为异常时,你可以直接看到模型实际收到的输入是什么,而不是靠猜。这个习惯帮我省了很多调试时间,也让我发现了不少消息格式上的问题。

相关新闻

医疗收据与处方单识别:基于多模态大模型的模糊手写体与药品明细结构化提取

医疗收据与处方单识别:基于多模态大模型的模糊手写体与药品明细结构化提取

上个月我帮一家做北美宠物医疗保险与跨境就医理赔的 SaaS 团队重构票据录入系统。在压测那天晚上&#xff0c;我家十四斤重的橘猫 Bug 趁我起身热牛奶&#xff0c;一屁股坐在了桌面高速扫描仪的触控板上&#xff0c;伴随着幽幽的绿光扫出了一张带着梅花肉垫爪印的空白页&#x…

2026/10/20 19:55:53 阅读更多 →
基于YOLOv8的手机屏幕缺陷检测系统:图像/视频/摄像头实时检测实战

基于YOLOv8的手机屏幕缺陷检测系统:图像/视频/摄像头实时检测实战

做工业检测项目的时间长了&#xff0c;你会发现手机屏幕质检是个特别典型的场景。产线上一天要过几千块屏&#xff0c;划伤、亮点、暗斑、崩边这些缺陷全靠人眼盯&#xff0c;师傅看久了眼睛吃力&#xff0c;漏检率还不好控。我接触过不少做视觉的同行&#xff0c;都在琢磨怎么…

2026/10/20 19:55:53 阅读更多 →
AI原生开发重塑企业IT:一篇博客引发的传统巨头股价暴跌启示

AI原生开发重塑企业IT:一篇博客引发的传统巨头股价暴跌启示

1. 现象回顾&#xff1a;一篇博客为何引发巨震先把这个事件放回时间线里看。某模型研究机构在博客上发布了一篇关于企业级AI应用开发模式的观察文章&#xff0c;内容本身并不长&#xff0c;核心观点是&#xff1a;基于大模型的原生开发方式正在快速取代传统的企业软件交付流程&…

2026/10/20 19:55:53 阅读更多 →

最新新闻

YOLO26目标检测训练到RK3588部署:ONNX与RKNN适配实践

YOLO26目标检测训练到RK3588部署:ONNX与RKNN适配实践

YOLO26 目标检测的训练到瑞芯微平台适配&#xff0c;这条链路我最近刚完整跑通一遍。先说结论&#xff1a;真正卡人的地方不在训练本身&#xff0c;而在从 PyTorch 模型走向 RKNN 运行时的那几步——ONNX 导出、算子兼容、INT8 量化&#xff0c;每一个都埋着不少雷。我是在给一…

2026/10/20 20:43:28 阅读更多 →
梯度增强PINN:攻克Allen-Cahn方程多陡峭界面难题

梯度增强PINN:攻克Allen-Cahn方程多陡峭界面难题

Allen-Cahn方程是我调过的最“拧巴”的PDE之一。它表达式极简&#xff0c;一个∂u/∂t ε∂u/∂x u - u&#xff0c;但解的画面完全不简单&#xff1a;能量驱动下的相分离会让解在空间里劈出好几条尖锐的界面&#xff0c;这些界面随时间演化、移动、合并。所谓“多非常陡峭区…

2026/10/20 20:43:28 阅读更多 →
计算机教材内容策划与写作:从概念到工程实践

计算机教材内容策划与写作:从概念到工程实践

抱歉&#xff0c;我无法处理这个请求。

2026/10/20 20:43:28 阅读更多 →
从图像到数据:HaoCurve曲线提取实战与避坑指南

从图像到数据:HaoCurve曲线提取实战与避坑指南

简介&#xff1a;HaoCurve&#xff08;薅曲线&#xff09;是一套面向科研人员、工程师与数据分析者的MATLAB曲线绘制工具源码&#xff0c;适合需要在MATLAB环境中快速生成、编辑和分析数据曲线图的用户&#xff0c;尤其对希望深入理解绘图脚本实现、进行二次开发的进阶使用者更…

2026/10/20 20:43:28 阅读更多 →
CLI编码代理失败过程解剖:从轨迹日志到根因分析

CLI编码代理失败过程解剖:从轨迹日志到根因分析

读到一篇很有意思的论文&#xff0c;标题叫《Failure as a Process: An Anatomy of CLI Coding Agent Trajectories》。这两年命令行AI编程工具越来越多&#xff0c;从最初的“能写一段函数”到现在能在真实仓库里自己定位问题、改代码、跑测试&#xff0c;甚至提交补丁。但大多…

2026/10/20 20:43:28 阅读更多 →
小橘子V4.790:本地可调试塔防策略模拟器

小橘子V4.790:本地可调试塔防策略模拟器

简介&#xff1a;本资源是《向僵尸开炮》游戏的自动化对局辅助工具——小橘子V4.790版本完整部署包&#xff0c;面向游戏AI开发者、自动化测试工程师及休闲游戏技术爱好者&#xff0c;用于快速实现人机对战环境下的策略执行、界面识别与操作模拟。压缩包共741个文件&#xff0c…

2026/10/20 20:42:25 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →