☰
本地Agent最小闭环:用Tool Trace与4张证据表实现离线回归
2026/10/3 10:56:38 网站建设 项目流程

最近有个朋友问我:网上Agent教程一堆,又是LangGraph又是CrewAI又是多Agent编排,看得头皮发麻,有没有一条初级选手也能走通的低门槛路线?我的回答很直接:先把最小闭环跑通,把注意力只放在Tool Trace和离线回归上,用4张证据表把行为钉死,之后再考虑花活。这套方法我自己在本地Agent的小项目里反复用过,从“能跑”到“敢改”,靠的就是每次运行留下证据。今天把字段设计、埋点代码、回归对比脚本和踩过的坑全部写出来,照着抄就能落地。

1. 先别碰框架:理清本地Agent最小闭环

1.1 Agent最小闭环 = 模型决策 + 工具执行 + 循环

Agent听起来高深,本质上就是一个带工具的循环程序。你问它“北京现在几点”,大模型本身不知道实时时间,它会返回一个工具调用请求,比如调用get_current_time(city="北京");你的程序执行完这个工具,再把结果回填给模型;模型拿到时间后组织出一句“北京时间14点30分”。这个过程反复进行,直到模型觉得不需要再调用工具,给出最终答案。循环就是Agent最核心的运行机制。

“本地Agent”里的“本地”,不一定指模型也必须跑在本机,而是指运行骨架、工具逻辑、数据记录都在你自己手里。你可以用Ollama跑7B模型,也可以连云端API,但所有中间过程都由本地代码控制。对初级来说,这是最好的学习方式:没有平台黑盒,每一步都能看日志。你暂时不需要纠结harness和agent区别,也不急着研究Agent框架与编排,先把一个手工循环跑通。

先手工写循环还有一个原因:主流框架帮你封装了状态流转,但也会把细节藏起来。初级直接上框架,碰到问题往往分不清是模型返回格式问题、工具函数问题还是框架状态问题。手工循环只要几十行,你完整看到每一次模型输出、每一次工具返回,踩坑时能准确定位。等理解透了,再去看LangGraph的callback机制,你会觉得“原来框架就是把我这段循环封了一下”。

1.2 为什么必须先做Tool Trace,而不是先选平台

Tool Trace,工具调用追踪,就是把“模型决定调用哪个工具、传了什么参数、工具返回了什么、有没有报错”按顺序记录下来。没有Trace,Agent就是个黑盒,整个调试全靠猜。举个真实例子:你改了一版Prompt之后,某个工具死活不调用了。没有Trace,你只能反复试Prompt;有Trace,你能看到上一版在第2步调用了search,这一版模型直接输出、没触发工具,问题范围瞬间缩小。

Trace同时也是离线回归的地基。离线回归需要把历史上“输入→工具调用序列→最终答案”录制下来,新版本改动后,用同一批输入跑一遍,再对比新旧trace是否一致。没有Trace,你只能肉眼比较两个对话,既不稳定也不可追溯。所以对Agent开发来说,埋点不是“以后再加”的加分项,而是第一步必须做的事。先有黑匣子,再谈优化。

1.3 4张证据表分别解决什么问题

我把证据分成4张表:会话证据表、工具调用证据表、判定证据表、回归证据表。会话表告诉你“这次请求花了多久、调了几次工具、最终是什么状态”;工具调用表是Trace核心,记录每一步工具调用的入参、出参、错误信息;判定表把“预期应该发生什么”写成可查记录,用来判断一次运行到底对不对;回归表负责在基线trace和新版本trace之间做差异记录,产出结论。这4张表正好覆盖三个问题:它干了什么、干得对不对、改完有没有变坏。

有人会问,为什么不直接上一套Agent评测平台?平台方案当然好,但对初级来说太重了,要写复杂用例、接数据库、出报表。4张表跑在本地SQLite里,零外部依赖,写两条SQL就能出对比结果。而且这套设计和重平台是同源的,以后真要上平台,只是把这几张表换成一个服务而已,思路完全一致。

2. 四张证据表怎么设计,直接抄作业

2.1 会话证据表:记录一次请求的完整生命周期

