AI 초안과 사람이 실제 전달한 한마디 — 피드백을 두 컬럼으로 쪼갠 이유
ai_feedback_content 와 real_feedback_content 를 굳이 분리한 세 가지 이유. UNSAFE 분류를 AI worker 가 자동 보강하되 사람이 채운 칸은 안 건드리는 규칙, 그리고 코칭의 '발행'과 '전달'을 read_date 한 칸으로 구분한 디테일.
2026년 8월 16일 · 정윤환 · 7분
BBS 가 정착된 회사와 정착 안 된 회사의 결정적 차이는 관찰 이 아니라 피드백 에서 갈립니다.
- 안 정착된 회사 — 관찰자가 카드를 채우고 제출하면 끝. 관찰 대상자는 자기가 관찰됐다는 사실조차 모를 때가 많습니다.
- 정착된 회사 — 관찰이 끝나면 관찰자가 대상자에게 그 자리에서 “방금 안전모 끈이 풀려있던데, 사다리에서 머리 부딪치면 진짜 크게 다칩니다” 같은 한마디를 건넵니다. 그 한마디가 BBS 의 본체입니다.
문제는 그 한마디를 어떻게 적게 만드느냐 입니다. 관찰자도 사람이라 12개 행동을 평가하고 나면 피곤합니다. 피드백 칸을 비우고 제출 버튼을 누릅니다. 그래서 우리는 AI 가 초안을 만들어 주기로 했어요.
01 · Two columns
두 컬럼을 굳이 분리한 이유
class BbsObsFeedback(Base):
"""관찰 1건에 대한 전체 종합 피드백"""
...
ai_feedback_content = Column(Text, nullable=True,
comment='AI 가 생성한 초안 피드백')
real_feedback_content = Column(Text, nullable=True,
comment='관찰자가 실제로 전달한 피드백')한 컬럼에 “AI 가 만든 걸 사람이 다듬어서 저장” 하면 더 간단합니다. 그런데 두 컬럼으로 분리하지 않으면 사라지는 정보가 있어요.
- AI 가 무엇을 제안했는가 — AI 의 제안 품질을 추적하지 못합니다. 다음 모델 버전이 더 나은지 비교할 수 없어요.
- 사람이 무엇을 바꿨는가 — “AI 가 너무 부드럽게 썼는데 관찰자가 더 단호하게 바꿨다” 같은 사용자 행동 패턴이 사라집니다. 이 패턴은 다음 프롬프트 튜닝의 핵심 단서입니다.
- AI 를 안 쓰고 직접 적었는가 —
ai_feedback_content = NULL이지만real_feedback_content가 채워져 있다면 AI 없이 직접 쓴 관찰자 입니다. 그 사람은 BBS 가 손에 익은 베테랑입니다. 이 분포가 사업장 BBS 성숙도의 신호입니다.
세 정보 모두 데이터 모델에 두 컬럼으로 분리해놓아야 살아남는 정보 입니다. 분석을 위해서가 아니라, 분리해두지 않으면 나중에 분석하고 싶어졌을 때 이미 늦었기 때문 입니다.
02 · Two layers of feedback
행동 1개에 피드백, 관찰 1건에 종합 피드백 — 두 층
피드백은 두 층에 붙습니다.
BbsObs (1) ──→ (N) BbsObsBehavior (1) ──→ (N) BbsObsBehaviorFeedback
│ (행동 1건에 대한 피드백)
│
└──→ (N) BbsObsFeedback
(관찰 전체에 대한 종합 피드백)행동마다 피드백을 따로 두는 이유는 단순합니다. “안전모 끈은 잘 매고 있었습니다. 그런데 사다리 3단째에 한 손으로 뭘 잡으면서 올라간 건 위험합니다.” 같은 한 관찰 안의 칭찬과 지적이 한 컬럼에 섞이면, 그 행동만 단독으로 빼서 보여줄 때 깨집니다.
종합 피드백은 행동들을 모두 본 다음의 한 문단 입니다. 끝맺음 문장이라 별도로 둡니다. AI 가 모든 행동을 종합해 자연어 한 문단을 만들고, 관찰자가 그 자리에서 다듬어 대상자에게 전달합니다.
03 · Worker fills blanks · never overwrites
Worker 가 사람의 빈칸을 메운다 — 단, 사람이 채운 칸은 안 건드린다
피드백뿐만 아니라 위험 분류 도 사람의 부담입니다. 관찰자가 UNSAFE 만 누르고 위험 이벤트·요소·통제조치 칸을 비워두는 일이 흔합니다. 그래서 우리는 worker 를 띄웠습니다.
class BbsObsBehavior(Base):
risk_analysis_status = Column(
Enum(BbsObsBehaviorRiskAnalysisStatus), # NULL/PROCESSING/FAILED/COMPLETED
nullable=True,
comment='위험분석 자동 생성 상태. NULL=아직 worker 손길 없음',
)
risk_analysis_attempt_count = Column(
Integer, nullable=False, default=0,
comment='worker 분석 시도 횟수. 일정 횟수 이상 실패 시 더 이상 재시도하지 않음',
)
risk_analysis_last_attempted_at = Column(DateTime, nullable=True,
comment='worker 가 마지막으로 처리 시도한 시각 (UTC)')
risk_analysis_failure_reason = Column(Text, nullable=True,
comment='최종 실패 사유 (status=FAILED 일 때)')
is_risk_analysis_ai_generated = Column(Boolean, nullable=False, default=False,
comment='위험분석 필드를 AI 가 자동 생성했는지 여부')worker 의 규칙은 두 줄입니다.
behavior_result=UNSAFE인데 분류 컬럼이 비어 있으면 큐에 넣는다.- 사용자가 직접 채운 row 는
is_risk_analysis_ai_generated=False인 채 그대로 두고, 절대 건드리지 않는다.
auto domain
- UNSAFE 이지만 분류 비어있음
- AI 가 채움 → is_ai_generated = True
- 사람이 덮어쓰면 False 로 굳음
human domain
- 사용자가 직접 채운 row
- is_ai_generated = False
- worker 가 절대 손대지 않음
특히 두 번째 규칙이 핵심입니다. AI 자동화의 가장 흔한 사고는 “사용자가 의도적으로 비워둔 칸” 을 AI 가 채우거나 “사용자가 의도적으로 채운 칸” 을 AI 가 덮어쓰는 것입니다. 그래서 AI 가 생성한 row 와 사람이 만진 row 를 boolean 한 칸으로 갈라놓았어요.
04 · Worker must know how to stop
Worker 가 멈출 줄도 알아야 한다
자동화 worker 의 두 번째 흔한 사고는 무한 재시도 입니다. 외부 API 가 죽었을 때, AI 모델이 잘못된 입력으로 영구 실패할 때, worker 가 같은 row 를 5분마다 시도하면 토큰 비용이 무한히 빠집니다.
그래서 risk_analysis_attempt_count 와 risk_analysis_failure_reason 을 같이 박았습니다. worker 는 시도 횟수가 임계값을 넘으면 그 row 를 영구 실패 로 두고 큐에서 뺍니다. risk_analysis_status=FAILED, risk_analysis_failure_reason 에 마지막 사유. 어드민 화면에서 “AI 가 못 메운 행동” 으로 따로 보이고, 관리자가 손으로 채우거나 다시 큐에 넣을 수 있습니다.
“AI 가 알아서 한다” 와 “무한히 시도한다” 사이의 간격을 데이터 모델로 메운 거예요.
05 · Coaching the observer
Coach — BBS 관찰자를 또 누가 코칭한다
BBS 의 마지막 층은 코칭 입니다. 관찰자가 동료를 잘 본 다음, 그 관찰자를 우리(플랫폼) 어드민이 직접 코칭합니다. “이 카드에 SAFE 가 너무 많아요. 보통 일주일에 12건 보면 UNSAFE 가 2~3건은 나오는데, 너무 후한 평가 같습니다.” 같은 한마디를 어드민이 관찰자에게 던집니다.
class BbsCoachCard(Base):
monitoring_admin_user_id = Column(String(36), ...) # 코칭 수행 어드민
target_org_member_id = Column(String(36), ...) # 코칭 대상
obs_id = Column(String(36), ...) # 데이터 주체인 관찰 건
monitoring_order = Column(Integer, ...) # 같은 멤버 코칭 회차
strength_memo = Column(Text, ...) # 강점
improvement_memo = Column(Text, ...) # 개선
conclusion_memo = Column(Text, ...) # 결론
next_practice_memo = Column(Text, ...) # 다음 실습 계획
read_date = Column(DateTime, nullable=True,
comment='코칭 대상자가 상세 화면에 진입한 시각')monitoring_order 가 회차를 추적합니다. 같은 멤버에게 1차 코칭에서 “안전모 결속을 더 꼼꼼히 봅시다” 라고 했으면, 3차 코칭 때 그게 정착됐는지 보면 됩니다.
read_date 한 줄이 인상적입니다. 어드민이 코칭 카드를 발행해도, 대상자가 상세 화면에 진입한 시각 이 없으면 이 코칭은 전달되지 않은 것 으로 간주합니다. ‘발행’ 과 ‘전달’ 은 다르다 라는 BBS 의 인간적 진실을 컬럼 한 칸으로 박았어요.
Next
다음 편 — BBS 의 가장 사소해 보이는 두 줄 — GPS 위경도 — 가 어떻게 ‘책상 BBS’ 와 ‘현장 BBS’ 를 가르는지.