基于LangChain与FastAPI构建Unity C#代码审查智能体:从原理到工程实践

📅 2026/7/12 9:19:42
基于LangChain与FastAPI构建Unity C#代码审查智能体:从原理到工程实践
1. 项目概述为什么我们需要一个Unity C#代码审查Agent如果你是一个Unity游戏开发团队的Tech Lead或者是一个独立开发者每天面对成百上千行新写的C#脚本你可能会面临一个共同的困境如何保证代码质量命名规范不统一、性能隐患比如在Update里频繁调用GetComponent、糟糕的面向对象设计比如滥用单例模式……这些问题在项目初期可能不显眼但随着项目规模扩大它们会像滚雪球一样让项目变得难以维护、性能低下最终拖垮整个团队。传统的解决方案是什么靠资深程序员人工Review或者引入SonarQube这类静态代码分析工具。前者成本太高后者对Unity这种游戏引擎特有的API和模式比如MonoBehaviour生命周期、协程、Addressables支持有限规则配置复杂反馈也不够直观。有没有一种方法能结合两者的优点——既像资深专家一样理解Unity最佳实践又能像自动化工具一样7x24小时工作还能用自然语言跟你讨论代码这就是我动手构建这个Unity C#代码审查Agent的初衷。它不是一个简单的代码格式化工具而是一个具备“思考”能力的智能体。它能理解你的代码意图结合Unity引擎的特性和C#语言规范给出有上下文、可落地的优化建议。更重要的是我把它从一个简单的“问答链”一步步升级成了一个具备自主决策、工具调用、结构化输出和工程化加固的“生产级”服务。整个过程我踩遍了从LangChain基础链到LangGraph智能体再到FastAPI工程化落地的每一个坑。这篇文章就是这份实战经验的完整记录。2. 技术选型与架构设计为什么是FastAPI LangChain DeepSeek在动手之前技术栈的选择决定了项目的上限和下限。我最终敲定的核心组合是FastAPI作为后端框架LangChain作为AI应用编排框架LangGraph用于构建智能体工作流DeepSeek作为大语言模型LLM提供“大脑”。2.1 后端框架为什么是FastAPI而不是Flask或Django对于一个AI Agent服务尤其是需要处理流式响应、并发请求的代码审查场景FastAPI有几点不可替代的优势异步支持Async/AwaitLLM API调用是典型的I/O密集型操作动辄几秒的响应时间。FastAPI原生支持异步可以轻松地用async/await处理LangChain的异步调用避免阻塞工作线程极大提升并发能力。相比之下Flask的异步生态不够成熟Django又显得过于臃肿。自动API文档FastAPI基于Pydantic类型提示自动生成交互式API文档Swagger UI和ReDoc。这对于需要前后端联调或者让其他团队成员快速测试Agent能力的场景来说是巨大的效率提升。你定义好请求/响应模型文档就生成了。高性能基于Starlette和PydanticFastAPI本身性能表现就非常出色对于需要快速响应的微服务架构是理想选择。2.2 AI应用框架为什么是LangChain LangGraph这是整个项目的核心。很多人分不清LangChain和LangGraph的关系简单来说LangChain是构建AI应用的“乐高积木”。它提供了连接LLM、管理记忆、处理输入输出如提示词模板、输出解析器等一系列标准化组件。它的核心抽象是Chain链可以把多个步骤如“查询 - 检索 - 生成”串联起来。LangGraph是构建有状态、多步骤工作流的框架。它基于LangChain但引入了“图”Graph的概念。智能体Agent的本质就是一个循环的工作流思考Thought - 行动Action调用工具- 观察Observation- 再思考。LangGraph让你能清晰地定义这个循环中的各个节点Node和它们之间的流转条件Edge非常适合构建复杂的Agent。对于代码审查Agent我一开始用LangChain的LLMChain搭建了基础版本但很快发现它能力有限是线性的。当我需要Agent能“主动”去获取信息比如查一下当前时间来判断一段日志代码的时间戳格式是否正确时就必须引入工具调用和循环决策这时LangGraph就成了不二之选。2.3 大模型为什么选择DeepSeek市面上LLM选择很多我选择DeepSeek-V3主要基于以下几点考虑强大的代码能力DeepSeek在多项代码生成和理解的基准测试中表现突出对C#和Unity API有很好的理解。出色的上下文长度支持128K上下文这意味着我可以把一整段复杂的Unity脚本连同其相关的类定义都塞给它分析不用担心截断。极高的性价比对于个人项目或创业公司来说成本是必须考虑的因素。DeepSeek的API价格非常有竞争力同时性能不打折扣。对国内开发者友好API访问稳定文档齐全没有复杂的网络门槛。这个技术栈组合保证了项目从原型验证到生产部署的全链路通畅。3. 从Chain到Agent理解智能体的进化之路很多教程一上来就讲Agent但如果不理解基础的Chain就很难把握Agent“智能”在哪里。我的构建路径是循序渐进的LLMChain - ReAct Agent - 工程化Agent服务。3.1 基础用LCEL构建你的第一个代码审查Chain在LangChain的现代写法中推荐使用**LCELLangChain Expression Language**来构建链。它的语法非常直观用管道符|连接各个组件。from langchain_core.prompts import PromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser # 1. 定义提示词模板告诉模型你的角色和任务 review_prompt PromptTemplate.from_template( 你是一名资深的Unity游戏引擎开发专家精通C#语言规范和性能优化。 请严格审查以下Unity C#代码指出其中的代码规范问题、潜在性能隐患以及面向对象设计缺陷并给出具体的优化建议和修改后的代码。 待审查代码 {code} 请按以下格式回答 1. **规范问题**[列出问题] 2. **性能隐患**[列出问题] 3. **设计缺陷**[列出问题] 4. **优化建议**[给出建议] 5. **改进代码**[提供修改后的完整代码片段] 注意回答必须基于Unity最佳实践如避免在Update中执行耗时操作、正确使用SerializeField、合理运用对象池等。 ) # 2. 初始化LLM这里以DeepSeek为例实际使用需配置base_url和api_key llm ChatOpenAI( modeldeepseek-chat, temperature0.1, # 低温度保证输出稳定、专业 base_urlhttps://api.deepseek.com, api_keyyour_api_key ) # 3. 使用LCEL组装链提示词 - 大模型 - 字符串解析器 review_chain review_prompt | llm | StrOutputParser() # 4. 调用链 async def review_code(code_snippet: str): result await review_chain.ainvoke({code: code_snippet}) return result这就是一个最简单的Chain。它的工作流是固定的、线性的用户输入代码 - 套入提示词模板 - 发送给LLM - 解析输出。它不会“思考”也不会去调用任何外部工具。但对于很多简单的代码审查任务这已经能提供非常有价值的建议了。踩坑心得一提示词工程是灵魂最初的提示词我写得很简单“请审查这段代码”。结果模型经常给出笼统的建议比如“建议优化性能”。后来我借鉴了人类Code Review的清单在提示词中明确列出了审查的维度规范、性能、设计并要求结构化输出模型的表现立刻提升了一个档次。给AI明确的指令和格式它才能给你高质量的回报。3.2 进化引入ReAct Agent与工具调用Chain很好但不够“智能”。假设它审查一段日志代码发现里面用了DateTime.Now.ToString(yyyy-MM-dd)它可能只会说“建议使用ISO8601格式”。但如果它能知道当前的日期它或许能发现代码里写死了年份是“2023”而今年已经是2025年了这是一个逻辑错误。这就需要Agent。Agent的核心能力是工具调用Tool Calling。我为你实现的Agent采用了ReActReasoning Acting范式这是目前最主流的Agent推理框架。它的工作流程是一个循环思考ThoughtLLM分析当前情况用户问题、历史对话、可用工具决定下一步该做什么。行动Action如果思考后认为需要调用工具它就选择最合适的工具并生成调用参数。观察Observation执行工具获取结果比如当前时间、计算的结果、查询数据库的记录。将观察结果作为新的输入回到第1步继续思考直到LLM认为信息足够给出最终答案。在LangGraph中用create_react_agent可以一键构建这个循环。但在此之前我们需要先定义“工具”。from langchain_core.tools import tool from datetime import datetime tool def get_current_time() - str: 获取当前的系统日期和时间格式为 YYYY-MM-DD HH:MM:SS。 **重要仅在审查的代码逻辑与时间戳、日志日期、定时任务等时间相关场景时使用此工具。** 对于普通的询问时间请求应拒绝并引导用户回到代码审查主题。 return datetime.now().strftime(%Y-%m-%d %H:%M:%S) tool def calculate_expression(expression: str) - str: 计算一个基础的数学表达式支持加减乘除和括号。 例如: \(10 2) * 3\, \100 / 4\。 **使用场景**当审查的代码中包含硬编码的魔法数字或复杂的数值计算逻辑时可用此工具验证计算结果或提供优化建议。 try: # 警告生产环境请使用更安全的库如numexpr或ast.literal_eval此处仅为演示。 allowed_names {abs: abs, round: round} result eval(expression, {__builtins__: None}, allowed_names) return str(result) except Exception as e: return f计算错误: {e}定义工具的关键在于tool装饰器和那个详细的文档字符串docstring。LLM并不执行你的函数它只“看”这个函数的名称和docstring来决定是否调用以及如何调用。因此docstring里必须清晰说明工具的用途、输入格式和最重要的——使用边界。后面我们会看到这个边界描述是控制Agent行为的关键。有了工具和LLM创建Agent就一行代码from langgraph.prebuilt import create_react_agent tools [get_current_time, calculate_expression] agent_graph create_react_agent(llm, tools)agent_graph就是一个可执行的、具备ReAct推理能力的智能体图。3.3 实战踩坑当系统提示词遇上工具调用这里我遇到了第一个大坑也是理解Agent行为优先级的关键。我给了Agent一个非常明确的系统提示词“你是一个Unity C#代码审查助手只回答与代码审查相关的问题。”然后我问它“现在几点了”我期望它拒绝回答。但实际结果是它愉快地调用了get_current_time工具告诉我“现在是2025-07-27 14:30:05”。为什么因为在我的工具描述里虽然写了“仅在时间相关场景使用”但LLM在推理时发现用户意图问时间和工具能力获取时间匹配度极高而系统提示词是一种“软约束”。在ReAct循环中工具调用的优先级往往高于系统提示词的泛泛约束。LLM被训练成“解决问题优先”当有一个现成的工具能完美解决用户问题时它倾向于先解决问题。解决方案与深度思考强化工具描述约束我在get_current_time的docstring里加上了更严厉的警告如上所示强调仅用于代码审查上下文。这有一定效果但非绝对。动态工具管理更工程化的做法是在Agent工作流前加一个“路由层”。根据用户输入判断意图如果是纯聊天或无关查询就不把get_current_time这类工具提供给Agent。这需要维护一个工具白名单或黑名单。接受并利用这种行为实际上对于一个专注的代码审查Agent你本就不应该提供与核心功能无关的工具。我只保留了get_current_time和calculate_expression做测试在生产环境中我会为它配备真正有用的工具比如search_unity_documentation(query): 查询Unity官方API文档。analyze_code_complexity(code): 调用本地代码分析库计算圈复杂度。check_naming_convention(identifier): 验证命名是否符合团队规范。这个坑让我明白构建Agent不是简单地把工具丢给它而是设计一个受控的环境。系统提示词设定目标和风格工具集定义能力边界而前置的意图分类或路由逻辑则是守门人。4. 工程化落地从Demo脚本到生产服务一个能在Jupyter Notebook里跑通的Agent离能在团队中实际使用的服务还差着十万八千里。接下来我要解决三个核心工程问题意图过滤、结构化输出、可观测性。4.1 第一道防线意图分类器我不能让Agent浪费算力去回答“今天天气怎么样”这种问题。所以在请求进入核心的Agent图之前我需要一个轻量级的意图分类器进行拦截。CODE_REVIEW_KEYWORDS [ # 通用编程 代码, 审查, review, 检查, 优化, bug, 错误, 缺陷, 改进, # C# 相关 c#, csharp, class, interface, method, property, field, namespace, using, # Unity 相关 unity, monobehaviour, gameobject, transform, update, start, awake, serializefield, scriptableobject, prefab, asset, scene, # 代码片段特征常见开头 public, private, protected, void, int, string, float, bool, list, dictionary ] def is_code_review_intent(user_input: str) - bool: 基于关键词的轻量级意图分类。 返回True表示输入与代码审查相关应进入Agent流程。 if not user_input or len(user_input.strip()) 5: return False input_lower user_input.lower().strip() # 检查是否像是一段代码包含花括号、分号、括号等 if any(char in input_lower for char in [{, }, ;, (), ]): return True # 检查是否包含代码审查关键词 return any(keyword in input_lower for keyword in CODE_REVIEW_KEYWORDS)这是一个基于规则和关键词的简单分类器。它的优点是速度快、零成本、完全可控。缺点是可能误判比如用户说“这段代码的update逻辑有问题”但输入里没带代码片段。对于生产环境你可以升级为一个微调的小型文本分类模型或者直接调用一次LLM来做意图判断精度更高但会有延迟和成本。4.2 核心需求结构化输出Agent用自然语言回复“这里有个空引用风险”我的CI/CD流水线怎么自动判断这次提交是否通过我的Unity编辑器插件怎么高亮出具体行答案就是结构化输出。我需要LLM返回标准的JSON而不是一段文本。LangChain提供了PydanticOutputParser它能把Pydantic数据模型“编译”成LLM能理解的格式指令并自动解析LLM的回复。首先定义我希望的输出结构from pydantic import BaseModel, Field from typing import List, Optional class CodeIssue(BaseModel): line: Optional[int] Field(description问题所在的大致行号如能推断) category: str Field(description问题类别如命名规范、性能、内存、设计) description: str Field(description问题的具体描述) severity: str Field(description严重等级HIGH、MEDIUM、LOW) suggestion: str Field(description具体的修复建议) class CodeReviewResult(BaseModel): 代码审查的结构化结果 overall_score: float Field(ge0.0, le10.0, description代码质量综合评分10分制) has_critical_issue: bool Field(description是否存在严重HIGH级别问题) issues: List[CodeIssue] Field(default_factorylist, description发现的所有问题列表) improved_code_snippet: Optional[str] Field(description优化后的代码片段如果适用) summary: str Field(description审查总结摘要)然后创建解析器和提示词模板from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import PromptTemplate parser PydanticOutputParser(pydantic_objectCodeReviewResult) review_prompt PromptTemplate( template 你是一个专业的Unity C#代码审查专家。请严格分析以下代码并按照指定的JSON格式输出结果。 只输出JSON不要有任何额外的解释、前缀或后缀。 待审查代码 {code} {format_instructions} , input_variables[code], partial_variables{format_instructions: parser.get_format_instructions()} ) # 组装链 structured_chain review_prompt | llm | parserparser.get_format_instructions()会生成一段详细的文本描述JSON的格式和每个字段的含义LLM会努力遵循这个格式生成输出。踩坑心得二LLM的输出清洗即使用了PydanticOutputParserLLM有时还是会“画蛇添足”在JSON外面包一层json。你必须写一个输出清洗函数来容错。import re import json def clean_llm_json_output(raw_text: str) - str: 尝试从LLM的回复中提取纯净的JSON字符串。 if not raw_text: return text raw_text.strip() # 尝试匹配Markdown代码块中的JSON json_code_block re.search(r(?:json)?\s*(\{.*?\})\s*, text, re.DOTALL) if json_code_block: return json_code_block.group(1).strip() # 尝试匹配第一个{到最后一个}之间的内容 start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: potential_json text[start:end1] # 快速验证是否为合法JSON不保证完整但能过滤明显无效内容 try: json.loads(potential_json) return potential_json except json.JSONDecodeError: pass # 如果都不行返回原文本让parser去处理并记录日志 logger.warning(f无法清洗出标准JSON使用原始文本: {text[:200]}...) return text4.3 可观测性思考链可视化Agent之所以叫“智能体”是因为它有思考过程。在生产环境如果Agent给出了一个离谱的建议我必须能回溯它到底“想”了什么、调用了什么工具。这就是可观测性。LangGraph的Agent在执行后会返回一个完整的messages列表里面包含了所有的SystemMessage、HumanMessage、AIMessage包含思考内容和工具调用请求和ToolMessage工具执行结果。我需要解析这个列表def extract_agent_thought_process(messages): 从LangGraph返回的消息列表中提取可读的思考链。 thought_chain [] for msg in messages: if msg.type system: thought_chain.append(f[系统指令] {msg.content[:100]}...) elif msg.type human: thought_chain.append(f[用户输入] {msg.content[:100]}...) elif msg.type ai: # AI消息可能包含思考‘thought’和工具调用‘tool_calls’ content msg.content or if hasattr(msg, tool_calls) and msg.tool_calls: for tc in msg.tool_calls: thought_chain.append(f[思考] 决定调用工具 {tc[name]}参数: {tc[args]}) elif content: thought_chain.append(f[思考] {content[:150]}...) elif msg.type tool: thought_chain.append(f[工具结果] {msg.content[:150]}...) return thought_chain把这个思考链和最终的CodeReviewResult一起返回给前端我就能清楚地看到“哦Agent先发现这里用了FindObjectOfType思考后认为这是性能隐患然后去查了Unity手册确认最佳实践最后给出了使用[SerializeField]注入的建议。”这极大地增强了调试和信任度。4.4 服务集成FastAPI接口封装最后把所有组件封装成整洁的FastAPI接口from fastapi import FastAPI, HTTPException from pydantic import BaseModel import logging app FastAPI(titleUnity C# Code Review Agent API) logger logging.getLogger(__name__) class ReviewRequest(BaseModel): code: str session_id: Optional[str] None # 用于追踪会话 class ReviewResponse(BaseModel): success: bool data: Optional[CodeReviewResult] None reasoning_chain: Optional[List[str]] None # 思考链 error: Optional[str] None app.post(/api/review, response_modelReviewResponse) async def review_code(request: ReviewRequest): 主审查接口 try: # 1. 意图过滤 if not is_code_review_intent(request.code): return ReviewResponse( successFalse, error输入内容似乎不是有效的Unity C#代码请提供需要审查的代码片段。 ) # 2. 执行结构化审查链 raw_result await structured_chain.ainvoke({code: request.code}) # 3. (可选) 如果需要Agent的深度分析可以再调用agent_graph # agent_result await agent_graph.ainvoke(...) # 并提取思考链 extract_agent_thought_process(agent_result[messages]) # 4. 返回结果 return ReviewResponse( successTrue, dataraw_result, reasoning_chain[使用结构化审查链直接分析。] # 简化示例 ) except Exception as e: logger.exception(f代码审查处理失败: {e}) raise HTTPException(status_code500, detail内部服务错误)现在一个具备生产雏形的代码审查Agent服务就搭建完成了。它接收代码片段进行意图判断执行审查返回结构化的JSON结果和可选的思考过程。5. 高级技巧与避坑指南在实战中我积累了一些让Agent更可靠、更高效的技巧。5.1 温度Temperature与重复惩罚Repetition Penalty的调优LLM有两个关键参数直接影响代码审查的质量Temperature温度控制输出的随机性。对于代码审查这种需要严谨、确定性的任务应该设置较低的值如0.1-0.3。温度太高它可能会“发明”一些不存在的API或给出不一致的建议。Repetition Penalty重复惩罚防止模型陷入循环重复输出相同的问题点。可以设置为1.1左右。llm ChatOpenAI( modeldeepseek-chat, temperature0.2, # 低温度输出稳定 # 某些API可能叫frequency_penalty或通过其他参数控制 # 例如OpenAI格式frequency_penalty0.1, presence_penalty0.1 # 请根据实际使用的LLM API文档调整 max_tokens2000, # 足够长的输出 )5.2 上下文管理处理长代码文件Unity脚本可能很长。DeepSeek支持128K上下文但把整个1000行的脚本直接扔进去不仅成本高而且模型可能无法聚焦细节。我的策略是分段审查对于长文件按类或按方法拆分成多个片段分别发送审查再汇总结果。摘要重点先让模型对整体代码结构做个摘要然后针对摘要中提到的潜在风险点如“发现多处使用FindObjectOfType”再提取相关代码片段进行深入审查。使用RAG检索增强生成为团队维护一个“最佳实践知识库”和“常见问题代码片段库”。当审查代码时先从这个知识库中检索相似案例和解决方案作为上下文提供给LLM让审查建议更精准、更符合团队规范。5.3 工具设计的艺术给Agent真正的“超能力”工具不是越多越好而是要精准。对于Unity代码审查Agent我构思了这些真正有用的工具search_unity_api(api_name): 连接Unity官方Scripting API实时查询某个方法的使用说明、性能备注和替代方案。check_asset_reference(path): 检查代码中引用的资源路径如Resources.Load(xxx)在项目中是否存在避免运行时错误。analyze_shader_complexity(shader_code): 调用一个简单的分析器评估Shader的指令数判断是否可能造成GPU瓶颈。suggest_design_pattern(problem_context): 根据代码中的设计问题如紧耦合从模式库中推荐合适的设计模式如观察者模式、状态模式。每个工具都要有清晰、严格的docstring说明其用途、输入、输出和调用条件。5.4 成本与性能优化LLM API调用是按Token收费的。一个团队每天产生大量代码成本必须控制。缓存对完全相同的代码片段审查结果可以缓存一段时间如24小时。可以使用redis或memcached。流式响应对于较长的审查结果使用FastAPI的StreamingResponse逐步返回提升用户体验。异步批处理在CI/CD流水线中可以对多个提交的代码进行异步批处理审查减少频繁建立连接的开销。模型分级简单的语法检查、命名规范检查可以用更小、更便宜的模型如DeepSeek Coder的小尺寸版本。只有复杂的逻辑和设计问题才调用大模型。6. 部署与集成让Agent融入开发流水线一个孤立的API服务价值有限。真正的威力在于与现有工具链集成。6.1 Unity编辑器插件使用Unity的EditorWindowAPI创建一个自定义窗口。开发者可以在里面粘贴代码点击审查结果会以富文本形式显示问题点可以点击跳转到对应行。甚至可以集成到Inspector窗口当选中一个脚本组件时自动分析其代码。6.2 CI/CD流水线集成如GitLab CI, Jenkins在git push或创建合并请求Merge Request时触发。# .gitlab-ci.yml 示例 code_review: stage: test script: - | # 提取本次提交变更的C#文件 CHANGED_FILES$(git diff --name-only HEAD~1 HEAD | grep \.cs$) for FILE in $CHANGED_FILES; do # 发送代码到审查Agent API RESPONSE$(curl -s -X POST ${AGENT_API_URL}/review \ -H Content-Type: application/json \ -d {\code\: \$(cat $FILE | jq -Rs .)\}) # 解析响应如果有严重问题则标记失败 HAS_CRITICAL$(echo $RESPONSE | jq .data.has_critical_issue) if [ $HAS_CRITICAL true ]; then echo ❌ 文件 $FILE 存在严重代码问题请检查审查报告。 exit 1 fi done only: - merge_requests6.3 与IDE如VS Code, Rider集成开发一个语言服务器协议LSP插件。这样开发者在编写代码时就能实时看到下划线的警告或建议就像内置的编译器警告一样。7. 总结与展望构建这个Unity C#代码审查Agent的全过程是一次从理论到实践的深度穿越。从最基础的LangChain链到引入工具调用和ReAct范式的智能体再到最后为生产环境加固的意图过滤、结构化输出和可观测性设计每一步都解决了实际开发中的一个痛点。这个项目的核心价值不在于替代人工审查而在于赋能。它像是一个不知疲倦的初级审查员可以抓出那些显而易见的规范错误和常见性能陷阱让资深工程师能更专注于架构设计、算法逻辑等更深层次的问题。它统一了团队的代码规范将最佳实践固化到自动化流程中。未来这个Agent还有很大的进化空间多模态结合Unity的Scene视图或Prefab审查代码与场景对象的关联是否合理。个性化学习团队的代码历史和评审记录给出更符合团队习惯的建议。主动学习当开发者采纳或拒绝它的建议时形成反馈闭环持续优化模型。AI Agent在软件开发领域的应用才刚刚开始。从代码审查出发我们可以构建测试用例生成Agent、性能剖析Agent、甚至自动修复Bug的Agent。这个项目是一个起点它验证了这条路是可行的并且已经能产生实实在在的价值。