表名建议叫session_evidence。字段最少包含session_id、user_query、final_answer、model_name、tool_count、status、latency_ms、created_at。status是字符串枚举,三种取值:success表示正常完成,error表示工具调用抛出未捕获异常或最终答案生成失败,stopped表示超过最大循环次数被强制终止。

latency_ms一定要有,本地Agent迭代最关心的就是慢在哪。如果工具调用占了90%时间,就去优化工具;如果模型生成占了80%,就考虑换模型或精简Prompt。没有这个字段,你只能说“好像变慢了”,有了它就能精确到毫秒。model_name也值得记录,方便对比不同模型在同一任务上的行为差异,以后做模型选型时直接查表。

字段说明可以先做成这样:

字段类型说明
session_idTEXT PK会话唯一ID,由程序生成
user_queryTEXT用户原始输入
final_answerTEXTAgent最终文本回答
model_nameTEXT实际使用的模型名或模型函数名
tool_countINTEGER本次会话实际调用的工具次数
statusTEXTsuccess / error / stopped
latency_msINTEGER整个会话耗时
created_atTEXT写入时间,建议统一存UTC

一条示例数据大概是这样的:session_id="c7d2...", user_query="查询北京天气", final_answer="北京今天晴,23度", model_name="qwen2.5:7b", tool_count=2, status="success", latency_ms=8425, created_at="2026-05-16T02:00:00Z"。看到这条记录,你就知道这个任务调用了2次工具、整体8.4秒、状态正常,这是会话级证据。

2.2 工具调用证据表:Trace的核心账本

表名用tool_call_evidence。字段:trace_id、session_id、tool_name、input_json、output_json、order_index、duration_ms、error_message、created_at。input_json和output_json直接用TEXT存JSON字符串,不要拆成几十个业务字段,否则每新增一个工具都要改表结构,维护成本会线性上升。

order_index非常关键,代表该工具调用在本次会话中的执行顺序。模型在同一轮可能会返回多个工具调用,比如同时调get_weather和get_city_code,必须给它们分配不同序号,否则无法还原顺序。如果只记时间戳,同一毫秒内可能出现重复值,排序会乱。error_message为空表示成功,非空时记录异常信息的第一行,方便后期分类统计。

这里有个新手常问的点:工具调用记录是模型“请求调用”时保存,还是“执行完成”后保存?我的建议是保存“执行完成后”的结果。因为只有执行完才能拿到output和真实耗时。如果你还想记录模型“发起调用”的意图,可以加一个trace_type字段区分,但初级版本不必上,避免数据量翻倍。

2.3 判定证据表:把“好不好”变成可查记录

表名eval_evidence。字段:eval_id、session_id、check_item、expected、actual、passed、remark。跑完一个Agent后,光有trace不够,你还要判断这次跑得对不对。不要拍脑袋记在脑子里,把断言写入这张表。

check_item写的是判定项描述,比如“用户询问天气时,最终答案是否包含温度”;expected是预期值,比如“答案包含 '23'”;actual是实际结果,比如真实回答是“今天晴,23度”。程序判断实际值是否命中预期,写入passed。remark字段留给人补充说明,比如“第1个预期没命中,但原因是用户只问了天气没问温度,可以接受”。

这张表还有一个重要作用:积累回归用例。你可以把每条用例的判定项固定下来,每次跑完新版本自动执行同一批check_item。这样改代码后是否破坏了某个行为,不靠感觉,靠记录。判定表加会话表加工具调用表,三张表关联起来,就是最朴素的“可解释评估”。

2.4 回归证据表:离线对比的底账

表名regression_evidence。字段:regression_id、baseline_session_id、candidate_session_id、tool_consistency、answer_similarity、diff_summary、verdict、created_at。baseline是旧版本代码跑出来的基线trace,candidate是新版本代码跑出来的候选trace。

tool_consistency记录工具调用序列的比对结果,建议取值就三种:consistent表示完全一致,diff表示有差异但不致命,blocker表示阻断性差异。answer_similarity用0到1之间的小数记录最终答案的相似度,初筛够用。diff_summary用文本简要描述差异点,比如“第2步工具由search变成get_weather”。verdict是最终判定:pass、warn或fail。

离线回归的关键词是“离线”。基线入库后,你改代码跑候选版本,后续对比过程不再需要模型在线。如果每次都要重新让模型跑一遍,那不叫离线回归,那叫在线测试。保存基线这步,是新手最容易忽略的。在改代码之前,先用旧代码跑一遍用例集并留底,否则后面没有任何参照物。

