从零构建 Unity C# 代码审查 Agent:从 Chain 到 Agent 全流程实战

2 阅读32分钟

从零构建 Unity C# 代码审查 Agent:从 Chain 到 Agent 全流程实战

—— FastAPI + LangChain + DeepSeek 保姆级教程,含踩坑与工程化加固

本文将带你从零开始,使用 FastAPI + LangChain + LangGraph + DeepSeek 构建一个专注于 Unity C# 的代码审查 Agent。从最基础的 Chain 链路,到具备工具调用的 ReAct Agent,再到结构化输出、意图分类、思考链可视化和工程化加固,完整呈现一个 AI Agent 从 Demo 到生产级的进化过程。适合对 AI Agent 开发感兴趣的后端/游戏开发同学阅读。


一、项目背景

随着 Unity 游戏开发规模不断扩大,团队编写的 C# 代码越来越多,代码规范不统一、性能隐患隐蔽、面向对象设计不合理等问题会直接影响项目维护成本与游戏运行效率。

为了解决这一问题,我决定学习并开发一个专注于 Unity C# 领域的代码审查智能助手,让 AI 自动完成:

  • 代码规范检查
  • 性能问题识别
  • 面向对象设计建议
  • 可直接落地的优化方案输出

本项目使用 FastAPI + LangChain + LangGraph + DeepSeek 技术栈,从最基础的 Chain 链路开始,逐步升级为具备自主决策、工具调用、行为约束的专业 Agent,最终形成可部署、可扩展、可用于真实游戏开发流程的智能代码审查工具。


二、基础篇:使用 LLMChain(LCEL)搭建代码审查基础链路

在构建具备工具调用能力的 Agent 之前,我先从 LangChain 最核心的 Chain(链路) 开始,搭建代码审查助手的基础版本。

2.1 什么是 Chain?

Chain 是 LangChain 体系中的基础执行单元,它将提示词模板大模型进行结构化组合,让模型调用流程变得可复用、可组合、可维护。

简单来说:Chain = 提示词模板 + 大模型

2.2 为什么使用 LCEL?

随着 LangChain 版本更新,传统的 LLMChain 已逐渐被 LCEL(LangChain Expression Language) 替代。LCEL 使用管道符 | 组合组件,写法更简洁、扩展性更强、支持异步、流式输出、异常捕获,是目前官方推荐的现代化开发方式。

最核心的写法:chain = prompt | llm

2.3 提示词模板设计

为了让模型专注于 Unity C# 代码审查,我通过 PromptTemplate 定义专业、严格的提示词,约束模型只输出代码审查相关内容,保证回答简洁、专业、可落地。

2.4 FastAPI 接口封装

为了让审查能力可被外部调用,我将 Chain 封装为 异步接口 /chain,让本地功能变成可部署、可对接前端的后端服务。

2.5 本章成果

  • 理解 Chain 是构建复杂 AI 应用的基础
  • 掌握 LCEL 现代化链式开发方式
  • 完成 DeepSeek 大模型接入
  • 实现代码审查专用提示词
  • 成功封装可运行的 FastAPI 接口
  • 为后续升级为 Agent 打下坚实基础

三、进阶篇:引入 Agent + 工具调用,实现智能化

在第二章中,我们通过 Chain 让 LLM 能够按照提示词进行代码审查。但 Chain 的能力是线性的:用户输入 → 模型输出,无法主动获取外部信息,也无法根据实际情况动态调整行为。

试想一个更智能的审查场景:Agent 在审查代码时,如果发现一段与时间相关的逻辑(比如 DateTime.Now 的使用),它可能需要知道当前的实际时间来判断代码是否正确。这种“主动获取信息”的能力,正是 Agent 与普通 Chain 的核心区别。

本章我们将为项目引入 Agent + 工具调用,让 AI 从“被动应答”升级为“主动决策”。

3.1 什么是 Agent?

在 LangChain 体系中,Agent 是一个能够自主决策、选择工具并执行任务的智能体。它的工作流程可以概括为:

用户输入 → 推理(Reasoning)→ 选择工具(Act)→ 观察结果(Observation)→ 继续推理或输出最终答案

这个循环被称为 ReAct 范式(Reasoning + Acting)。与 Chain 相比,Agent 最大的特点是:

对比维度ChainAgent
执行路径固定、线性动态、循环,根据推理结果分支
外部交互可调用外部工具(API、数据库、计算器等)
决策能力自主选择调用哪个工具、何时停止
适用场景简单的问答、翻译、摘要复杂任务规划、多步信息检索、工具编排

3.2 ReAct 工作原理:从“思考”到“行动”

ReAct 范式的核心是将 LLM 的推理能力工具执行能力交替进行。每一轮循环包含三个阶段:

  1. Thought(思考):Agent 分析当前对话状态,决定下一步该做什么。
  2. Action(行动):根据思考结果,选择一个工具并传入参数,执行该工具。
  3. Observation(观察):接收工具执行的结果,将其作为新的上下文信息,用于下一轮思考。

这个循环会持续进行,直到 Agent 认为已经获得足够信息来回答用户,此时它不再调用工具,而是直接生成最终答案。

