山东大学软件学院项目实训-创新实训-大数据租房推荐智能体-前端部分(5)
在实现了前端的对话数据持久化(依靠后端存储,前端主要与后端进行对接)之后,我这一次实现了计划书中预想的租房推荐markdown的输出部分,直接根据房源list进行输出markdown,保证无乱码。
因此,本阶段我投入精力重构了报告生成模块,核心思路是“模板约束 + 结构化渲染”。
1. 架构抉择
在开发初期,我尝试让大模型直接输出完整的 Markdown 报告。但在测试中发现,模型经常会输出未闭合的代码块、错乱的表格管道符(|),导致 Streamlit 渲染崩溃。
为了解决这个问题,我确立了新的架构原则:
- 模型只负责“填空”:让模型仅输出纯文本的分析结论。
- 代码负责“骨架”:使用 Jinja2 模板引擎拼接标题、表格和固定章节。
这样既保证了报告的格式永远工整,又确保了任务书要求的“房源摘要、评分理由、核心参数”三大要素齐全。
2. 核心实现:report_builder.py 的模板化设计
这是本阶段最核心的代码文件。我引入了 Jinja2 模板引擎,将报告拆解为固定的结构。
强约束结构:无论模型输出什么,报告必定包含“摘要”、“推荐列表”、“评分依据”等固定章节。
数据与展示分离:Python 负责将房源列表(Listings)和评分(Scores)组装成字典,传给模板渲染,彻底杜绝了 Markdown 表格错位的问题。
双轨展示策略:针对 Streamlit 对复杂 Markdown 支持有限的问题,我在代码中预留了逻辑——核心参数表既生成 Markdown 文本,也支持在渲染层转换为 st.dataframe 进行二次展示。def _sanitize_plain(text: str, *, max_len: int = 4000) -> str: """避免未闭合代码块破坏整页:弱化 ```;控制长度。""" s = str(text).replace("\r\n", "\n").strip() s = s.replace("```", "'''") if len(s) > max_len: s = s[: max_len - 3] + "..." return s
def _sanitize_plain(text: str, *, max_len: int = 4000) -> str:
"""避免未闭合代码块破坏整页:弱化 ```;控制长度。"""
s = str(text).replace("\r\n", "\n").strip()
s = s.replace("```", "'''")
if len(s) > max_len:
s = s[: max_len - 3] + "..."
return s
def build_report_md(
listings: list[dict[str, Any]],
score_rows: list[dict[str, Any]],
user_query_summary: str,
*,
risk_notes: str | None = None,
) -> str:
"""
输出完整 Markdown:房源摘要、评分理由、核心参数(表格在渲染层辅之以 dataframe)。
"""
items = [normalize_listing(x) for x in listings if isinstance(x, dict)]
score_by_id = _index_scores(score_rows)
rows: list[dict[str, Any]] = []
for li in items:
lid = str(li.get("listing_id", ""))
sr = score_by_id.get(lid)
title = (li.get("title") or "").strip() or "(标题待补充)"
rows.append(
{
"listing_id": lid,
"title": _sanitize_plain(title, max_len=200),
"title_short": _sanitize_plain(title, max_len=60),
"price_text": _fmt_price(li),
"area_text": _fmt_area(li),
"rooms": _sanitize_plain(str(li.get("rooms") or "户型待补充"), max_len=80),
"district": _sanitize_plain(str(li.get("district") or "区域待补充"), max_len=80),
"community": _sanitize_plain(str(li.get("community") or "小区待补充"), max_len=80),
"score_text": _fmt_score(li, sr),
"rationale": _rationale_for(li, sr),
}
)
env = Environment(
loader=FileSystemLoader(str(_TEMPLATE_DIR)),
autoescape=False,
trim_blocks=True,
lstrip_blocks=True,
)
tpl = env.get_template(_TEMPLATE_NAME)
uq = _sanitize_plain(user_query_summary or "(本轮未捕获用户需求摘要)", max_len=2000)
risk_section = (
_sanitize_plain(risk_notes, max_len=2000)
if risk_notes and str(risk_notes).strip()
else "(默认提示)实地看房、核验产权与户口、贷款与税费方案请咨询专业机构;本报告由系统自动生成,不构成投资建议。"
)
table_lines = [
"",
"### 核心参数表",
"",
"| 序号 | 标题 | 总价 | 面积 | 户型 | 区域 | 小区 | 综合分 |",
"| --- | --- | --- | --- | --- | --- | --- | --- |",
]
for i, li in enumerate(items, start=1):
sr = score_by_id.get(str(li.get("listing_id", "")))
table_lines.append(
"| "
+ " | ".join(
[
str(i),
_sanitize_cell(li.get("title") or "—"),
_sanitize_cell(_fmt_price(li)),
_sanitize_cell(_fmt_area(li)),
_sanitize_cell(li.get("rooms")),
_sanitize_cell(li.get("district")),
_sanitize_cell(li.get("community")),
_sanitize_cell(_fmt_score(li, sr)),
]
)
+ " |"
)
core_table_md = "\n".join(table_lines) + "\n"
md = tpl.render(
user_query_summary=uq,
rows=rows,
core_table_md=core_table_md,
risk_section=risk_section,
)
return md
3. 安全渲染与双轨展示策略
有了构建器还不够,还需要一个安全的渲染器 report_render.py。在 Streamlit 中直接渲染外部生成的 Markdown 存在风险,因此我增加了自动化检查机制。
实现逻辑:
章节完整性校验:在渲染前,通过 validate_report_sections 检查生成的 Markdown 是否包含“房源摘要”、“评分依据”等关键词。如果缺失,前端会弹出警告而不是渲染半截页面。
双轨展示(Double-Track):这是本阶段的一个体验优化。考虑到 Streamlit 对复杂 Markdown 表格的支持有限,我渲染了 Markdown 文本,还额外调用 st.dataframe 展示了一份交互式表格。用户既能看报告,又能对表格进行排序筛选。
UTF-8 导出:下载按钮显式指定了 charset=utf-8,解决了中文乱码问题。
def render_report_tab(
markdown_text: str,
*,
listings: list[dict[str, Any]] | None = None,
show_dataframe: bool = True,
) -> None:
"""
安全渲染 Markdown(默认关闭 HTML);结构不完整时警告。
核心参数表同时用 dataframe 展示,缓解 Streamlit 下复杂表格限制。
"""
ok, missing = validate_report_sections(markdown_text)
if not ok:
st.warning("报告结构不完整,缺失:" + "、".join(missing))
try:
st.markdown(markdown_text, unsafe_allow_html=False)
except Exception as e: # noqa: BLE001 — 渲染兜底
st.error(f"Markdown 渲染失败:{e}")
st.text(markdown_text[:8000])
if show_dataframe and listings:
items = [x for x in listings if isinstance(x, dict)]
if items:
st.subheader("核心参数(表格补充)")
try:
st.dataframe(
_listings_to_rows(items),
use_container_width=True,
hide_index=True,
)
except Exception as e: # noqa: BLE001
st.caption(f"表格展示失败:{e}")
col_copy, col_dl = st.columns(2)
with col_dl:
st.download_button(
label="下载报告 (.md)",
data=markdown_text.encode("utf-8"),
file_name="选房分析报告.md",
mime="text/markdown; charset=utf-8",
use_container_width=True,
)
with col_copy:
with st.expander("复制全文(选中复制)", expanded=False):
st.caption("UTF-8 文本;亦可使用上方下载保存为文件。")
st.text_area(
"report_md_copy",
value=markdown_text,
height=280,
label_visibility="collapsed",
key="report_md_copy_area",
)
4. 总结与展望
通过引入 Jinja2 模板和自动化校验,我们彻底解决了“大模型输出格式不可控”的顽疾。
稳定性提升:无论模型如何发挥,报告的结构永远符合任务书的要求。
体验优化:用户不仅能看到结构清晰的报告,还能通过双轨表格进行交互,并一键下载无乱码的文件。
至此,我们的智能体不仅能“记住”历史,还能“输出”专业的书面报告。
更多推荐
所有评论(0)