2.5 设计原则:字段和存储方面少踩坑

几条经验直接说。第一,所有JSON字段用TEXT存储,不要用VARCHAR。VARCHAR有长度限制,工具输出动不动几千字,截断后你根本没法判断问题。第二,时间统一存UTC ISO字符串,避免不同时区导致排序混乱。第三,数据库用SQLite完全够用,不需要为初级项目上PostgreSQL,等有并发写入需求再迁移不迟。第四,给session_id和tool_name建索引,因为离线回归要频繁按这两个字段过滤。

允许“宽表”,不要过早优化。一张表多几个冗余字段,比做各种关联查询更利于初级阅读。例如工具调用表里冗余session_id、tool_name、order_index,方便直接对单表还原trace。我就曾经为了“专业”拆了5张明细表,最后自食其果,查一条调用链要join三次,初级阶段完全不值。表结构宽一点、查询简单一点,在这个阶段是优点。

3. Trace埋点实操:给Agent装上黑匣子

3.1 用装饰器统一记录工具调用

最简单的埋点方式就是写一个装饰器,把所有工具函数包一层:执行前记录入参,执行后记录出参和耗时,异常时记录错误信息。工具函数本身不用改业务代码,只加一层包装,侵入性极低。下面给出一段SQLite初始化代码和装饰器代码,只用Python标准库,复制就能跑。

