【学习笔记】Unity C# 代码审查 Agent 开发记录:第四阶段(Memory、流式输出与部署)

前言

按照个人学习计划,第四阶段(原计划 4.20-4.26)的任务是让 Agent 具备多轮对话记忆、流式输出,并部署到公网。实际执行中因事推迟了几天,于 4.29-5.4 完成。本文记录这一阶段的实现过程、代码要点及踩坑记录。

实际完成时间线

原计划日期任务实际完成日期
4.20LangChain Memory 学习4.29
4.21对话历史管理接口5.2
4.22前端聊天界面5.2
4.23流式输出 Agent 思考过程5.4
4.24部署后端到 Railway5.4
4.25部署前端到 Netlify5.4

一、LangChain Memory 与多轮对话

要让 Agent 支持多轮对话,需要将之前的对话历史保存下来,并在每次请求时一并发送给 LLM。

1. 数据模型

在 models.py 中新增请求模型,添加 session_id 字段用于区分不同会话:

class AgentMemoryRequest(BaseModel):
    input: str = Field(..., description="用户输入")
    session_id: str = Field(default="default", description="会话ID")

2. 会话存储

在 agent_api.py 中,使用字典管理多用户会话历史:

session_store: Dict[str, list] = {}

def get_session_history(session_id: str) -> list:
    if session_id not in session_store:
        session_store[session_id] = []
    return session_store[session_id]

def add_message_to_session(session_id: str, message):
    session_store.setdefault(session_id, []).append(message)

3. Memory 接口

新建 /agent/memory 接口,将历史消息拼接到当前消息前发送给 LLM,并在收到回复后保存本轮对话:

@app.post("/agent/memory")
async def agent_with_memory(request: AgentMemoryRequest):
    history = get_session_history(request.session_id)
    # 拦截逻辑:只有全新会话且不含代码关键词时才拦截
    if not history and not is_code_review_intent(request.input):
        return UnifiedResponse(success=True, code=200, message="非代码请求已拦截", ...)

    messages = [SystemMessage(content="你是专业的 Unity C# 代码审查助手...")]
    messages.extend(history)
    messages.append(HumanMessage(content=request.input))

    result = await agent_graph.ainvoke({"messages": messages})

    add_message_to_session(request.session_id, HumanMessage(content=request.input))
    add_message_to_session(request.session_id, result["messages"][-1])

    return UnifiedResponse(success=True, data={"output": result["messages"][-1].content})

测试:先用 "public int age;" 开启会话,再问 "刚才那个字段有什么问题?",Agent 能准确复述之前的审查内容。


二、对话历史管理接口

为了方便管理,增加了查看和清除历史的接口:

  • GET /agent/memory/history?session_id=xxx:返回指定会话的历史记录
  • DELETE /agent/memory/history?session_id=xxx:清除指定会话历史

实现中需要注意:GET 和 DELETE 没有请求体,参数通过 Query 传递。例如:

@app.get("/agent/memory/history")
async def get_memory_history(session_id: str = Query(..., description="会话ID")):
    history = get_session_history(session_id)
    # ... 构造返回数据

三、流式输出(SSE)

问题ainvoke 需要等待 Agent 完全执行完毕才返回,用户体验不佳。希望实现类似 ChatGPT 的打字机效果。

方案:使用 LangGraph 的 astream_events 方法,并通过 FastAPI 的 StreamingResponse 实现 SSE。

后端

@app.post("/agent/memory/stream")
async def agent_with_memory_stream(request: AgentMemoryRequest):
    # ... 准备 messages ...
    async def stream_events():
        async for event in agent_graph.astream_events({"messages": messages}, version="v2"):
            kind = event.get("event")
            if kind == "on_chat_model_stream":
                chunk = event["data"]["chunk"]
                yield f"data:{json.dumps({'type':'text','content':chunk.content})}\n\n"
            elif kind == "on_tool_start":
                yield f"data: {json.dumps({'type':'tool_start','content':f'调用工具: {event["name"]}...'})}\n\n"
            # ... 其他事件处理 ...
    return StreamingResponse(stream_events(), media_type="text/event-stream")

前端:使用 fetch + ReadableStream 逐块解析 SSE 数据,动态更新消息气泡。

const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    // 解析 data: 行,更新 UI
}

四、前后端部署

后端 Railway

  1. 编写 requirements.txt 和 runtime.txt(指定 Python 3.12)
  2. 推送代码到 GitHub
  3. 在 Railway 新建 Web Service,设置 Start Command uvicorn main:app --host 0.0.0.0 --port 10000
  4. 添加环境变量 DEEPSEEK_API_KEY
  5. 获得公网地址

前端 Netlify

  1. 修改 index.html 中 API_BASE 为 Railway 地址
  2. 将项目导入 Netlify,自动部署
  3. 注意:runtime.txt 会使 Netlify 误判为 Python 项目,需加入 .gitignore

五、小结

这一阶段完成了:

  • ✅ Memory 多轮对话
  • ✅ 历史管理接口
  • ✅ 前端聊天界面
  • ✅ 流式输出
  • ✅ 后端 Railway + 前端 Netlify 部署

项目已可公网访问,具备实际演示能力。后续将继续优化提示词和管理 Token 消耗。

更多推荐