2026/10/5 5:19:58

用四张证据表打造可追踪可回归的本地Agent项目

用四张证据表打造可追踪可回归的本地Agent项目 直接说结论如果你已经能写好一个普通脚本但总觉得Agent离自己还很远这篇文章就是给你写的。我从一个连“工具调用循环”都讲不清的初级开发做到能在本地跑通一套带追踪、可回归的Agent项目中间没碰任何玄学架构靠的就是四张事实表。它们分别是会话表、工具调用表、轨迹表、评测表。别被名字吓到它们不是什么基础设施级的设计只是你在做Agent时顺手把关键节点落库让每次对话、每个工具调用、每段思考过程、每次测试打分都有据可查。这套方案的核心理念非常朴素Agent能不能用得看证据不是看感觉。你不需要分布式框架、不需要复杂的消息队列、不需要云端大集群一台能跑代码的机器加一个本地模型服务就够了。本文会详细拆解为什么工具调用日志如此重要、离线回归怎么落地、四张证据表各自的职责边界以及我自己踩过的坑和排查经验适合那些正在自学Agent开发、想搭一个能反复迭代的本地项目、但又不确定从哪下手的同学。1. 整体设计与思路拆解很多刚接触Agent开发的朋友会把注意力放在模型选型和提示词润色上。模型确实重要但如果你只盯着模型你的项目会永远停在demo阶段。我自己的体会是Agent工程的骨架其实是两条线一条是Agent在运行期的调度循环另一条是Agent在开发期的可观测与评测闭环。这两条线正好对应了标题里的Tool Trace和离线回归。1.1 Agent的核心结构调度循环、工具层、上下文先说调度循环。一个Agent本质上就是一段循环代码拿到用户请求交给模型模型输出一个决策可能是最终答案也可能是一次工具调用请求如果是工具调用程序去执行工具把结果返回给模型模型再继续决策直到产生最终答案或者达到最大轮数。这个循环听起来简单实际写的时候很容易乱。最容易乱的是“工具的触发与返回值处理”因为模型返回的并不总是结构化内容。我曾经在早期项目里直接让模型输出JSON格式的调用指令结果模型偶尔会在JSON前后加解释性文字导致解析直接崩掉。后来我把工具调用协议统一成OpenAI兼容的function calling风格由框架解析结构化参数用户完全不需要担心格式。如果你想快速搭建而不是从零造轮子可以先用现成的Agent框架比如LangChain的AgentExecutor、Spring AI的Agent相关模块或者专门面向工作台的轻量编排器。它们做的事情都是一样的把循环跑起来把工具注册进去把上下文管理好。1.2 为什么选择本地部署与轻量编排这篇标题里有个关键词是“本地Agent”。本地部署的好处很直接数据在自己手里调试链路短运行成本可控。缺点则是模型推理性能通常不如云上API特别当你用7B、13B量级的开源模型时生成速度可能会比较感人但只要任务不复杂速度完全够用。我选择轻量编排而不是重量级框架出于一个很实际的理由刚入门时如果直接上一个带服务发现、模型网关、多租户隔离的框架体系你会同时面对很多无关问题反而不清楚一个Agent到底是怎么跑起来的。轻量编排允许我在几百行代码内看清楚循环的每一步把日志先打出来之后再抽象。先做能跑的东西再谈架构对初级开发者是更加稳妥的路径。1.3 四张证据表的价值与职责划分所谓四张证据表不是说数据库只能有四张表而是说你需要至少这四张维度完全独立的事实记录才能支撑一个正循环。它们分别是会话表、工具调用表、轨迹表、评测表。每一张表回答一个核心问题会话表回答“用户在什么时间开始了什么对话”工具调用表回答“某次对话里调用了哪些工具、参数是什么、结果是什么”轨迹表回答“模型每一步都看了什么、想了什么、输出了什么”评测表回答“这次改动到底让效果变好了还是变差了”。这四张表单独看都不复杂但合起来就构成了一个可追踪、可回放、可评分的闭环。很多人做Agent项目容易陷入一种状态改两句提示词跑一下感觉不错再改两句又感觉不错。但这种“感觉不错”完全不可靠因为你没有记录基准线。四张表最核心的价值就是把“觉得效果不错”变成“评测分数提升了X个百分点”这是初级开发者向专业Agent工程迈进的标志性转变。2. 核心细节解析与实操要点四张表听上去清晰但落地时每一张表都有讲究。这节我会按表逐个展开说清楚字段设计、存储选型、数据流走向以及我在初期设计时犯过的错误。2.1 会话表对话生命周期的根节点会话表是所有记录的入口它描述一次独立对话的元信息。核心字段包括会话ID、用户标识本地项目可以空着或填设备号、创建时间、更新时间、会话状态、可选的会话标题、会话来源。初期我犯的错是把所有信息揉进一张宽表。比如把工具调用结果、模型回复都塞在会话表的JSON字段里这方便是方便但排查问题时完全没法索引。正确做法是让会话表保持极简只承担聚合入口的职责具体数据分散到其他三张表。获取会话时通过时间倒序拉取列表通过会话ID联查其它表。这里补充一个实操细节如果未来要做多轮记忆会话表里一定要预留memory_summary字段。这个字段不需要每次都写可以等会话累计到一定轮数后让Agent自己生成一段摘要存进去。这比你每次硬塞完整历史要节省大量上下文窗口。2.2 工具调用表Agent行为的操作日志工具调用表记录Agent每一次向外部能力发起的操作。字段建议包括调用ID、会话ID、工具名称、输入参数JSON、输出结果、错误信息、开始时间、结束时间、耗时、调用序号。这张表的核心价值在于定位工具本身的问题。比如我发现一次联网搜索工具经常超时用SQL直接按工具名分组计算平均耗时和错误率几分钟就能确认问题。如果没有这张表你只能靠反复重跑对话效率极低。工具调用表设计时有两个容易踩的坑。第一个坑是忽略调用序号。没有序号你就没法知道上一次工具调用和当前模型回复之间的先后关系回放时一团乱。第二个坑是没有区分“工具本身报错”和“Agent逻辑报错”。工具执行时抛异常日志要写在错误信息里模型把工具结果理解错了那是推理逻辑的问题记录时是正常结果。两者不能混为一谈否则你会对着一条错误日志误判成工具故障实际是提示词写得不够清晰。2.3 轨迹表Agent思维回放的原始素材轨迹表记录的是模型在每一轮决策中看到的完整上下文和输出内容。字段建议包括轨迹ID、会话ID、轮次序号、输入内容用户问题加之前的上下文摘要、模型输出内容、调用的工具名、工具返回结果、进入下一轮时的摘要、时间戳。你可能觉得这些内容日志里已经有没必要单独建表。但普通日志和轨迹表有一个本质差别日志是面向程序调试的轨迹表是面向Agent行为分析的。日志行的目标用户是人轨迹表的目标用户既有人也有程序你可以写脚本遍历轨迹表统计模型在多少轮之后放弃调用工具、在什么场景下反复调用同一个工具从而发现策略性的问题。我有一段亲身经历Agent在某个任务上总是绕圈子来回调用同一个搜索引擎结果拖了很多轮。后来我查轨迹表发现每一轮的“输入内容”里我都把历史完整塞进去导致模型在超长上下文中迷失了重点。调整上下文裁剪策略之后平均轮数直接下降了40%。这个结论如果不是靠轨迹表积累数据几乎不可能被定位。2.4 评测表量化改动效果的标尺评测表是整个四表方案里最容易被人忽视、但实际上最重要的一张。它记录的是某次评测的配置、用例集、评测结果和关键指标。字段建议包括评测ID、评测集名称、模型型号、系统提示词版本、温度参数、用例总数、通过数、失败数、平均得分、通过率、评测时间、备注。评测得有输入输入就是评测集。评测集不是随便凑几条问题而是要覆盖正常请求、边界情况、异常输入、恶意输入。正常请求比如“帮我查一下明天的天气”边界情况比如超长文本输入、多轮追问异常输入比如前后矛盾的指令恶意输入类似“忽略你之前的所有指令”。Agent不是一个只有一种行为模式的功能它面对的是开放世界评测集必须是多样化的。评测结果要怎么打分取决于你希望Agent达到什么标准。如果是检索问答类你可以定义信息准确度评分如果是工具调度类你可以统计工具选择是否合理、执行是否成功如果是生成类你还可以引入一个辅助模型当裁判按维度打关联度、完整性、礼貌度。我自己的项目为了控制成本评测表只记录最终的数值型指标辅助模型的详细打分过程则落成另一份细节表避免评测表本身膨胀。2.5 存储选型先SQLite别急着上重型数据库如果你只是做本地项目首选SQLite。它的优点不需要多讲单文件、零配置、支持标准SQL。四张表的数据量在个人项目中完全不会成为瓶颈哪怕每天跑几百次会话也毫无压力。但如果你后续要接团队协作或者前端展示可以无缝换成PostgreSQL因为四张表的结构可以原样搬过去。我在迁移时几乎没有改业务代码只换了一个数据库驱动加连接串。这里建议初期就把id统一用UUID字符串方便将来分布式环境下生成避免自增整数在数据合并时冲突。3. 实操过程与核心环节实现这节我会完整演示一遍从埋点到回放的流程不讲直接可复制的全部代码但会给出最关键的骨架和每一步的意图。你可以照着自己的工具集去改。3.1 定义证据记录模型在设计四张表之前先在代码层组织一个简单的模型。以Python为例用dataclass定义会话、工具调用、轨迹、评测记录的数据结构。注意这里不要在数据模型内部写业务逻辑只做字段容器。from dataclasses import dataclass from datetime import datetime from typing import Any, Optional dataclass class SessionRecord: session_id: str status: str created_at: datetime updated_at: datetime memory_summary: Optional[str] None dataclass class ToolCallRecord: call_id: str session_id: str tool_name: str input_params: dict output_result: Any error: Optional[str] started_at: datetime ended_at: datetime sequence_no: int dataclass class TraceRecord: trace_id: str session_id: str turn_index: int context_input: str model_output: str tool_name: Optional[str] tool_result: Optional[str] summary_next: Optional[str] created_at: datetime dataclass class EvalRecord: eval_id: str eval_set_name: str model_name: str prompt_version: str temperature: float total_cases: int passed_cases: int average_score: float pass_rate: float evaluated_at: datetime这段代码核心是明确分层SessionRecord负责会话边界ToolCallRecord负责工具操作TraceRecord负责思考过程EvalRecord负责评测结果。每个dataclass字段基本对应数据库表的一列迁移测试都很直接。3.2 在Agent主循环中埋点主循环是所有事件的发生地。以最简单的自定义Agent为例我通常在三个地方做埋点循环开始时创建会话记录模型每次返回时写入轨迹记录工具执行前后写入工具调用记录。def run_agent(session_id: str, user_input: str): # 1. 更新会话状态 session_store.touch(session_id, statusrunning) history load_history(session_id) turn_index len(history) context_input build_context(user_input, history) while True: # 2. 调用模型前记录本轮输入 model_output llm.chat(context_input) trace_store.insert( TraceRecord( trace_iduuid.uuid4().hex, session_idsession_id, turn_indexturn_index, context_inputcontext_input, model_outputmodel_output, ) ) tool_call parse_tool_call(model_output) if not tool_call: break # 3. 执行工具前后记录耗时和结果 call_start datetime.now() try: result tool_executor.execute(tool_call) error None except Exception as e: result None error str(e) call_end datetime.now() tool_store.insert( ToolCallRecord( call_iduuid.uuid4().hex, session_idsession_id, tool_nametool_call.name, input_paramstool_call.params, output_resultresult, errorerror, started_atcall_start, ended_atcall_end, sequence_noturn_index, ) ) context_input build_context(user_input, history, tool_call, result if not error else error) history.append({role: assistant, content: model_output}) fill_trace_tool_info(trace_id, tool_call.name, result if not error else error) turn_index 1 if turn_index MAX_TURNS: break session_store.touch(session_id, statusfinished) return final_answer这段代码展示了证据表如何与主循环紧密结合。trace_store.insert先插一条没有工具信息的轨迹然后等工具执行完再用fill_trace_tool_info补充工具信息。原因是模型可能有多轮调用如果不先插入轨迹就执行工具那么在工具耗时较长时后续查询就看不到“这一步正在发生什么”会丢失一部分实时状态。3.3 从Tool Trace中生成回放与检索表建好、日志写完真正价值的体现是在回放。最开始我只把Tool Trace当成排查问题用的日志后来我写了一个回放脚本输入会话ID能按轮次打印出模型看到了什么、调用了什么工具、工具返回了什么。这个回放脚本还可以做更深层的统计。写一段SQL就能回答很多之前靠猜的问题比如“平均每个会话调用了几次工具”“哪个工具失败率最高”“最长链条是多少轮”。这些指标拿到手再去调系统提示词和上下文管理策略效果完全不一样。# 统计每个工具的平均耗时和错误率 SELECT tool_name, COUNT(*) AS total_calls, AVG(CAST(STRFTIME(%s, ended_at) AS INTEGER) - CAST(STRFTIME(%s, started_at) AS INTEGER)) AS avg_cost_ms, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS error_rate FROM tool_calls GROUP BY tool_name;这就是典型的Tool Trace分析。Trace在这里不仅是一条线性的日志更是一种可以聚合、筛选、对比的数据资产。很多Agent项目跑起来很顺但问它为什么好说不出来有了数据资产至少你能用数字去解释。3.4 构建离线回归脚本与评测报告接下来是离线回归。我的做法是准备一个固定评测集配置好模型参数和提示词版本跑一遍所有用例把结构化结果写入评测表最后直接用SQL报表对比本次与上次的指标。离线回归的脚本需要保证用例隔离。每个用例都开新的会话不要复用历史否则上一条用例的上下文会污染下一条结果。另一个关键是固定随机性温度调成0关闭采样随机这样对比才公允。评测集一旦成为回归基线尽量少改用例如果要加新用例就追加到一个新版本里保持历史可比性。# 输出最近两次评测的通过率和平均分对比 SELECT eval_id, eval_set_name, model_name, prompt_version, total_cases, passed_cases, pass_rate, average_score, evaluated_at FROM evals ORDER BY evaluated_at DESC LIMIT 5;通过这张表你能直观看到某个改动是在提升还是在下滑。假设你把工具选择的提示词重写了跑一次回归通过率从82%提升到88%那这个改动就是值得保留的。如果通过率反而下降到75%你应该立刻回滚不要用感觉判断。4. 常见问题与排查技巧实录这部分是我的实战排坑经验。很多问题看起来和四张表无关但真正把它们串起来的正是那四张表。4.1 上下文无限膨胀导致Agent迷路最开始我的Agent越跑越慢而且到后面几轮开始答非所问。用轨迹表一查发现每轮我都把完整会话历史塞给模型到第五轮时上下文已经有三千多个token模型完全被不相关信息稀释。后来我加了上下文滑动窗口只保留最近三轮历史加一个历史摘要效果立竿见影。这个问题的排查依赖的就是轨迹表里的context_input字段没有这个字段你只能靠肉眼盯调试输出。4.2 工具调用循环卡死另一个常见问题是工具调用循环出不去。模型反复调用同一个工具形成死循环。我在工具调用表里看到同一个会话连续二十次调用同一个搜索工具时间间隔非常短马上意识到是模型想通过不断搜索来掩盖自己没法下结论。解决办法是设置最大轮次限制同时在系统提示词里加入“如果搜索结果仍无法判断请基于现有信息给出带不确定性的回答”这个限制不仅兜底还能引导模型更早收手。4.3 并发场景下Agent扛不住很多人在介绍自己的Agent项目时会被问“你这里面到底能不能并行处理多个会话”。我刚开始做本地服务时采用的是简单串行处理来一个请求处理一个结果界面卡得像老式拨号上网。后来我按会话ID维度做了并发调度数据库本身用SQLite时需要注意写入锁。SQLite并发写是有限制的可靠做法是把写入操作集中在事务里提交或者干脆在早期就不追求高并发把并发场景留给后续迁移PostgreSQL再解决。记住一个原则没有证据表支撑时先别谈并发优化因为你看不见瓶颈在哪里。4.4 评测集被污染还有一个非常隐蔽的问题。离线回归跑了几轮指标越来越高我一度很兴奋后来发现评测集里有些用例已经在历史对话里出现过了模型等于在“背答案”。这种情况在评测表里其实看得出来如果某条用例的得分永远接近满分且没有波动很可能是记忆污染。解决办法是评测时使用全新会话、隔离历史并定期抽查评测结果不要盲信分数。另一个技巧是给评测集留一部分“金丝雀用例”这些用例永不对外使用专门用来检测泄漏。5. 一点实践后的扩展思路四张表的结构不仅适用于个人项目。如果你之后想把Agent做成团队共享能力可以在会话表与工具调用表之间再加一张用户反馈表记录用户在每条回复上的点赞、点踩、纠错内容。这些反馈是比模型分数更真实的信号可以作为评测集的种子数据来源用户经常点踩的场景应该优先补充进评测集。同样地如果你的Agent开始接入更多模型供应商评测表里的模型型号字段会成为你做模型选型的重要依据。同一套评测集跑GPT、Claude、开源模型分数对比一目了然不用再靠“好像这个模型聪明一点”的主观感受做决策。到了这个阶段你已经不是在做一个玩具项目而是在建立一套可以被客观评估的Agent评价基线。我个人在这套方案里最看重的一点是它能把“感觉”从开发过程中剥离出来。模型能力可能会有起伏提示词风格会变但证据表不撒谎。只要四张表在你随时可以从任意一个会话出发说清楚那次请求发生了什么、Agent做了哪些决策、最终效果几分、相比上一版是否进步。对于初级开发者建立这种证据意识可能比掌握某个特定框架更有长期价值。我最后分享的小技巧是哪怕你不打算写独立的评测脚本也一定从第一天就开始记录工具调用日志。你可以不用四张表那么完整但至少保留一份原始的、没有被截断的工具调用流水。因为日后你想改进Agent时那是你唯一可以真正依赖的出发点。