import sqlite3 import json import time import uuid from functools import wraps DB_PATH = "agent_evidence.db" def get_conn(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_conn() conn.executescript(""" CREATE TABLE IF NOT EXISTS session_evidence ( session_id TEXT PRIMARY KEY, user_query TEXT, final_answer TEXT, model_name TEXT, tool_count INTEGER, status TEXT, latency_ms INTEGER, created_at TEXT ); CREATE TABLE IF NOT EXISTS tool_call_evidence ( trace_id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, tool_name TEXT, input_json TEXT, output_json TEXT, order_index INTEGER, duration_ms INTEGER, error_message TEXT, created_at TEXT ); CREATE TABLE IF NOT EXISTS eval_evidence ( eval_id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, check_item TEXT, expected TEXT, actual TEXT, passed INTEGER, remark TEXT ); CREATE TABLE IF NOT EXISTS regression_evidence ( regression_id INTEGER PRIMARY KEY AUTOINCREMENT, baseline_session_id TEXT, candidate_session_id TEXT, tool_consistency TEXT, answer_similarity REAL, diff_summary TEXT, verdict TEXT, created_at TEXT ); """) conn.commit() conn.close() def trace_tool(session_id, order_index): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): input_json = json.dumps( {"args": args, "kwargs": kwargs}, default=str, ensure_ascii=False ) start = time.time() error = None try: result = func(*args, **kwargs) output_json = json.dumps(result, default=str, ensure_ascii=False) except Exception as e: error = str(e) output_json = "null" raise finally: duration = int((time.time() - start) * 1000) conn = get_conn() conn.execute( """ INSERT INTO tool_call_evidence (session_id, tool_name, input_json, output_json, order_index, duration_ms, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( session_id, func.__name__, input_json, output_json, order_index, duration, error, time.strftime("%Y-%m-%dT%H:%M:%SZ") ) ) conn.commit() conn.close() return result return wrapper return decorator

注意装饰器里的order_index不能写死,要在主循环中动态传入。我们用trace_tool(session_id, order_index)生成一个新装饰器,再包装真正的工具函数。这样每个工具调用都能拿到独立的会话ID和顺序号。

3.2 在主循环里埋点:会话记录和停止条件

主循环负责调度模型和工具。它至少要做四件事:生成session_id,循环调用模型,遇到tool_calls就执行工具并回填结果,拿到最终答案后写会话表。必须设置max_steps限制,比如5次,防止模型陷入“调用工具→看到结果→再调用工具”的死循环。每次循环递增order_index,同一个模型响应里如果有多个工具调用,按数组下标顺序累加。

下面是最小主循环的示例,不绑定任何框架,只按OpenAI兼容的Chat接口格式处理,方便替换成Ollama、DeepSeek或其他本地网关。重点关注消息结构和order_index的传递。

def run_agent(user_query, model_fn, tools, max_steps=5): session_id = uuid.uuid4().hex start = time.time() messages = [{"role": "user", "content": user_query}] order = 0 final_answer = "" for step in range(max_steps): resp = model_fn(messages) msg = resp["message"] if not msg.get("tool_calls"): final_answer = msg.get("content", "") break messages.append(msg) for tc in msg["tool_calls"]: name = tc["function"]["name"] args = json.loads(tc["function"]["arguments"]) fn = tools[name] decorated = trace_tool(session_id, order)(fn) try: result = decorated(**args) except Exception as e: result = f"TOOL_ERROR: {e}" order += 1 messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": str(result) }) else: final_answer = "STOPPED: reached max steps" latency_ms = int((time.time() - start) * 1000) conn = get_conn() conn.execute( """ INSERT INTO session_evidence (session_id, user_query, final_answer, model_name, tool_count, status, latency_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( session_id, user_query, final_answer, model_fn.__name__, order, "success" if final_answer else "error", latency_ms, time.strftime("%Y-%m-%dT%H:%M:%SZ") ) ) conn.commit() conn.close() return session_id, final_answer

这里有个细节:工具执行异常时,我把异常信息拼到result里还给模型。这样模型能“看到”错误并自动调整,而不是整个会话崩溃。trace_tool中的finally仍然会把错误消息写进工具调用表,两边都不耽误,既留了证据,又给了模型自我修正的机会。

3.3 一个能直接跑的最小Agent骨架

下面给出最小Agent骨架。工具只有两个:get_current_time获取当前时间,calculate做四则运算。模型函数调用Ollama的/api/chat接口,机器上没装Ollama的话,换成其他OpenAI兼容接口也基本不需要改逻辑,只要返回结构里带tool_calls。

import requests OLLAMA_URL = "http://localhost:11434/api/chat" def call_ollama(messages): resp = requests.post( OLLAMA_URL, json={ "model": "qwen2.5:7b", "messages": messages, "stream": False, } ) resp.raise_for_status() return resp.json() def get_current_time(): import datetime return datetime.datetime.now().isoformat() def calculate(expression: str): # 仅演示,真实项目不要直接 eval return eval(expression) tools = { "get_current_time": get_current_time, "calculate": calculate, } if __name__ == "__main__": init_db() sid, answer = run_agent("帮我算一下 123*456 等于多少", call_ollama, tools) print(sid, answer)

重点提醒:calculate里用eval只是为了演示,真实环境绝对不能直接eval用户字符串,存在严重安全风险。比较稳妥的做法是用ast模块解析表达式,只允许数字和加减乘除,或者干脆用专门的表达式计算库。既然做Agent,工具执行的安全必须优先考虑,后面第5章还会展开。这个骨架跑通后,你可以往tools字典里加任意自定义函数,装饰器会自动记录Trace。

3.4 记录粒度与敏感信息处理

Trace粒度要克制。初级版本只记录“工具调用级别”,不要一开始就记录模型内部的token序列、logits、中间推理,否则表会迅速膨胀到几十MB,而且大部分信息用不上。等真要深入调试时,再加一张model_debug表,存储原始请求和响应报文,用session_id关联即可。

更重要的是脱敏。工具入参里可能包含用户隐私、API Key、数据库密码;工具出参可能包含内网地址、账单信息。写入证据表之前,先做一层脱敏:凡是字段名里包含key、token、password、secret的,值一律替换成"***"。如果只是本地单人开发,很多人会忽略,但一旦要把trace分享给同事或接入持续集成,就会泄露敏感信息。养成“入库前脱敏”的习惯,成本很低,收益很大。

4. 离线回归:用四张表证明“改了没坏”

4.1 离线回归的四步流程

离线回归的核心思路是“用旧trace当基准,比较新trace”。流程分四步。第一步,准备一个固定用例集,至少包含每个工具的正例和反例。第二步,用旧版本代码跑用例集,把session_evidence和tool_call_evidence入库,标记为baseline。第三步,修改代码,用同一用例集跑新版本,产生candidate trace。第四步,写脚本比较baseline和candidate的工具调用序列、参数、答案,把结果写入回归证据表。整个对比过程不需要模型在线。

为什么要强调“同一用例集”?因为只有输入相同,trace差异才能归因于代码变化。如果每次用例都不同,对比就是无效的。我习惯用cases.json文件固定用例,里面存用户输入和判定项。跑版本时循环读取cases.json,后续增加用例往文件里追加即可。这是最基础也最有效的回归资产管理方式,比在Excel里记几条强得多。

4.2 SQL直接对比工具调用轨迹

如果基线会话ID和候选会话ID已知,用SQL就能做几个快速对比。第一个是工具调用顺序是否一致:按order_index连接两张工具调用证据表,逐行比较tool_name。第二个是参数是否一致:先比input_json完全相等,如果不等再对JSON做键值排序后比较。第三个是异常是否新增:比较error_message从空变成非空的行。

-- 检查候选会话是否有缺失的工具调用 SELECT a.order_index, a.tool_name FROM tool_call_evidence a LEFT JOIN tool_call_evidence b ON b.session_id = 'candidate_session_id' AND a.order_index = b.order_index WHERE a.session_id = 'baseline_session_id' AND b.trace_id IS NULL; -- 检查候选会话是否新增了工具调用 SELECT b.order_index, b.tool_name FROM tool_call_evidence b LEFT JOIN tool_call_evidence a ON a.session_id = 'baseline_session_id' AND a.order_index = b.order_index WHERE b.session_id = 'candidate_session_id' AND a.trace_id IS NULL;

参数对比在SQL里比较啰嗦,我建议用Python读取两条trace,json.loads之后做归一化比较。写一个normalize函数:把JSON里的key排序、去掉值中多余空格、递归统一嵌套结构,再比较字符串。如果某个字段本身就是时间戳或随机数,把它加入忽略清单,比如request_id、current_time。否则每次跑trace都可能因为时间不同而误报回归失败。

4.3 回归通过标准:阻断性差异和提示性差异

不要一看到两条trace不一样就标红。我习惯把回归结果分成三个等级:blocker、warning、pass。工具调用序列不一致、参数归一化后仍然不一致、出现了新的异常,这些属于blocker。最终答案措辞不同但语义一致、额外多调用了一次无副作用工具,这些属于warning。没有任何blocker,且warning数量在可接受范围内,才判定通过。

三个等级都要写进回归证据表。blocker写进tool_consistency,warning写到diff_summary,pass也要留痕。判定函数可以很朴素:先对比tool_name序列,再对比每个input_json归一化后的哈希,最后用difflib或简单的字符重叠率比较答案。答案相似度低于0.6就标warning。大模型输出本来就带随机性,要求两次回答逐字一致不现实,语义层面的初筛交给人工判断最稳妥,既不烧token又可解释。

4.4 从单Agent扩展到多Agent与记忆

跑通单Agent后,很多人想上多Agent。我的建议是先在证据表里加一个actor字段,再谈编排。每个Agent、每一条内部消息都可以当作工具调用记录,把actor放进tool_call_evidence表,比如actor="planner"或actor="executor",查询时按actor过滤,就能看到每个角色的动作。多Agent的编排本质上还是循环,只是消息从一个循环拆成了多个子循环,Trace逻辑是通用的。

如果引入记忆机制,可以在session_evidence表增加memory_snapshot字段,每次会话开始时把注入记忆的摘要快照存进去。离线回归时对比两个版本的memory_snapshot,能防止记忆污染导致行为漂移。Agent安全方面,如果需要执行Shell命令、写文件或访问内部网络,工具表里加一个permission_check字段,记录每次调用的权限校验结果。这既满足审计需要,也为后续接入更严格的安全策略打底。

5. 新手常踩的坑:我的排查记录

5.1 工具输出太大把表撑爆

我遇到的第一个坑是数据库字段用VARCHAR(255)装工具输出,结果一查数据全是截断后的前255个字符,离线回归时两块trace明明一样却显示不一样。后来把所有JSON字段改成TEXT,问题立刻消失。另外,工具返回几万字时,即使TEXT也占空间。我的办法是入库前截断到2000字符,并把完整输出写到output_files/{trace_id}.txt,在output_json里只保留截断文本和一个file_path字段。既能看摘要,又能追溯到原文。

建议在装饰器里加一个MAX_OUTPUT_LENGTH常量,超过就截断并加提醒,而不是全量入表。否则用例集一大,SQLite文件涨到几百MB,查询变慢,备份也麻烦。对本地Agent这个阶段,保留摘要和路径是性价比最高的方案。

5.2 模型非确定性导致对比失真

默认情况下大模型不是确定性的,同样输入两次回答可能不同,工具参数里的键值顺序也可能变。我第一次做回归时,因为两次trace参数顺序不一致,把结果标成blocker,白折腾一个小时。后来在对比脚本里加了两步:一是对JSON Key排序,二是定义忽略字段列表,如时间戳、请求ID。同时,实验阶段尽量把模型temperature设成0,减少随机性。虽然不能完全消除,但能大幅减少无意义diff。

如果你用开源模型本地部署,还可以固定随机种子、关闭采样来进一步稳定输出。但不要追求完全一致,Agent工具调用的顺序和参数只要语义等价,就应该算通过。判定表里也应有对应check_item,例如“工具名、必填参数、工具顺序三个维度一致,则判定通过”,低相关差异不会干扰主线判断。

5.3 回归通过但线上出问题的排查思路

有一种情况特别气人:离线回归全pass,一上线就出问题。原因通常是回归用例集覆盖不够。比如你新增了calculate工具,但用例集没覆盖计算类请求,那回归“通过”其实是假通过。我的做法是每次新增工具,强制补至少三条用例:一条正常输入、一条边界输入、一条反例或异常输入。更新Prompt或Agent逻辑时,把受影响的历史会话挑出来加入用例集,再跑一轮回归。

还有一些问题只出现在真实环境,比如用户输入格式不规范、工具依赖的外部服务超时。离线trace只能证明“逻辑链路没变”,不能证明“外部依赖可用”。排查线上问题时,我第一件事是把当次会话的session_evidence按时间倒序捞出来,看status是不是error、卡在哪个order_index。这套证据表能直接定位是被哪个工具拖垮的,比翻日志高效得多。

5.4 Agent安全审计和沙盒意识

本地Agent虽然跑在自己机器上,安全问题同样不能忽略。工具函数可能执行任意代码、访问文件系统、发起网络请求。我的习惯是:所有可能产生副作用的工具,调用前先经过一个权限检查函数,返回allow或deny,并把检查结果写入permission_check字段。比如删除文件、修改配置这类操作,默认拒绝,除非在配置文件里显式允许。Trace里的permission_check本身就是审计证据,比出问题后再翻日志更有说服力。

还要注意不要把密钥放进trace。如果工具的参数里有api_key,入库前必须把值替换成"***"。我吃过一次亏:把包含第三方密钥的trace文件发给朋友做回归对比,对方反编译后拿到了密钥。自那以后,我在装饰器里加了脱敏列表,凡是字段名里包含key、token、password、secret的,一律打码再写证据表。建议你第一时间也加上这条。

5.5 初级避坑清单

最后整理几条避坑清单。第一,先手工写循环,再看框架,不要一上来研究LangGraph、CrewAI的编排机制。第二,一定要先保存基线再改代码,否则离线回归无从谈起。第三,证据表字段能用TEXT不用VARCHAR,能用SQLite不用重数据库。第四,回归对比要区分阻断性差异和提示性差异,不要把随机噪声当成功能破坏。第五,任何工具入库前先脱敏,权责边界要在一开始就划好。

这些坑都是我实际踩过的,不是从文档里抄来的。按这个顺序做,初级选手完全能在三天内把一个带工具调用的本地Agent跑起来,并且拥有一套“改完能证明没改坏”的证据体系。今天先把表建好,明天埋点,后天跑第一次基线,迭代速度会超出预期。

我现在每次改Agent,都会先问自己一句:这次改动,我手上有旧版本的Trace吗?如果有,改完直接拉diff;如果没有,就先补跑基线再动手。这套习惯帮我省了特别多无意义的试错。四张证据表看起来不起眼,但它们让Agent从“神秘黑盒”变成了“可审计、可回归、可回放”的普通程序。你要是也刚入门,建议从这4张表开始,跑通后再考虑要不要上专业测评平台。等工具多了、用例多了,你会回来感谢今天存的每一张证据表。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询