资讯动态

构建Agent可追溯审计链路:身份、行为与决策的全记录

发布时间:2026/10/18 8:02:29 来源:尧图企业网站定制
1. 审计难题的根源Agent 为什么比传统系统难管先把话说在前面Agent 应用真正投入生产之后第一个找上门来的往往不是效果问题而是审计问题。用户会问刚才那个动作是谁发起的Agent 为什么要调那个接口中间做过哪些决策如果你答不上来这个项目就很难从试点走向规模化。传统系统的审计其实很成熟。一个请求进来经过哪个服务、调用了哪个数据库、返回了什么状态链路追踪工具一拉就出来了。但 Agent 不一样它的行为链条长得多用户输入一句自然语言Agent 要理解意图、拆解任务、规划步骤、选择工具、拼接参数、解析结果、决定下一步……每一步都可能出错而且每一步都可能产生合规风险。更麻烦的是LLM 本身的输出具有概率性同样的输入可能给出不同的动作序列这意味着你没法靠“复现”来排查问题只能靠“记录”。我见过不少团队踩过同样的坑Agent 上线时只关注准确率和响应速度等到出了事故才开始补日志。结果发现日志里只有最终答案中间调了哪些工具、传了什么参数、模型当时看到了什么上下文全是空白。这就是典型的“事后想审计但当初没设计”。所以做 Agent 的审计与合规核心不是加一个日志功能而是从一开始就把“可追溯性”作为系统架构的一等公民来设计。你需要回答三个最朴素的问题谁让 Agent 做的身份与授权Agent 做了什么行为与动作为什么 Agent 这么做决策依据与上下文这三个问题对应着审计链路的三大支柱身份溯源、行为记录、决策还原。缺任何一个审计链路都是断的。1.1 为什么传统日志方案不够用有人可能会说Agent 不就是个接口调用吗把输入输出打到日志里不就行了。这话对了一半。如果你只是用 Agent 做单轮问答比如“帮我写一封邮件草稿”那确实记一下 prompt 和 response 就够用了。但真实生产环境里的 Agent 往往具备工具调用能力比如查数据库、调 API、操作文件、发消息甚至多 Agent 协作。这时候动作链条是分叉的、嵌套的、带循环的。举个例子一个客服 Agent 在帮用户查订单时可能先调用用户信息接口确认身份再调用订单查询接口获取列表发现订单状态异常后又调用工单系统创建一个投诉单。这一系列操作如果只记最终回复出了问题根本没法定位。到底是身份校验环节拿错了参数还是订单查询接口返回了脏数据还是 Agent 决策逻辑选错了分支没有过程记录全靠猜。传统日志方案的另一个问题是“上下文断裂”。LLM 的每一次调用都包含系统提示词、历史消息、工具返回结果等庞大的上下文。这些内容如果不做快照事后根本不知道模型是基于什么信息做出的决策。我之前在一个项目里排查过一个问题Agent 在回答用户时给出了一个错误的价格最终日志显示模型确实输出了那个价格但没人知道这个价格是从哪儿来的。后来翻查完整上下文才发现是某个工具接口返回的 JSON 里有一个字段被错误解析了模型基于错误输入给出了合理但错误的输出。这种情况没有完整的决策上下文记录永远查不清楚。1.2 审计链路的三大目标身份、行为、决策聊清楚了为什么难再来说说审计链路要实现的三个具体目标。第一是身份溯源。每个 Agent 任务必须能追溯到发起者包括用户 ID、用户角色、认证凭证的上下文中携带的权限范围。如果系统里还有多级审批或者委派机制A 用户发起任务审批后由 B 用户的实际权限去执行这条身份链也要完整记录。特别注意在 Agent 场景里身份不只是“人”还包括“哪个应用调用的”“哪个定时任务触发的”“哪个上游 Agent 委派的”。我见过一个很隐蔽的坑定时任务触发了 Agent 去批量处理数据处理过程中调用了某个写操作事后审计根本找不到“人”的记录因为任务的发起者本身就是一个系统账号。这类情况必须在设计审计链路时提前定义好“身份”的粒度。第二是行为记录。Agent 每次调用外部工具、读取数据、执行写操作、发送请求都要记录完整的行为日志。包括动作类型、目标对象、传入参数、返回结果、耗时、状态码、异常信息。这里的关键是参数和结果都要记而不能只记动作名。因为同一个动作名参数不同代表的行为完全不同。比如“发送邮件”这个动作发给 A 和发给 B、正文包含机密信息和正文为空合规意义天差地别。第三是决策还原。这是 Agent 审计区别于传统系统审计的最核心特征。你需要记录模型在每一步看到了什么上下文、输出了什么中间思考、选择了哪个工具、拒绝了哪个候选动作。虽然很多 LLM 提供商会支持开启思维链输出但生产环境里通常因为安全和成本考虑不会开所以更实际的做法是记录系统自己能够捕获的决策信息用户输入的完整内容、系统提示词版本、检索到的候选工具列表、模型输出的结构化动作指令、工具执行结果回填后的最新状态。这三个目标对齐之后审计链路的设计就有了依据。下面我会讲具体的架构设计和实现方案。2. 可追溯链路的设计思路从埋点到全链路追踪搞清楚了要记录什么接下来就是怎么记录的问题。这里要引入一个在可观测性领域已经很成熟、但很多 Agent 项目还没用上的概念链路追踪Distributed Tracing。传统微服务架构里的链路追踪核心是一个全局唯一的 TraceID从请求进入系统开始生成贯穿整个调用链路的每个环节。每个环节记录自己的 SpanID、父 SpanID、开始时间、结束时间、状态、标签。这样一条完整的请求路径就可以被还原成一颗调用树。Agent 的审计链路完全可以借鉴这个模式但要加一层“意图层”。因为 Agent 的执行路径不是固定的 RPC 调用链而是动态分叉的任务树。同样是“处理用户退款请求”这次可能走两步就结束了下次可能因为信息不足需要反问用户再继续。静态定义的链路模板不管用必须实时记录任务树的生长过程。我推荐的设计方案是“4 1”模型四个层级的数据结构加一个贯穿性的关联 ID。2.1 链路追踪模型TraceID 贯穿始终先建立一个核心概念每一个用户请求或任务触发生成一个全局唯一的 TaskTraceID可以简称为 TraceID。这个 ID 从任务创建开始生成一直到整个 Agent 会话彻底结束才销毁。期间产生的所有日志、审计事件、工具调用记录、模型调用记录、检索记录都必须带上这个 TraceID。同时在 TaskTraceID 之下每一轮 Agent 的思考-行动-观察循环也就是常说的 ReAct 循环分配一个 StepID每个工具调用分配一个 ActionID每个 LLM 调用分配一个 LLMCallID。这些 ID 之间存在明确的父子关系Task - Step - Action / LLMCall - ToolCall。这样任何时候你都能还原出一棵完整的执行树总任务是什么、分成了哪几步、每一步调了什么工具、各自结果如何。这个设计我在实际项目里验证过非常有用。有一次排查一个 Agent 误操作数据的问题从最终状态往回查先找到触发误操作的那个工具调用 ActionID然后顺着父 ID 找到对应的 Step发现是该 Step 的决策逻辑出了问题——模型在选择工具参数时把旧数据当作最新数据传了进去。继续向上追溯定位到前一轮工具调用返回了缓存结果导致后续决策用了过期信息。这一整套排查过程没有 TraceID 体系根本做不来。2.2 审计事件需要覆盖的五个维度确定了链路模型之后再来定义每一层具体要记录哪些内容。我把它总结为五个维度缺一个都不完整身份维度用户 ID、用户角色、权限令牌摘要不要存明文 Token、应用来源、会话 ID、发起方式人工/定时任务/上游系统。指令维度用户输入的原始内容、系统收到指令的时间、指令的版本标识如果指令本身是结构化 Json就存完整 Json 快照。决策维度当前 Step 的输入上下文摘要、候选工具集合模型可选哪些工具、模型原始输出、选择执行的工具、未选择的工具及原因如果模型输出里带的话、上下文快照存储位置。行为维度工具名称、工具版本、传入参数脱敏后的完整参数、返回结果摘要、执行耗时、状态码、异常堆栈。环境维度Agent 框架版本、模型名称与版本、提示词模板版本、运行时环境标识、时间戳统一用 UTC 存储。这五个维度合在一起才能回答“谁在什么时间基于什么信息通过哪个工具对什么对象做了什么事结果如何”。少一个维度审计链条就有断点。2.3 数据采集的位置与时机数据采集的位置决定了审计的覆盖面和准确性。我的建议是“三层埋点”第一层在入口处。也就是所有用户请求进入 Agent 系统的网关或入口函数在这里生成 TaskTraceID记录身份信息和原始指令。这一层必须是最先执行的不能等业务逻辑跑起来之后才生成 ID。第二层在框架核心处。如果你用的是成熟的 Agent 框架比如 LangChain、LlamaIndex 这类或者自研的框架最好在框架的模型调用、工具调用、记忆读写这几个核心钩子处统一埋点。这样不管上层业务怎么变化审计数据都能完整采集。实测下来在框架层埋点的收益远大于在各个业务函数里手动埋点——后者要么漏记要么埋点代码跟业务逻辑耦合太深后期维护成本极高。第三层在工具出口处。也就是 Agent 实际调用外部系统的那一层包括 HTTP 客户端、数据库连接器、文件操作封装等。在这里要额外记录目标系统的地址、请求头摘要、响应码。这一层特别重要因为当外部系统报错或者返回异常数据时审计日志要能证明 Agent 本身的调用是合规的还是外部系统的锅。采集时机上有一个重要原则宁可多记不可漏记。Agent 执行过程中的上下文变化很快每一步的结果都会影响下一步的决策所以任何可能影响决策的数据变化点都应该立刻产生一条审计事件而不是等到整个任务结束了再统一写。任务结束后的“汇总写日志”中间如果进程崩溃或者超时终止前面的过程就全丢了。3. 核心实现方案审计模块的工程落地光有设计思路还不够审计得落到代码上才算数。我分享一下我在项目里实际使用的方案核心是一个独立的审计中间件加一个结构化的审计数据表。3.1 审计数据结构设计先看核心的数据结构定义。我用 Python 写一个简化版的示例生产环境里可能还会加更多字段但这个骨架已经能覆盖绝大多数场景。# audit_event.py from dataclasses import dataclass, field from typing import Any, Optional from datetime import datetime, timezone dataclass class AuditEvent: trace_id: str # 全局任务追踪ID event_id: str # 当前事件ID parent_event_id: Optional[str] # 父事件ID用于构建树形结构 event_type: str # task_start / step_start / llm_call / tool_call / task_end event_time: datetime # 事件发生时间UTC # 身份维度 user_id: Optional[str] None user_role: Optional[str] None auth_scope: Optional[str] None source_app: Optional[str] None # 指令维度 instruction_raw: Optional[str] None instruction_version: Optional[str] None # 决策维度 llm_model: Optional[str] None prompt_version: Optional[str] None context_snapshot_ref: Optional[str] None # 上下文快照存储地址 tool_candidates: Optional[list] field(default_factorylist) model_raw_output: Optional[str] None selected_tool: Optional[str] None # 行为维度 tool_name: Optional[str] None tool_params: Optional[dict] field(default_factorydict) tool_result_summary: Optional[str] None tool_status: Optional[int] None tool_error: Optional[str] None # 环境维度 framework_version: Optional[str] None environment: Optional[str] None extra: Optional[dict] field(default_factorydict)这里有一个细节值得说明tool_params 和 tool_result_summary 要特别注意脱敏处理。审计系统能拿到明文数据但在落库之前应该自动对密码、Token、身份证号、手机号这类敏感字段做掩码处理。判断敏感规则的引擎可以是正则匹配、字段名匹配或者第三方脱敏组件。我一般是用字段名白名单加正则双重校验。3.2 埋点采集模块的实现接下来是采集模块。我习惯用装饰器加上下文变量的方式这样业务代码里只需要打一个标记审计逻辑自动执行。# audit_middleware.py import contextvars import uuid import json from datetime import datetime, timezone from typing import Callable, Any # 使用 ContextVar 在异步调用链中传递 TraceID current_trace_id: contextvars.ContextVar contextvars.ContextVar(current_trace_id, defaultNone) current_step_id: contextvars.ContextVar contextvars.ContextVar(current_step_id, defaultNone) def new_trace_id() - str: return uuid.uuid4().hex class AuditLogger: 审计日志收集器统一异步写入审计存储。 def __init__(self, sink): self._sink sink self._buffer [] async def emit(self, event: AuditEvent): # 这里可以增加批量写入的逻辑减少IO开销 await self._sink.write(event) def build_event(self, event_type: str, **kwargs) - AuditEvent: return AuditEvent( trace_idcurrent_trace_id.get() or unknown, event_iduuid.uuid4().hex, parent_event_idcurrent_step_id.get(), event_typeevent_type, event_timedatetime.now(timezone.utc), **kwargs ) # 全局实例 audit_logger AuditLogger(sinkaudit_sink) def audit_task(func: Callable) - Callable: 任务入口装饰器生成 TraceID记录任务开始和结束。 async def wrapper(*args, **kwargs): trace_id new_trace_id() token current_trace_id.set(trace_id) start_event audit_logger.build_event( task_start, instruction_rawkwargs.get(instruction, ), user_idkwargs.get(user_id), user_rolekwargs.get(user_role), ) await audit_logger.emit(start_event) try: result await func(*args, **kwargs) end_event audit_logger.build_event(task_end, extra{result_summary: str(result)[:200]}) await audit_logger.emit(end_event) return result finally: current_trace_id.reset(token) return wrapper def audit_tool(tool_func: Callable) - Callable: 工具调用装饰器自动记录工具调用参数和结果。 async def wrapper(*args, **kwargs): tool_name tool_func.__name__ params_safe mask_sensitive(kwargs) # 脱敏处理 start datetime.now(timezone.utc) try: result await tool_func(*args, **kwargs) status 0 error None result_summary str(result)[:500] except Exception as e: status 1 error str(e) result_summary raise finally: event audit_logger.build_event( tool_call, tool_nametool_name, tool_paramsparams_safe, tool_result_summaryresult_summary, tool_statusstatus, tool_errorerror, ) await audit_logger.emit(event) return result return wrapper这段代码有几个值得注意的设计点。第一个是 ContextVar 的使用。在异步环境里如果不用 ContextVar 而是用全局变量多个并发任务会把 TraceID 串掉导致 A 用户的任务日志里混入 B 用户的操作。ContextVar 天然支持异步上下文隔离实测比手动传参要可靠得多还不容易漏传。第二个是装饰器的职责划分。audit_task 负责任务级的生命周期audit_tool 负责工具级的调用日志。这样 Agent 框架层只需要在调用工具的地方统一加上 audit_tool 装饰器后续新增工具的时候自动获得审计能力不需要每个工具手动写日志代码。第三个是 sink 的抽象。审计日志的写入端可以是数据库、消息队列、对象存储加全文检索服务。我在生产环境里用 Kafka 做缓冲异步批量写入 ClickHouse兼顾写入性能和查询性能。如果项目规模不大直接异步写入 PostgreSQL 或者 MongoDB 也够用关键是不要在业务线程里同步写审计日志否则 Agent 的响应时延会被拖得很厉害。3.3 上下文快照与关联LLM 调用上下文的快照是决策还原的关键也是很多人容易忽略的一块。我的做法是每次 LLM 调用之前把系统提示词、历史消息摘要、检索到的文档片段、工具返回结果等全部序列化为一个 JSON 对象计算哈希后存到对象存储或者数据库的大字段里。审计事件里只存快照的引用地址和哈希值不直接内联完整快照避免审计事件体积过大很多场景下上下文快照动辄几十 KB直接塞审计表里会严重影响写入性能。这个做法的另一个好处是防篡改。快照的哈希值存在审计事件里事后如果发现某次决策有问题可以把存储的快照重新计算哈希跟审计事件里的哈希对比。不一致就说明数据被改过这一条在合规审查里非常有说服力。具体实现上快照存储可以复用对象存储OSS 之类的也可以用带版本管理的数据库表。我建议在快照对象里带上一个 schema_version 字段方便以后调试格式升级。别小看这个字段等审计数据积累几个月之后你会感激当初设计了它——否则旧数据在升级后可能全部解析不了。4. 实操案例一个订单退款 Agent 的审计链路重现拿一个实际场景把所有概念串起来会比抽象的描述好理解得多。假设有一个电商系统的售后 Agent用户对一笔订单发起了退款请求。Agent 需要依次执行确认订单归属、查询订单状态、校验退款条件、计算退款金额、执行退款操作。整个流程里审计链路会记录什么我直接贴一段简化版的操作流程描述。用户 A 在前端发起退款请求网关收到请求后生成 TraceID:a1b2c3d4e5。Agent 框架的入口记录 task_start 事件事件内容包含用户 ID、原始指令“我想退掉刚买的黑色耳机订单 20240511001”。Agent 开始第一轮思考。框架先调用了 LLM记录 llm_call 事件包含模型名称、提示词模板版本。此时上下文快照的引用指向对象存储某个 key。模型输出结构化动作调用get_order_info工具参数为订单号 20240511001。get_order_info工具被打上 audit_tool 装饰器自动生成 tool_call 事件。事件里记录的参数是脱敏后的{order_id: 20240511001}返回结果的摘要包含订单状态为“已发货”、金额 399 元。Agent 第二轮思考。模型发现订单已发货不是直接可退状态需要判断退货条件。此时决策记录里写明了候选工具集合create_refund、create_return_order、ask_user。模型选择了ask_user先确认用户是否愿意退货。用户答复“愿意退货”Agent 第三轮调用create_return_order工具参数包含退货地址 ID 和退货原因。工具执行成功状态码 0返回退货单号。Agent 最后调用create_refund执行退款。这一步是敏感操作审计事件里额外标记了权限范围字段记录本次退款操作是在用户 A 的授权范围内执行的。退款接口返回成功Agent 生成最终答案给用户。这整套流程的审计事件会形成这样一棵树task_startTraceID: a1b2c3d4e5 ├── llm_callStep 1 决策 ├── tool_call get_order_info执行查询 ├── llm_callStep 2 决策 ├── tool_call ask_user询问用户 ├── llm_callStep 3 决策 ├── tool_call create_return_order创建退货单 ├── llm_callStep 4 决策 └── tool_call create_refund执行退款 task_end正常完成如果用户 A 事后投诉说“我没同意退款却被退款了”审计人员可以照着这棵树快速定位到create_refund那个节点确认当时操作权限是用户 A 自己的如果再往上查能看到第三步的用户确认环节系统记录了用户 A 的“愿意退货”答复。一条链路完整还原了当时的真实情况。在这个案例里有一个具体的设计决策很关键在create_refund这种高敏操作上不能简单记录“调用了该工具”还要额外标记执行前的“权限预检结果”。很多合规事故的争议点在“该操作是否在授权范围内”所以权限校验环节的结果必须进入审计链路。要注意的是权限令牌本身不能记录原文只能记录 scope、有效期、签发者这些元信息。5. 常见问题与排查技巧实录以下内容全部来自我在真实项目里的踩坑和填坑经历不是理论推演。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案审计日志里 TraceID 是空的ContextVar 没有在入口设置或者异步任务在子协程里丢掉了上下文检查任务入口的装饰器是否生效确认在创建 Task 之前设置 TraceID不能用asyncio.create_task之后再手动设置工具调用日志缺失新增工具函数没加装饰器或者工具调用走了原生 Client 而不是封装后的调用函数全量搜索工具调用入口统一改为通过带中间件的调用层建议在 CI 里加一个测试自动扫描所有工具函数是否有对应审计钩子日志对不上时间序多台实例的机器时钟不同步统一用 UTC 时间戳并且时间字段在事件落库前由日志收集层统一补充不要依赖业务实例的本地时钟生产环境部署 NTP 同步是基本要求上下文快照丢失快照异步写入失败但审计事件仍然保存了引用地址快照写入和审计事件写入应该是同一个事务要么都成功要么都失败如果用了异步批量要加失败重试和告警隐私字段以明文入日志脱敏规则覆盖不全建立字段名扫描机制对每个入库日志跑一遍敏感字段正则在审计日志可视化查询页面上默认强制隐藏敏感字段查询审计数据太慢事件存量太大缺乏合理的索引和分区策略按天对审计事件做分区以 trace_id 和 event_time 作为联合索引分析类查询走单独的只读副本审计事件与模型请求记录对不上LLM 调用是直接走 SDK 的绕过了一层框架封装在 SDK 的外层再加一层统一代理统一注入 TraceID 和审计逻辑5.2 审计数据量过大时的工程取舍审计数据比业务数据大得多这是很多团队初期没预料到的。一次会话动辄产生几十条事件每条事件带上下文引用和工具参数一天几万次调用就轻松攒下几 GB 数据。这时候要分清“热数据”和“冷数据”。热数据保留 30 天放在 ClickHouse 或者 Elasticsearch 里支持实时查询。冷数据定期归档到对象存储按压缩格式存放保留至少一年具体周期按业务合规要求定。归档时不能简单丢弃字段而是要完整保留 JSON 结构否则某个历史事件需要重新分析时发现字段被裁剪了根本没法用。另一个取舍点是快照与事件是否要分库。我建议快照独立存对象存储。因为快照的访问频率远低于事件本身通常只有深入排查时才需要拉取而且快照体量大混在事件库里会把热查询拖慢。事件里只存一个引用地址和一个哈希值需要时按引用地址加载。5.3 关于性能损耗的实测数据很多负责人会问加了全链路审计到底慢多少。我在一个实际 Agent 服务上做过对比测试未接入审计前单次会话平均响应时间约 6 秒包含多次 LLM 调用接入完整的审计链路每步写事件、上下文快照异步上传、工具参数脱敏后平均响应时间约 6.2 秒增加约 3%。因为所有 IO 操作都是异步的业务主链路没有被阻塞。但我必须提醒3% 这个数字依赖于“异步写入 批量攒批”的实现方式。如果你图省事在每个步骤里同步写一次数据库那延迟可能从 6 秒直接跳到 8 秒以上。审计日志写入绝不能同步阻塞业务线程这一点没有妥协空间。业界成熟的方案是本地队列加后台消费者或者直接投递到消息队列。5.4 提示词层面的审计配合技巧审计不光是代码的事提示词设计也要配合。我在系统提示词里固定加了一段话要求模型在调用风险动作删除、修改、转账、发送外部消息等时先输出一条结构化的“动作确认”信息包括动作名称、目标对象和理由摘要。这么做有两个好处一是审计链路的决策记录更丰富。模型输出的结构化动作确认本身就是一条很有价值的审计内容能直接回答“模型当时为什么要这么选”。二是在模型输出层多了一道校验机会。解析到结构化动作后可以在工具执行前做一次规则校验比如操作对象是否在白名单内不通过就直接拦截并记录。这样审计不只是“记录发生了什么”还能变成“阻止不该发生的”。把审计从被动记录升级为主动控制这才是合规的正确姿势。5.5 审计记录不可变与权限控制合规场景下审计数据本身容易成为篡改目标。日志被删了或者被改了审计的意义就没了。所以在存储层面我建议审计库单独使用只写账号业务逻辑使用的数据库账号没有 update 和 delete 权限。查询审计数据走单独的只读账号并且要记录“谁查了审计日志”的日志。用数据库账号权限做约束比在应用层做防篡改控制要硬得多。如果合规要求极高还可以考虑用哈希链做防篡改。简单说就是对事件按时间顺序做哈希拼接每个事件的哈希包含前一个事件的哈希这样任何中间事件的篡改都会导致后续哈希断裂。这个方案的复杂度会高一些但对金融、医疗这类严格合规场景是值得的。6. 从一个可追溯的 Agent 项目回看核心体会做了几个带完整审计链路的 Agent 项目后我个人最大的感受是不要把审计当成上线后才补的“合规作业”而要把它当成 Agent 系统的基础能力来设计。身份溯源、行为记录、决策还原这三个目标必须在架构阶段就定好否则后面补的成本是几何级增长的。另一个体会是审计数据是 Agent 系统最有价值的数据资产之一。很多团队只盯着业务效果数据忽略了审计日志里蕴含的模型行为规律。比如从决策记录里能发现哪些提示词模板经常让模型选错工具从工具调用日志里能发现哪些接口的高频报错模式。审计数据不只是用来出问题时甩锅的也是改进 Agent 行为的重要输入。最后分享一个小技巧上线初期哪怕审计查询页面做得很简陋也一定要保留“按 TraceID 一键拉取全链路事件”这个功能。排查问题时 90% 的情况都是先拿到一个异常现象然后反查 TraceID把整棵事件树拉出来看。界面丑一点无所谓但这个核心路径一定要通畅。我就是从这棵事件树里排查出了文章开头说的那个“缓存数据导致误决策”的案例分析路径整个过程没有审计链路的支撑根本无法完成。如果你正在做一个要上生产环境的 Agent 项目我建议先把审计链路的设计文档写出来再开始写业务代码。等系统跑起来之后再回头看你会庆幸当初多花的那几天是项目里最值得的投入。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