在 LangGraph 中,这个循环被抽象为一个图(Graph),由 create_react_agent 函数自动构建。我们只需要提供 LLM 和工具列表,LangGraph 会帮我们完成节点编排、条件路由和状态管理。

3.3 自定义工具:为 Agent 装上“手脚”

工具(Tool)是 Agent 与外部世界交互的接口。LangChain 提供了 @tool 装饰器,可以将一个普通的 Python 函数快速转换为 Agent 可识别的工具。

3.3.1 定义第一个工具:获取当前时间

考虑到未来代码审查中可能需要判断时间相关逻辑,我们首先实现一个获取当前时间的工具:

from langchain_core.tools import tool
from datetime import datetime

@tool
def get_current_time() -> str:
    """
    获取当前日期和时间。
    注意:只有在与代码审查相关的场景下使用(例如为日志建议添加时间戳)。
    对于普通闲聊问题,不要使用此工具。
    """
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

关键点

  • @tool 装饰器会将函数的名称和 docstring 作为工具的描述信息,LLM 正是通过读取这些描述来决定何时调用该工具。
  • docstring 中明确写入了使用条件,这是约束 Agent 行为边界的重要手段(第四章将详细展开)。

3.3.2 定义第二个工具:计算器

为了测试 Agent 在多工具环境下的选择能力,我们再添加一个简单的计算器工具:

@tool
def calculator(expression: str) -> str:
    """
    计算一个数学表达式。例如: "2 + 2", "100 / 3", "15 * 4"。
    当用户提出数学计算问题时使用。
    """
    try:
        result = eval(expression, {"__builtins__": None}, {"abs": abs, "round": round})
        return str(result)
    except Exception as e:
        return f"计算出错:{e}"

安全提示eval() 在示例中已通过限制 __builtins__ 降低了风险,但生产环境建议使用更安全的数学解析库(如 numexpr)。

3.4 系统提示词设计:划定 Agent 的职责边界

Agent 的“人设”和职责范围由系统提示词(SystemMessage)决定。为了让 Agent 专注于 Unity C# 代码审查,我们设计如下提示词:

from langchain_core.messages import SystemMessage

system_prompt = SystemMessage(content="""
    你是专业的 Unity C# 代码审查助手。
    你只会做:
    1. 代码规范检查
    2. 性能问题检查
    3. 面向对象设计建议
    4. 给出可直接使用的优化代码
    回答必须简洁、专业。
""")

这个提示词将在 Agent 启动时作为第一条消息传入,为整个对话设定基调。

3.5 使用 LangGraph 的 create_react_agent

有了 LLM、工具列表和系统提示词,我们只需一行代码即可创建 ReAct Agent:

from langgraph.prebuilt import create_react_agent

tools = [get_current_time, calculator]
agent_graph = create_react_agent(llm, tools)

版本提示:在 LangGraph 1.0+ 中,create_react_agent 已从 langgraph.prebuilt 迁移至 langchain.agents。本文使用的是当前稳定版本,若未来升级,只需调整导入路径即可。

create_react_agent 在内部做了大量工作:

  • 自动构建包含 Agent 节点和 Tool 节点的图结构。
  • 配置条件边:当 Agent 节点输出包含 tool_calls 时,自动路由到 Tool 节点执行工具;工具执行完毕后,自动返回 Agent 节点继续推理。
  • 管理对话状态,确保 SystemMessageHumanMessageAIMessageToolMessage 按正确顺序传递。

3.6 封装为 FastAPI 接口

将 Agent 封装为接口,供外部调用:

class AgentRequest(BaseModel):
    input: str

@app.post("/agent")
async def run_agent(request: AgentRequest):
    result = await agent_graph.ainvoke({
        "messages": [
            system_prompt,
            HumanMessage(content=request.input)
        ]
    })
    final_message = result["messages"][-1].content
    return {"output": final_message}

此时,访问 POST /agent 接口即可与 Agent 对话。

3.7 初探 Agent 行为:一次意外的“越界”

完成上述代码后,我们进行了初步测试。当输入 “现在几点?” 时,Agent 返回了:

{
  "output": "我是专业的 Unity C# 代码审查助手,专注于代码规范检查...关于当前时间的问题不在我的职责范围内。"
}

这表明系统提示词生效了,Agent 拒绝回答无关问题。但当我们输入 “计算 123 * 456” 时,Agent 却毫不犹豫地调用了计算器工具,返回了精确结果。

这一现象暴露了 Agent 行为优先级的重要规律:当工具描述与用户意图高度匹配时,工具调用的优先级会高于系统提示词的约束。关于这一现象的深入分析和解决方案,我们将在第四章和第六章详细展开。

3.8 本章小结

技术点收获
Agent 概念理解 Agent 与 Chain 的本质区别:Agent 具备自主决策和工具调用能力
ReAct 范式掌握 Thought → Action → Observation 的循环推理模式
工具定义使用 @tool 装饰器快速将 Python 函数转化为 LangChain 工具
系统提示词通过 SystemMessage 划定 Agent 的职责边界
LangGraph 集成使用 create_react_agent 一行代码构建完整的 ReAct 智能体

至此,我们的项目从简单的“问答链”升级为具备工具调用能力的智能体。下一章,我们将深入探讨 Agent 行为优先级的问题,并引入工程化方案来约束 Agent 的“越界”行为。


四、实战踩坑:当系统提示词遇上工具,行为优先级观察

4.1 问题场景

在开发 Unity C# 代码审查 Agent 时,我遇到了一个有趣的现象:

我给 Agent 设置了严格的系统提示词:

你是专业的 Unity C# 代码审查助手。你只会做:

  1. 代码规范检查

  2. 性能问题检查

  3. 面向对象设计建议

  4. 给出可直接使用的优化代码

    回答必须简洁、专业。

同时,我为它配备了一个工具 get_current_time,用于获取当前时间(后续可能用于给日志建议添加时间戳)。

当用户输入 “现在几点?” 时,我期望 Agent 能拒绝回答或引导回代码审查场景。但实际结果是:

{
  "output": "现在是2026年4月13日,上午10点25分05秒。"
}

Agent 毫不犹豫地调用了时间工具,完全无视了系统提示词中“只会做代码审查”的限制。


4.2 原理解析:ReAct 循环的决策优先级

这个现象并非 Bug,而是 LangGraph / ReAct Agent 的设计机制决定的。

ReAct 循环流程

  1. 接收输入 → 将 SystemMessage + HumanMessage 组装为上下文。
  2. LLM 思考 → 分析用户意图,扫描可用工具列表。
  3. 工具选择 → 如果存在匹配工具,优先执行工具调用
  4. 观察结果 → 工具返回结果后,再结合系统提示词生成最终回答。

关键结论

当可用工具能够直接完成任务时,工具调用的优先级高于系统提示词的“人设”约束。

这是因为 LLM 被训练为“高效解决问题”,而调用工具是解决问题的最短路径。系统提示词在此时更像“语气和风格的指导”,而非硬性规则拦截器。


4.3 验证实验

为了确认系统提示词本身是否有效,我进行了一组对照实验:

实验 1:有工具时(默认状态)

  • 输入{"input": "现在几点?"}
  • 输出"现在是2026年4月13日,上午10点25分05秒。"

实验 2:移除工具后(tools = []

  • 输入{"input": "现在几点?"}
  • 输出"我无法提供当前时间,我的职责是协助进行 Unity C# 代码审查。如果您有相关代码需要审查,请提供给我。"

对比结果:系统提示词在无工具干扰时完美生效,证明了提示词本身设计是正确且有效的。


4.4 解决方案与优化思路

针对这一问题,有三种渐进式的解决方案:

方案一:按场景启用工具(简单实用)

  • 在不需要时间功能时,直接设置 tools = []
  • 适用于功能单一、边界明确的 Agent。

方案二:增强工具描述(轻量级优化)

  • 在 @tool 的 docstring 中加入使用条件
@tool
def get_current_time() -> str:
    """
    获取当前日期和时间。
    注意:只有在与代码审查相关的场景下使用(例如为日志建议添加时间戳)。
    对于普通闲聊问题,不要使用此工具,应拒绝回答并引导回代码审查。
    """
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  • Agent 在读取工具描述时,会多一层“何时使用”的判断逻辑。

方案三:前置意图分类(工程化方案,推荐写入简历)

  • 在 Agent 调用前,增加一个轻量级意图分类器(可以是简单规则或小型 LLM)。
  • 分类流程:
    1. 用户输入 → 判断是否属于“代码审查”范畴。
    2. 不属于 → 直接返回预设回复,不进入 Agent 工具调用循环。
    3. 属于 → 进入完整 Agent 流程,必要时调用工具。
  • 这能显著提升 Agent 的专业性和用户体验,也是游戏工业化管线中 Agent 设计的常见模式。

4.5 总结与收获

关键点说明
Agent 行为优先级工具调用 > 系统提示词约束
提示词验证方法通过移除工具进行对照测试
实战优化思路工具描述注入使用条件 / 前置意图分类
对后续项目的意义在设计“Unity 代码审查 Agent”时,可以放心地为它添加“读取文件”“执行静态分析”等工具,同时通过前置分类确保它不会沦为“万能聊天机器人”

这次小小的踩坑,让我对 Agent 的“大脑”运作方式有了更直观的理解,也积累了可落地的优化经验。


五、让 Agent 更专业:结构化输出与工程化落地

在上一章中,我们已经成功搭建了基础的代码审查链路,AI 能够用自然语言给出审查建议。但在真实的游戏工业化管线中,自然语言回复是无法被下游工具直接解析的

举个例子:如果我们想把审查能力集成到 Unity 编辑器插件中,插件需要知道“哪一行代码有问题”“问题严重等级”,才能自动高亮并跳转。如果 CI/CD 流水线要自动拦截不合规的代码提交,它依赖一个明确的 true/false 来判断是否放行。自然语言段落无法满足这些需求。

因此,本章的目标是:让 LLM 的输出从“自由文本”变成“固定 JSON Schema”,为后续工具链集成打下基础。

5.1 技术选型与踩坑

LangChain 提供了两种主流的结构化输出方案:

方案原理优点缺点
llm.with_structured_output()利用模型原生的 response_format 或 Tool Calling代码简洁,模型直接返回 JSON依赖 API 支持,部分服务商未开放
PydanticOutputParser在提示词中注入 JSON Schema,LLM 返回字符串后由 Pydantic 解析通用性强,兼容所有 LLM需编写格式说明,对提示词有一定依赖

我首先尝试了更简洁的 with_structured_output

structured_llm = llm.with_structured_output(CodeReviewResult)
review_chain = prompt_template | structured_llm

但调用 /review 接口时,DeepSeek API 返回了如下错误:

openai.BadRequestError: Error code: 400 - {'error': {'message': 'This response_format type is unavailable now'}}

踩坑结论:DeepSeek 当前版本尚未完全支持 response_format 参数。这意味着对于这类 API,我们必须切换到更通用的 PydanticOutputParser 方案。这也恰好印证了 Agent 开发中“兼容性优先”的工程原则。

5.2 定义输出结构

首先,用 Pydantic 定义一份严格的输出契约:

from pydantic import BaseModel, Field

class CodeReviewResult(BaseModel):
    """Unity C# 代码审查结果"""
    has_problem: bool = Field(description="代码是否存在问题")
    issues: list[str] = Field(description="发现的问题列表,如命名不规范、性能隐患等")
    suggestions: list[str] = Field(description="具体的优化建议列表")
    improved_code: str = Field(description="优化后的完整代码片段")

这个模型不仅是 Python 中的类型约束,Field(description=...) 中的文本会被 PydanticOutputParser 提取并转化为 LLM 能理解的格式指令。

5.3 构建解析器与提示词模板

接下来,创建解析器并将格式说明注入提示词:

from langchain_core.output_parsers import PydanticOutputParser

parser = PydanticOutputParser(pydantic_object=CodeReviewResult)

review_prompt = PromptTemplate(
    input_variables=["code"],
    template="""
你是专业的 Unity C# 代码审查助手。
请审查以下代码,并严格按照 JSON 格式输出审查结果。

待审查代码:
{code}

{format_instructions}

只输出 JSON,不要添加任何额外说明或 Markdown 代码块标记。
""",
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

这里的关键是 parser.get_format_instructions()。它会自动生成一段包含 JSON Schema 的指令文本,并注入到提示词中。LLM 在生成回复时,会以这段 Schema 为约束,逐 token 输出符合格式的 JSON 字符串。

5.4 组装链路与接口

使用 LCEL 管道语法将组件串联,并封装为 FastAPI 接口:

review_chain = review_prompt | llm | parser

class CodeReviewRequest(BaseModel):
    code: str = Field(..., description="待审查的 Unity C# 代码")

@app.post("/review")
async def review_code(request: CodeReviewRequest):
    result = await review_chain.ainvoke({"code": request.code})
    return result

链末尾的 | parser 负责将 LLM 返回的原始字符串自动解析为 CodeReviewResult 对象,FastAPI 会进一步将其序列化为 JSON 返回给前端。

5.5 实测效果

向 /review 接口发送一段有问题的 C# 代码:

请求

{
  "code": "public int age;"
}

返回结果(Swagger UI 截图):

image.png

{
  "has_problem": true,
  "issues": [
    "字段命名不符合C#命名规范(应使用PascalCase)",
    "字段访问权限过于宽松(public)",
    "缺少XML注释"
  ],
  "suggestions": [
    "将字段名改为PascalCase命名法,例如‘Age’",
    "考虑将字段设为私有(private)或受保护(protected),并通过属性(property)提供受控的访问",
    "为字段添加XML注释以说明其用途"
  ],
  "improved_code": "/// <summary>\n/// 角色的年龄。\n/// </summary>\n[SerializeField]\nprivate int age;\n\n/// <summary>\n/// 获取或设置角色的年龄。\n/// </summary>\npublic int Age\n{\n    get => age;\n    set => age = value;\n}"
}

结果分析

  • has_problem 准确识别了代码缺陷。
  • issues 列表从三个维度指出了问题(命名、封装、注释)。
  • improved_code 给出了可直接替换的优化版本,且包含了 [SerializeField] 这样的 Unity 特有用法。

作为对比,在本章实现结构化输出之前,我们通过 /chain 接口得到的审查结果是自然语言段落。虽然内容专业,但程序无法直接提取“是否存在问题”的布尔值,也无法分离“问题列表”和“建议列表”。结构化输出解决了这一痛点。

5.6 工业级思考(后续优化方向)

虽然当前功能已能稳定返回结构化数据,但在实际生产环境中仍需考虑以下问题:

1. 输出格式异常容错

LLM 偶尔会在 JSON 外包裹 Markdown 代码块标记(````json`)或遗漏括号。一个稳健的工程化方案通常会增加输出清洗层,或在解析失败时触发重试机制。这部分将在后续项目完善阶段加入。

2. 与 Agent 的联动

目前 /agent 接口仍返回自然语言,但结构化输出的思想完全可以延伸过去。在下一阶段的计划中,我们将尝试让 Agent 的最终输出同样遵循 Pydantic Schema,从而让上游调度系统能够精确解析 Agent 的决策结果。

5.7 本章小结

关键点收获
结构化输出的意义让 Agent 从“对话玩具”升级为“可集成的生产工具”
方案对比与踩坑with_structured_output 虽简洁,但 DeepSeek 暂不支持,改用 PydanticOutputParser 解决
通用方案原理通过 parser.get_format_instructions() 将 Pydantic 模型转为 JSON Schema 注入提示词
工程思考识别了容错、联动等后续优化方向,为工业化落地铺路

本章完成的功能,将成为后续 Unity 代码审查 Agent 项目的输出标准


第五章补充:再探 Agent 行为优先级——计算器工具的意外“越界”

注:本节内容为 4.16 周四 Agent 实战中的额外发现,与第四章“系统提示词 vs 时间工具”形成对照,故作为补充案例置于此处。

在完成结构化输出功能后,我回过头对之前构建的 Agent 进行了更全面的边界测试。第四章中,我观察到 Agent 在强提示词约束下拒绝了时间查询。但当我用一个数学计算请求测试时,Agent 的行为却出乎意料。

测试输入(通过 Swagger UI 调用 /agent 接口):

{
  "input": "计算 123 * 456"
}

Agent 返回

{
  "output": "123 × 456 = 56088"
}

image 1.png

对比:时间查询 vs 数学计算

测试场景用户输入Agent 行为是否遵守“只做代码审查”约束
时间查询"现在几点?"拒绝回答,主动引导回审查领域✅ 遵守
数学计算"计算 123 * 456"直接调用计算器工具,返回精确结果❌ 违反

image 2.png

image 1.png

为什么同一套提示词,行为却截然相反?

关键在于工具描述与用户意图的匹配程度

  • 时间工具的描述中,我明确注入了约束条件:“只有在与代码审查相关的场景下使用”。因此当用户仅询问时间时,Agent 在推理阶段识别到不满足使用条件,从而选择不调用工具。
  • 计算器工具的描述则相对通用:“当用户提出数学计算问题时使用”。面对纯粹的数学计算请求,这条描述与用户意图形成了完美匹配,触发了 ReAct 循环中“解决问题优先”的默认倾向,覆盖了系统提示词的人设约束。

现象背后的机制

这一对比实验进一步揭示了 LangGraph ReAct Agent 的决策优先级:

工具描述 > 用户意图匹配度 > 系统提示词的人设约束

当工具描述足够宽泛、与用户请求高度契合时,Agent 会倾向于“先解决问题再说”。系统提示词更像一层“软约束”,在工具调用意愿不强时发挥主导作用;但当工具“自告奋勇”时,提示词的人设限制就可能被绕过。

工程启示

这一现象并非 Bug,而是 Agent 设计中的核心权衡。对于工业级应用,我们需要更精细的控制策略:

  1. 工具描述注入领域边界:像时间工具那样,在 docstring 中明确写出使用前置条件。
  2. 前置意图分类:在进入 Agent 循环前判断用户意图,非领域请求直接拦截。
  3. 分级工具权限:根据用户意图动态屏蔽与核心领域无关的工具。

这次补充实验进一步印证了第四章的核心结论,也为下一章“工程化优化方案”中的“增强工具描述约束”提供了直接的实证支撑。


六、工程化落地:意图分类 + 结构化输出 + 思考链可视化

在第四章中,我们观察到 Agent 在工具调用与系统提示词之间存在行为优先级博弈;第五章我们为 Chain 实现了结构化输出,让审查结果可被程序解析。本章将把这两条线索汇聚,打造一个真正可接入游戏工业化管线的工程级 Agent 服务

6.1 问题回顾:从“能用”到“好用”的最后一公里

回顾此前的 Agent 接口(/agent),存在三个工程化短板:

痛点具体表现工程影响
边界模糊面对计算请求,Agent 会调用计算器工具“越界”回答无法保证服务只处理代码审查,上游调用方需要额外校验
输出不可解析返回自然语言文本,程序难以提取 has_problem 等关键字段编辑器插件、CI 流水线无法自动响应
内部黑盒无法知晓 Agent 是否调用了工具、经历了哪些推理步骤调试困难,生产环境排错成本高

为解决这些问题,我们需要对 Agent 进行工程化加固,具体目标:

  1. 前置意图分类:非代码审查请求直接拦截,不进入工具调用循环。
  2. 输出结构化:与第五章的 CodeReviewResult 对齐,返回可解析的 JSON。
  3. 思考链透明化:暴露 Agent 内部的推理、工具调用、观察结果序列。

6.2 整体架构设计

在原有 agent_graph(ReAct Agent)基础上,我们在 FastAPI 接口层增加以下组件:

用户输入
    ↓
[ 意图分类器 ] → 非代码相关 → 直接返回拒绝响应
    ↓ 代码相关
[ SystemMessage 约束 ][ agent_graph (ReAct 循环) ][ 思考链提取器 ] ← 解析 messages
    ↓
[ 调用 /review 结构化审查链 ][ 组装 AgentReviewResponse ] → 返回 JSON

其中 AgentReviewResponse 是一个嵌套 Pydantic 模型,复用了第五章定义的 CodeReviewResult

6.3 核心代码实现

6.3.1 定义结构化响应模型

class AgentReviewResponse(BaseModel):
    """Agent 审查的完整结构化响应"""
    success: bool = Field(description="请求是否成功处理")
    is_code_related: bool = Field(description="用户输入是否与代码审查相关")
    review_result: Optional[CodeReviewResult] = Field(
        default=None,
        description="审查结果(仅当 is_code_related=True 时有值)"
    )
    reasoning: str = Field(description="Agent 的推理过程简述")
    tool_calls_made: List[str] = Field(default=[], description="实际调用的工具列表")
    thinking_chain: List[Dict[str, Any]] = Field(default=[], description="完整的思考链记录")

6.3.2 轻量级意图分类器

CODE_REVIEW_KEYWORDS = [
    "代码", "审查", "review", "c#", "unity", "mono", "update",
    "gameobject", "transform", "public", "private", "class"
]

def is_code_review_intent(user_input: str) -> bool:
    input_lower = user_input.lower()
    return any(keyword in input_lower for keyword in CODE_REVIEW_KEYWORDS)

设计说明:关键词匹配是 Demo 级方案。工业场景可替换为小型 BERT 分类模型或调用一次轻量 LLM 做二分类,但原理相同。

6.3.3 思考链提取函数

这是本章的核心逻辑之一,从 LangGraph 返回的 messages 列表中逐条解析:

def extract_thinking_chain(messages: List[Any]) -> tuple[List[Dict], List[str], str]:
    thinking_chain = []
    tool_calls_made = []
    reasoning_parts = []

    for msg in messages:
        msg_type = type(msg).__name__

        if msg_type in ["SystemMessage", "HumanMessage"]:
            thinking_chain.append({"type": msg_type, "content": msg.content[:200]})

        elif msg_type == "AIMessage":
            entry = {"type": "AIMessage", "content": msg.content}
            if hasattr(msg, "tool_calls") and msg.tool_calls:
                entry["tool_calls"] = [{"name": tc["name"], "args": tc["args"]} for tc in msg.tool_calls]
                tool_names = [tc["name"] for tc in msg.tool_calls]
                tool_calls_made.extend(tool_names)
                reasoning_parts.append(f"调用工具:{', '.join(tool_names)}")
            else:
                if msg.content:
                    reasoning_parts.append(msg.content[:100])
            thinking_chain.append(entry)

        elif msg_type == "ToolMessage":
            thinking_chain.append({
                "type": "ToolMessage",
                "tool_name": getattr(msg, "name", "unknown"),
                "result": msg.content[:200]
            })

    final_reasoning = " → ".join(reasoning_parts) if reasoning_parts else "直接回答,未调用工具"
    return thinking_chain, tool_calls_made, final_reasoning

该函数能区分 AIMessage(含工具调用请求)、ToolMessage(工具执行结果)以及普通对话消息,为上层提供完整的审计追踪。

6.3.4 整合接口

@app.post("/agent/structured")
async def run_structured_agent(request: AgentReviewRequest):
    # 1. 前置意图拦截
    if not is_code_review_intent(request.input):
        return AgentReviewResponse(
            success=True,
            is_code_related=False,
            review_result=None,
            reasoning="用户输入与代码审查无关,已拦截",
            thinking_chain=[{"type": "IntentFilter", "result": "rejected"}]
        ).model_dump()

    # 2. 执行 Agent(复用周四的 agent_graph)
    system_prompt = SystemMessage(content="你是专业的 Unity C# 代码审查助手...")
    result = await agent_graph.ainvoke({
        "messages": [system_prompt, HumanMessage(content=request.input)]
    })

    # 3. 提取思考链
    messages = result["messages"]
    thinking_chain, tool_calls_made, reasoning = extract_thinking_chain(messages)

    # 4. 调用结构化审查链(复用周三的 review_chain)
    review_result = await review_chain.ainvoke({"code": request.input})

    # 5. 组装响应
    return AgentReviewResponse(
        success=True,
        is_code_related=True,
        review_result=review_result,
        reasoning=reasoning,
        tool_calls_made=tool_calls_made,
        thinking_chain=thinking_chain
    ).model_dump()

6.4 实测效果

场景一:代码审查请求

请求

{
  "input": "public int age;"
}

响应(节选):

{
  "success": true,
  "is_code_related": true,
  "review_result": {
    "has_problem": true,
    "issues": [
      "字段命名不符合C#命名规范(应使用PascalCase)",
      "字段访问权限过于开放(public)",
      "缺少XML注释"
    ],
    "suggestions": [
      "将字段名改为PascalCase命名规范,例如‘Age’",
      "考虑将字段访问权限设为private或protected,并通过属性(Property)进行访问控制",
      "为字段添加XML注释以说明其用途"
    ],
    "improved_code": "/// <summary>\n/// 角色的年龄\n/// </summary>\n[SerializeField]\nprivate int _age;\n\npublic int Age\n{\n    get => _age;\n    set => _age = value;\n}"
  },
  "reasoning": "我主要专注于Unity C#代码审查,包括代码规范、性能优化和面向对象设计建议。您提供的代码片段 `public int age;` 是一个简单的公共字段声明。\n\n不过,为了给您提供更有价值的代码审查",
  "tool_calls_made": [],
  "thinking_chain": [...]
}

image 4.png

场景二:无关问题拦截

请求

{
  "input": "现在几点?"
}

响应

{
  "success": true,
  "is_code_related": false,
  "review_result": null,
  "reasoning": "用户输入与代码审查无关,已拦截",
  "tool_calls_made": [],
  "thinking_chain": [{"type": "IntentFilter", "result": "rejected"}]
}

image 5.png

效果对比

测试用例原 /agent 行为工程化 /agent/structured 行为
"现在几点?"拒绝回答(依赖提示词软约束)前置拦截,不进入 Agent 循环
"计算 123 * 456"调用计算器工具,越界回答前置拦截,不进入 Agent 循环
"public int age;"返回自然语言审查建议返回结构化 JSON + 思考链

6.5 工程价值与可扩展性

本章完成的 /agent/structured 接口已具备以下工程属性:

  1. 确定性输出:无论何种输入,返回格式始终为 AgentReviewResponse,上游调用方可放心解析。
  2. 边界守护:意图分类器作为“门禁”,保证 Agent 能力不被滥用,降低无效算力消耗。
  3. 可观测性thinking_chain 字段提供了完整的决策审计日志,便于生产环境问题回溯。
  4. 高复用性:直接复用了第三章的 agent_graph 和第四章的 review_chain,体现 LangChain 组件化优势。

后续演进方向

  • 将关键词分类器替换为轻量级 LLM 调用(如 DeepSeek 的小参数模型)做语义级意图判断。
  • 在 thinking_chain 中增加耗时统计,用于性能分析。
  • 与 Unity Editor 插件或 CI 流水线对接,实现真正的“AI 驱动研发管线”。

6.6 本章小结

技术点实现方式解决的核心问题
前置意图分类关键词匹配函数阻止非代码审查请求进入 Agent 循环
输出结构化Pydantic 嵌套模型 AgentReviewResponse让上游程序可解析审查结果
思考链透明化遍历 messages 并分类提取提供完整的决策审计追踪
组件复用调用 agent_graph + review_chain避免重复开发,体现架构分层

至此,我们从零到一构建了一个可部署、可观测、边界清晰的 Unity C# 代码审查 Agent。


七、工程化加固:从“能跑”到“生产级”的最后一步

在第六章中,我们为 Agent 添加了意图分类、结构化输出和思考链可视化,让它从一个“会聊天的 Demo”升级为“可集成的工具”。但在真实的游戏工业化管线中,仅靠这些还不够——服务必须稳定、可观测、能容错

想象一下:如果 CI/CD 流水线在凌晨自动运行代码审查时,因为 LLM 偶尔返回的 JSON 少了一个括号就导致整个流程崩溃,那这个 Agent 就不是“生产力工具”,而是“定时炸弹”。

本章的目标,就是为我们的 Agent 加上最后一道防线:容错清洗、重试机制、统一异常处理和结构化日志。这也是区分“Demo 项目”和“生产级服务”的关键分水岭。

7.1 为什么需要容错?—— LLM 输出的不确定性

尽管我们在第五章中通过 PydanticOutputParser 向 LLM 注入了严格的 JSON Schema 指令,但大语言模型本质上是概率生成模型,它偶尔还是会“叛逆”:

  • 输出被 Markdown 代码块包裹:````json { ... }`
  • 前后附带解释性文字:“好的,以下是审查结果:{ ... }”
  • JSON 格式微瑕:少一个闭合括号 },或多一个逗号

这些问题对于人类阅读者无伤大雅,但对于程序化的 json.loads() 或 Pydantic 验证来说,是致命的——一次解析失败就可能导致整个接口返回 500 错误

因此,我们需要在解析层增加一层“缓冲垫”,让服务具备自修复能力

7.2 输出清洗函数:让“脏数据”变干净

我们设计一个 clean_llm_output 函数,专门处理 LLM 输出中的常见噪音:

import re

def clean_llm_output(raw_output: str) -> str:
    """清洗 LLM 原始输出,移除 Markdown 标记和多余文字"""
    if not raw_output:
        return ""

    cleaned = raw_output.strip()

    # 1. 提取 Markdown 代码块中的 JSON
    json_pattern = r"```(?:json)?\s*\n?([\s\S]*?)\n?```"
    match = re.search(json_pattern, cleaned)
    if match:
        cleaned = match.group(1).strip()
        logger.info("从 Markdown 代码块中提取了 JSON")

    # 2. 提取第一个 { 到最后一个 } 之间的内容
    first_brace = cleaned.find("{")
    last_brace = cleaned.rfind("}")
    if first_brace != -1 and last_brace != -1:
        cleaned = cleaned[first_brace:last_brace + 1]

    # 3. 移除常见的中英文前缀
    common_prefixes = ["以下是审查结果:", "审查结果:", "Result:", "Output:"]
    for prefix in common_prefixes:
        if cleaned.startswith(prefix):
            cleaned = cleaned[len(prefix):].strip()

    # 4. 自动补全缺失的闭合括号
    if cleaned.count("{") > cleaned.count("}"):
        missing = cleaned.count("{") - cleaned.count("}")
        cleaned = cleaned + "}" * missing
        logger.warning(f"自动补全了{missing} 个缺失的闭合括号")

    return cleaned.strip()

设计思路:通过多层正则和字符串操作,尽可能从“脏”输出中抢救出有效的 JSON 字符串。

7.3 带重试的安全解析:不轻言放弃

有了清洗函数,我们还需要一个带重试逻辑的解析器

async def safe_parse_with_retry(raw_output: str, parser: PydanticOutputParser, max_retries: int = 2):
    """安全解析 LLM 输出,支持清洗和重试"""
    last_error = None
    current_output = raw_output

    for attempt in range(max_retries + 1):
        try:
            if attempt == 0:
                logger.info("尝试直接解析 LLM 原始输出...")
                return parser.parse(current_output)

            logger.info(f"第{attempt} 次重试:清洗输出...")
            cleaned = clean_llm_output(current_output)
            return parser.parse(cleaned)

        except (OutputParserException, json.JSONDecodeError) as e:
            last_error = e
            logger.warning(f"解析失败 (尝试{attempt + 1}/{max_retries + 1}):{str(e)[:200]}")

    raise HTTPException(status_code=422, detail={
        "error": "LLM 输出格式解析失败,已尝试所有修复策略",
        "last_error": str(last_error)
    })

7.4 统一异常处理:让错误响应也专业

通过 FastAPI 的全局异常处理器,让所有异常返回格式一致的 JSON 响应

from fastapi.responses import JSONResponse
from langchain_core.exceptions import OutputParserException

@app.exception_handler(OutputParserException)
async def output_parser_exception_handler(request, exc: OutputParserException):
    logger.error(f"OutputParserException:{str(exc)[:200]}")
    return JSONResponse(
        status_code=422,
        content={"success": False, "error": "LLM 输出格式解析失败", "detail": str(exc)[:500]}
    )

@app.exception_handler(Exception)
async def global_exception_handler(request, exc: Exception):
    logger.exception(f"未处理的异常:{str(exc)}")
    return JSONResponse(
        status_code=500,
        content={"success": False, "error": "服务内部错误"}
    )

7.5 结构化日志:为生产排错装上“行车记录仪”

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("code_review_agent")

7.6 增强版接口:/review/safe 与 /agent/structured/safe

基于以上组件,我们封装了两个生产级接口:

@app.post("/agent/structured/safe")
async def safe_structured_agent(request: AgentReviewRequest):
    logger.info(f"收到 Agent 请求,输入长度:{len(request.input)}")
    # ... 意图分类、Agent 执行、容错解析、日志记录 ...

7.7 实测效果:从容错设计到生产级稳定性

测试输入

{
  "input": "public int age;"
}

终端日志输出

2026-04-18 15:08:58 [INFO] code_review_agent - 收到 Agent 请求,输入长度: 15
2026-04-18 15:08:58 [INFO] code_review_agent - 意图分类:代码相关,进入 Agent 流程
2026-04-18 15:08:58 [INFO] code_review_agent - 开始执行 Agent 图...
2026-04-18 15:09:04 [INFO] code_review_agent - Agent 执行完成
2026-04-18 15:09:04 [INFO] code_review_agent - 工具调用记录: []
2026-04-18 15:09:04 [INFO] code_review_agent - 开始调用结构化审查链...
2026-04-18 15:09:11 [INFO] code_review_agent - 尝试直接解析 LLM 原始输出...
2026-04-18 15:09:11 [INFO] code_review_agent - 结构化审查完成,has_problem=True

image 6.png

关键观察

  1. 意图分类生效:输入被正确判定为代码相关。
  2. 思考链完整:工具调用记录为空,thinking_chain 完整记录流转。
  3. 结构化解析成功:首次解析即成功,DeepSeek 输出格式稳定。
  4. 日志可追溯:每个步骤都有时间戳和日志级别。

容错机制的意义:本次测试中清洗和重试逻辑未被触发——但这恰恰证明了容错设计的价值。它就像汽车的“安全气囊”,平时感觉不到,关键时刻能救命。

7.8 本章小结

加固维度技术手段解决的问题
容错性输出清洗 + 重试解析LLM 输出格式不稳定导致的解析崩溃
可观测性结构化日志生产环境难以追踪请求和排错
稳定性全局异常处理异常响应格式不一致,调用方难以处理

经过本章的工程化加固,我们的 Unity C# 代码审查 Agent 已经具备了以下生产级特性:

  • ✅ 结构化输出(第五章)
  • ✅ 意图分类与思考链可视化(第六章)
  • ✅ 容错清洗与重试(本章)
  • ✅ 统一异常处理(本章)
  • ✅ 结构化日志(本章)

这不再是一个“玩具级”的 Demo,而是一个健壮、可观测、可部署的 AI Agent 服务。它可以被自信地接入 Unity 编辑器插件、CI/CD 流水线,或作为微服务部署到云端,真正实现“AI 驱动的敏捷研发新模式”——这也正是我在实习岗位中希望参与构建的系统。